Why is $scope.$apply necessary to update this array

251 Views Asked by At

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

2

There are 2 best solutions below

2
ginman On BEST ANSWER

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?

5
Yaser On

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;
});