Linell
Linell

Reputation: 749

Angular Directive Template Update on Data Load

I have a directive whose data is being received via an api call. The directive itself works fine, the problem arises (I believe) because the directive is loaded before the api call finishes. This results in the whole shebang just not working. Instead of my expected output, I just get {{user}}.

My directive looks like this:

app.directive('myDirective', function() {
  return {
    restrict: 'A',
    require: '^ngModel',
    scope: {
      ngModel: '=',
    },
    template: '<tbody style="background-color: red;" ng-bind-html="renderHtml(listing_html)"></tbody>',
    controller: ['$scope', '$http', '$sce',
      function($scope, $http, $sce) {
        $scope.listing_html += "<td>{{user.name}}</td>"
        $scope.renderHtml = function(html_code) {
          return $sce.trustAsHtml(html_code);
        };
      }
    ],
    link: function(scope, iElement, iAttrs, ctrl) {
      scope.$watch('ngModel', function(newVal) {
        // This *is* firing after the data arrives, but even then the
        // {{user}} object is populated. And the `user in ngModel` doesn't
        // run correctly either.
        console.log(scope.ngModel);
        scope.listing_html = "<tr ng-repeat='user in ngModel'><td>{{user}}</td></tr>"
      })
    }

  };
});

And my html is simply

<table my-directive my-options='{"Name": "name", "Email": "email"}' ng-model='userData'></table>

I've created a plunker with a ton of comments to hopefully help explain the issue.

This question is very similar to this one, with the key distinction of that solution not working. Adding ng-cloak to mine just makes it not display. It may also be worth noting that I've been using this as reference on the way to construct a directive.

Upvotes: 0

Views: 1116

Answers (2)

gkalpak
gkalpak

Reputation: 48212

I am not 100% sure, but I believe that ngBindHtml will not help you in this case.
ngBindHtml is for displaying some "normal" HTML, but you want to display some Angular, magic HTML.
For that you need to $compile the HTML to something that is Angular-aware and link the compiled HTML to a scope.

I used the following approach (with apparently good results):

controller: function ($scope, $element, $compile) {
  var html = createTmpl(angular.fromJson($scope.myOptions));
  $scope.$watch('ngModel', function (newVal) {
      var elem      = angular.element(html);   // Creating element
      var linkingFn = $compile(elem);          // Compiling element
      linkingFn($scope);                       // Linking element
      $element.html('');                       // Removing previous content
      $element.append(elem);                   // Inserting new content

      // The above is purposedly explicit to highlight what is 
      // going on. It's moe concise equivalent would be:
      //$element.html('').append($compile(html)($scope));
  });

where createTmpl() is defined to take into account myOptions and return the appropriate template for creating a table with a header-row (based on the keys of myOptions) and data-rows with the properties defined as myOptions's values:

function createTmpl(options) {
    // Construct the header-row
    var html = '<tr>';
    angular.forEach(options, function (value, key) {
        html += '<th>' + key + '</th>';
    });
    html += '</tr>\n';

    // Construct the data-rows
    html += '<tr ng-repeat="user in ngModel">';
    angular.forEach(options, function (value, key) {
        html += '<td>{{user' + value + '}}</td>';
    });
    html += '</tr>\n';

    // Return the template
    return html;
}

See, also, this short demo.
Of course, this is for demonstration purposes only and does not handle everything a production-ready app should (e.g. accounting for errors, missing properties, changes in myOptions and whatnot).


UPDATE:

I had very strong competion, so I did a slight modification of the code above in order to support nested properties. E.g. given an object with the following structure:

user = {
    name: 'ExpertSystem',
    company: {
        name: 'ExpertSystem S.A.',
        ranking: 100
    }
};

we can have the company name displayed in a column of our table, just by defining myOptions like this:

myOptions='{"Company name": "company.name"}

Upvotes: 1

andrew.burk
andrew.burk

Reputation: 533

I think you're making this a bit more complicated that it needs to be. If you're going to try to insert dynamic HTML with Angular expressions in them, you need to use the $compile service to compile them first (this hooks up the directives, etc, in that dynamic HTML to Angular). With that said, I don't think you need to do that for what you're trying to accomplish.

Take a look at this updated plunk: http://plnkr.co/edit/RWcwIhlv3dMbjln4dOyb?p=preview

You can use the template in the directive to produce the dynamic changes you need. In my example, I've used ng-repeat to repeat over the users provided to the directive, and also to the options provided to the directive. ng-repeat does the watching, so as soon as the data provided to the directive via ng-model is updated, the ng-repeats reflect those changes.

<tbody style="background-color: red;">
    <tr><th ng-repeat="option in myOptions">{{option.name}}</th></tr>
    <tr ng-repeat="user in ngModel">
        <td ng-repeat="option in myOptions">{{user[option.value]}}</td>
    </tr>
</tbody>

The options I defined in the main controller like this.

$scope.tableOptions = [
  {"name": "Name", "value": "name"}, 
  {"name": "Email", "value": "email"}
];

You could add other properties to this that are used by the directive, such as display order, etc. You could even remove an item from the options dynamically and that data would then be removed from the output table.

Let me know if this helps, or if I've misunderstood what you were trying to accomplish.

Upvotes: 2

Related Questions