2

I am using Angular 1.5.9 and angular-route 1.5.9. I have a working solution, but it doesn't make sense to me that I would need $scope.$apply() in order to update the DOM.

I am using a factory and a controller to update an array that is used as within an ng-repeat. I understand that the digest cycle is not being triggered, which is why I need to use $scope.apply(), but I don't understand why. Here is the code that is failing:

myApp.controller('MyGamesController', ['$http', '$firebaseAuth', 'DataFactory', '$scope', function ($http, $firebaseAuth, DataFactory, $scope) {
  console.log('mygamescontroller running');
  var self = this;
  self.newGame = {}
  self.games = [];

  getGames();

  function getGames() {
        DataFactory.getGames().then(function (response) {
          console.log('returned to controller from factory', response); // logs the correct response including lastest data, but DOM doesn't update
          self.games = response; // self.games is correctly set, but not updated on the DOM
          $scope.$apply(); // Updates the DOM 
        });
  } //end getgames function


  self.addGame = function () {
    DataFactory.addGame(self.newGame).then(getGames);
  }

}]); //end controller

Angular Router is handling my controllerAs like so:

  .when('/mygames' ,{
    templateUrl: '/views/templates/mygames.html',
    controller: 'MyGamesController',
    controllerAs: 'mygames'
  })

And the HTML for the ng-repeat portion of the HTML looks like this:

  <tbody>
    <tr ng-repeat="game in mygames.games">
      <td>{{game.game}}</td>
      <td>{{game.number_players}}</td>
      <td>{{game.time_to_play}}</td>
      <td>{{game.expansion}}</td>
    </tr>
  </tbody>

As an aside, I did attempt to clear out the self.games array and create a loop that pushed each one on and that didn't solve the problem. I still needed $scope.$apply() in order to update the DOM.

I have the entire repo (linking specific commit) available here if it helps: https://github.com/LukeSchlangen/solo_project/tree/df72ccc298524c5f5a7e63f4a0f9c303b395bafa

Luke Schlangen
  • 3,722
  • 4
  • 34
  • 69
  • Change `var self = this;` to `var self = $scope;` should update your array, `self.games` – Taku Dec 14 '16 at 04:39

2 Answers2

2

Instead of getting the games array in your controller, you can get it on page load using your routeProvider like this:

Since your page has been rendered at that time of the loading (that array is empty), and there wouldn't be any event which triggers the digest cycle.

.when("/news", {
    templateUrl: '/views/templates/mygames.html',
    controller: 'MyGamesController',
    controllerAs: 'mygames'
    resolve: {
        games: function(){
            return DataFactory.getGames();
        }
    }
}

And in your controller:

app.controller("MyGamesController", function (games) {
    var self = this;
    self.newGame = {}
    self.games = games;
});
Yaser
  • 5,609
  • 1
  • 15
  • 27
  • Cool! But will that fix the problem of it not updating the DOM when I call `addgames`? – Luke Schlangen Dec 14 '16 at 03:46
  • The controller is not initialised until resolve happens @LukeSchlangen – Yaser Dec 14 '16 at 03:49
  • And I think that would work really well for my initial page load. Then I have an `addGame` function called at the bottom of the controller that calls the factory again. Wouldn't I still have that issue of it not updating properly on that call? – Luke Schlangen Dec 14 '16 at 03:53
  • If the same happens you can always use a directive to rerender the DOM on adding an item to list @LukeSchlangen, but the initial load shouldn't be done without required data – Yaser Dec 14 '16 at 03:56
  • I would disagree that it *shouldn't* be done. I think it depends on the use case. Time to first interaction is more important in my opinion, as well as the illusion of progress. Blocking any rendering before data is loaded can lead to a less than ideal UX. Ex. https://www.airbnb.com/ loads each component individually, rather than waiting for all data before an initial render. – ginman Dec 15 '16 at 18:16
2

Angular isn't watching for changes here because the execution of this code is outside of Angular's purvey. The easiest way to have Angular watch for results of a promise is to utilize the $q library.

function getGames() {  
    return $q.resolve(DataFactory.getGames().then(function (response) {
      self.games = response; // self.games is correctly set, but not updated on the DOM
    }));
} //end getgames function

$q promises are managed by Angular, and it will expect to run the digest cycle when a $q promise resolves.

You will also need to include and inject $q into your controller.

myApp.controller('MyGamesController', ['$q', '$http', '$firebaseAuth', 'DataFactory', '$scope', function ($q, $http, $firebaseAuth, DataFactory, $scope) {

This excellent post details how async calls work with the digest cycle: How do I use $scope.$watch and $scope.$apply in AngularJS?

Community
  • 1
  • 1
ginman
  • 1,315
  • 10
  • 20
  • This seems to work great when I run my `addGame` function, but it doesn't seem to show the games on the initial load - when I call `getGames()` for the first time. I'm guessing that I can't call the `$q` library like that on the initial load? – Luke Schlangen Dec 14 '16 at 04:10
  • You should be able to call it on initial load, but the data will not be loaded before the controller is run. This means your view will load, but you wont see games until the data is fetched and the promise resolves. I would recommend the resolve method for vital data as shown in Yaser's answer. The other option is to have contextual loading indicators, which can give a nice UX. – ginman Dec 15 '16 at 18:13