31

I'm having serious troubles understanding AngularJS sometimes. So I have a basic array in my controller like

$scope.items = ["a","b","c"]

I'm ngRepeating in my template over the items array ng-repeat="item in items". Super straightfoward so far. After a couple of UX actions, I want to push some new stuff to my array.

 $scope.items.push("something");

So, 50% of the time, the new element is added to the view. But the other 50%, nothing happens. And it's like super frustrating; bc if I wrap that within $scope.$apply(), I got a "$digest already in progress" error. Wrapping that into $timeout doesn't help either.

And when I inspect my element scope using the Chrome extension; I can see the new data is there and the $scope.items value is correct. But the view is just not taking care of adding that to the DOM.

Thanks!

spacenick
  • 1,171
  • 3
  • 14
  • 19

3 Answers3

21

You are modifying the scope outside of angular's $digest cycle 50% of the time.

If there is a callback which is not from angularjs; (posibbly jquery). You need to call $apply to force a $digest cycle. But you cannot call $apply while in $digest cycle because every change you made will be reflected automatically already.

You need to know when the callback is not from angular and should call $apply only then.

If you don't know and not able to learn, here is a neat trick:

var applyFn = function () {
    $scope.someProp = "123";
};
if ($scope.$$phase) { // most of the time it is "$digest"
    applyFn();
} else {
    $scope.$apply(applyFn);
}
Umur Kontacı
  • 35,403
  • 8
  • 73
  • 96
  • I believe you should avoid checking `$scope.$$phase`. It's internal variable, and you should not rely you code on these, if possible. The best solution would be you to use `$apply` where you are absolutely sure there will be no `$apply` already in progress. And this is why I'm asking for the codes that might update scope. – Caio Cunha Mar 22 '13 at 13:18
  • It is not a good way to code, I do agree. Yet, it does work and `$$phase` is very consistent and reliable; at least it will not break your code unexpectedly, except the fact the it can be changed between releases. – Umur Kontacı Mar 22 '13 at 13:24
  • It is. I've already used it indeed. The question is we don't know if this will break in the next minor release, for its internal state controller. I'm just pondering that there is probably a better place to put the `$apply` securely. – Caio Cunha Mar 22 '13 at 13:27
  • 2
    By "It is" I mean the reliability part. We agree on it being not a good practice. Just to be sure :) – Caio Cunha Mar 22 '13 at 13:44
4

As pointed out by Umur Kontacı, you are making model changes outside of the digest cycle sometimes. However, instead of working around this problem and trying to detect whether you are in an apply/digest context or not, I suggest you make sure this never happens.

The main cause for this problem is that your function is called as a reaction to a DOM event. E.g.

jQuery('.some-element').click(function() { seeminglyProblematicCode() })

This is where your $apply() needs to go, not within the function. Otherwise your whole code will be littered with such distinctions sooner or later. Assuming there is a scope in the context of this event handler, you can write:

jQuery('.some-element').click(function() { 
    $scope.$apply(function() { seeminglyProblematicCode() })
})

However, there is one caveat you have to be aware of: When you trigger a click event from your code, you will run into the problem that a digest cycle is already in progress. This is where you need the $timeout. The answers to this question cover this problem very well.

Community
  • 1
  • 1
lex82
  • 11,173
  • 2
  • 44
  • 69
0

I had the same issue, and my fix was to watch which controllers are being called in nested directives.

# Parent Controller
app.controller 'storeController', ($scope, products) ->

  $scope.cart = ["chicken", "pizza"]
  $scope.addToCart = (item) ->
    $scope.cart.push item

  # from service
  products.get().then (items) ->
    $scope.products = items

# Parent Directives
app.directive 'storeContainer', ($scope, config) ->
  restrict: 'E'
  templatUrl: 'store-container.html'
  controller: 'storeController'

# Nested Directive
app.directive 'storeFront', ($scope, config) ->
  restrict: 'E'
  templatUrl: 'store-front.html'
  controller: 'storeController'

# Parent Template templates/directives/store-container.html
<div ng-repeat="item in cart">{{ item }}</div>
<strore-front></store-front>

# Nested Template templates/directives/store-front.html
<ul ng-repeat="item in products">
  <li ng-click"addToCart(item)">{{ item }}</li>
</ul>

The bug here is the nested directive creates a second controller in prototype chain (a duplicate of storeController), which the parent template doesn't have access to. To resolve write the nested controller like so:

# Nested Directive
app.directive 'storeFront', ($scope, config) ->
  restrict: 'E'
  templatUrl: 'store-front.html'

There are better ways to create the inheritance chain, but this will resolve the issue for many people learning AngularJS.

brettu
  • 417
  • 1
  • 5
  • 13