jollege
jollege

Reputation: 1157

How to update an angular directive with async data

I have a controller which gets json data called content.json. When I render the UI, I can't know if the json is available yet. The controller gets the json data asap when the app loads, it is an array of objects used throughout the entire app for various purposes:

vg.ctrls.controller('vg.ctrls.getContentJson', ['$scope', '$http', 
  function($scope, $http) {
    $http.get('models/content.json').success(function(data) {
      if(data instanceof Array){
        vg.content = data; // store in app global variable
      } else {
        vg.log('vg.ctrls.getContentJson: data not array.');
      }
      $scope.vg = vg;
      // vg.Content.createMenu(); // Document may not be ready yet.
    });
  }]
);

In my UI, I have a navigation menu which is based on the content.json. Obviously a good place to use ng-repeat within a <ul>:

<li ng-repeat="entry in content">
  <a ng-click="vg.Content.setById(entry.id)">{{entry.title}}</a>
</li>

Unfortunately, content.json has not always loaded by the time that the directive is rendered by AngularJS. So, I made a function vg.Content.createMenu() in my app that generates the menu and put it in the callback from $http.get() - but alas, the document is not always ready yet at this point.

I other words, I have two async processes and a function that depends on them both, but I can't tell which one will be done first. I have solved similar problems before without a framework using my own custom events. However, I would like to believe that there is a way of handling this within AngularJS?

How about creating a custom directive that doesn't execute until content.json has loaded?

--------------------- EDIT -----------------------

Here's an intermediate solution:

vg.directives.directive('vgContentMenuByCategory', function(){
  return {
    scope: {
      categoryId: '='
      ,title: '='
      ,content: '='
    }
    ,replace: true
    ,link: function(scope, element, attrs){
      scope.$watch('content', function(content){
        var $ul = element.find('ul.dropdown-menu');
        for (var i = 0; i < content.length; i++) {
          var entry = content[i];
          if(entry.category===scope.categoryId){
            var $li = $('<li><a>'+entry.title+'</a></li>');
            _goToContentClickHandler({$element:$li, id:entry.id});
            $ul.append($li);
          }
        };
      });
    }
    , templateUrl: 'templates/vgContentMenuByCategory.html'
  }
});

This is not really a "proper" solution, though - because I am not using "ng-repeat" in my template but doing so instead within a loop in scope.$watch(). If I use ng-repeat in the template instead, the 'content' hasn't loaded yet. The problem is complicated further because I actually want to use the directive above within another directive that runs through categories - and that also has to wait for content.json to load.

So, how do I make the directive wait with rendering the template or executing any directives within it, before the values have loaded?

Upvotes: 1

Views: 4295

Answers (2)

Justin Goro
Justin Goro

Reputation: 1

Create a content factory:

app.factory('Content',function(){ 
            var category; 
            var title; 
            var id;
            return {
                     category:function(){return category;},
                     title:function(){return title;},
                     id:function(){return id;},
};

Then in your app.js define a function with a get request to retrieve content and populate a Content object in the app.js scope. Finally, when routing to your controller add this function to the resolve list. When defining your controller extend the parameters to

vg.ctrls.controller('vg.ctrls.getContentJson', ['$scope', '$http', 
  function($scope, $http,Content) {

to access the Content object.

Upvotes: 0

jollege
jollege

Reputation: 1157

OK, I figured it out - thanks to your comments and answers and to this tutorial: https://youtu.be/rHmk0UhJSb4.

First, I create a service that gets the data and populates it through a method:

vg.ctrls.service('vg.ctrls.contentService', ['$http', '$q',
  function($http, $q) {
    var deferred = $q.defer();

    $http.get('models/content.json').then(function(data) {
      deferred.resolve(data);
    });

    this.getContent = function(){
      return deferred.promise;
    };

  }]
);

Then, I create a controller that injects the service and puts the data in the scope:

vg.ctrls.controller('vg.ctrls.content', ["$scope", "vg.ctrls.contentService", 
  function($scope, service){
    var promise = service.getContent();
    promise.then(function(o){
      $scope.content = o.data;
    });
}]);

Subsequently, directives within the scope of that controller, will be updated when the data has loaded such as:

<div class="well" id="test" ng-controller="vg.ctrls.testService">
  <span ng-repeat="entry in content">
    <p>{{entry.id}}: {{entry.title}}</p>
  </span>
</div>

Or in a directive like this:

vg.directives.directive('vgContentMenuAllCategories', function(){
  return {
      scope: {
        content: '='
      }
    , controller: 'vg.ctrls.content'
    , link: function(scope, element, attrs){
        //vg.log('vgContentMenuAllCategories');
    }
    , template: 
      '<span ng-repeat="entry in content"><a ng-click="vg.Content.setById(entry.id)">{{entry.title}}</a><br/></span>'
  }
});

To make sure it works when the data arrives later, I modified the service to fetch the data 5 seconds later - and the directive indeed gets updated when the data arrives:

var foo = function(){
  $http.get('models/content.json').then(function(data) {
    deferred.resolve(data);
  });
};
setTimeout(foo, 5000);

Upvotes: 0

Related Questions