KungWaz
KungWaz

Reputation: 1956

ng-model does not update correctly

I have a structure like this (much simplified):

<div ng-controller="MainCtrl">
  <div>Views and other stuff</div>
    <div ng-controller="MyCtrl">
      <div ng-repeat="item in $root.items track by $index">
        {{item.id}}
      </div>
    <div>
        Total value: {{totVal}}
    </div>
  </div>
</div>

And in my main controller I have defined $rootScope.items = [{id: 1,..},{id: 2,..},{id: 3,..},..] and in my other controller I have defined $scope.totVal = 0 and a watcher for the items array which updated the totVal when the items array changes;

.controller('MainCtrl', ['$rootScope', '$scope', function($rootScope, $scope) {
    // Are actually fetched from a service
    $rootScope.items = [
        {id: 1,..},
        {id: 2,..},
        ..
    ];
}])

.controller('MyCtrl', ['$rootScope', '$scope', function($rootScope, $scope) {
    $rootScope.updateTotVal = function() {
        var totVal = 0;
        // calculations are done here
        // Is correct here when I console log after all calculations are complete
        console.log(totVal); 
        $scope.totVal = totVal;
    }

    $scope.totVal = 0;

    $rootScope.$watch('items', function() {
        $rootScope.updateTotVal();
    }, true);
}]);

The problem I have is that the totVal does not update in the view. When I console log the value it displays correctly in the console but it only updates in the view if I scroll the totVal div out of view and back again. Seems like an AngularJS bug in Chrome, but can't find any similar cases when i Google. It works fine in FireFox.

If I move the totVal before the ng-repeat it works and if I remove the ng-repeat it works (I rather don't change the structure of the html since it will be much work and make the design worse). I have also tried to move the totVal to the root scope without any success.

It's only the painting/rendering that fails, Batarang and console.log always gives me the updated value.

Has anyone had this problem before?

Upvotes: 0

Views: 900

Answers (3)

KungWaz
KungWaz

Reputation: 1956

The issue is with Angular and Chrome. If I removed an ng-repeat that was before the totVal output it worked. It also worked if I put totVal higher up in the DOM structure or before the ng-repeat. I think it has to do with the number of bindings and the complexity of the DOM, many nested DIV's breaks the binding somehow. I also noted that if I had many nested bootstrap classes like 'row' and 'col-xs-12' it also broke the binding. So it seems to be related to the browser rendering/painting the view (since scrolling the totVal out and back in to the view updated the value).

The main thing I learnt from this was to use smarter directives and try to simplify the DOM structure, especially if you use bootstrap.

Original:

<div ng-controller="MainCtrl">
  <div>Views and other stuff</div>
  <div ng-controller="MyCtrl">
    <div ng-repeat="item in $root.items track by $index">
      {{item.id}}
    </div>
    <div>
      Total value: {{totVal}}
    </div>
  </div>
</div>

Different changes that made it work properly:

<div ng-controller="MainCtrl">
  <div>Views and other stuff</div>
  <div ng-controller="MyCtrl">
    <div>
      Total value: {{totVal}}
    </div>
  </div>
</div>

<div ng-controller="MainCtrl">
  <div>Views and other stuff</div>
  <div ng-controller="MyCtrl">
    <div ng-repeat="item in $root.items track by $index">
      {{item.id}}
    </div>
  </div>
  <div>
    Total value: {{totVal}}
  </div>
</div>

<div ng-controller="MainCtrl">
  <div>Views and other stuff</div>
  <div ng-controller="MyCtrl">
    <div>
      Total value: {{totVal}}
    </div>
    <div ng-repeat="item in $root.items track by $index">
      {{item.id}}
    </div>
  </div>
</div>

Upvotes: 0

link
link

Reputation: 1676

I don't know if this is exactly what you where looking for because I don't know your use case. However, I achieved a working example, see it here codepen.

I refactored your code a bit, removing the extra controller and encapsulated totVal in a service. Also, I use the controllerAs syntax to make everything clearer.

The problem with your code is the fact that totVal is a primitive. Thus, when you assign it a new value, angular will not be able to update the reference. This is why you should always have a dot in your model!

Take a look at my code, you will se that I declared totVal as var totVal = {val : 0}. I simply update val, the reference is kept and the view is correctly updated.

.service('totVal', function(){
  var totVal = {val:0};

  this.computeTotVal = function(){
    //perform computations, I will just increment
    totVal.val++;
  }

  this.getTotVal = function() { return totVal; }
})

See also this article.

Upvotes: 1

domokun
domokun

Reputation: 3003

I'm not sure if this is the culprit, but you definitely shouldn't use $rootScope for this.

As the doc states:

$rootScope exists, but it can be used for evil

See here: https://docs.angularjs.org/misc/faq#-rootscope-exists-but-it-can-be-used-for-evil


I would rewrite your code without it, when not necessary:

<div ng-controller="MainCtrl">
  <div>Views and other stuff</div>
  <div ng-controller="MyCtrl">
  <!-- You also had an $root.items that didn't match any element here -->
    <div ng-repeat="item in items track by $index">
      {{item.id}}
    </div>
    <div>
      Total value: {{totVal}}
    </div>
  </div>
</div>

And in your controller you will bind your items, like this:

.controller('MainCtrl', ['$scope', function($scope) {
    // Are actually fetched from a service
    $scope.items = [
        {id: 1,..},
        {id: 2,..},
        ..
    ];
}])
.controller('MyCtrl', ['$rootScope','$scope', function($rootScope, $scope) {
    $scope.updateTotVal = function() {
        var totVal = 0;

        // calculations

        console.log(totVal); // Is correct here

        $scope.totVal = totVal;
    }

    $scope.totVal = 0;

    // Here $rootScope makes sense
    $rootScope.$watch('items', function() {
        $scope.updateTotVal();
    }, true);
}]); 

Edit:
Also, everytime you instantiate your controller, you set $rootScope.totalVal = 0, eventually resetting it.
Maybe this is why you don't see the update?

Upvotes: 0

Related Questions