Directive runs $digest on whole ngApp - causing unwanted calls

378 views
Skip to first unread message

Adnan Ibrišimbegović

unread,
Nov 22, 2012, 6:45:57 AM11/22/12
to ang...@googlegroups.com
Example of the problem is shown here: http://jsfiddle.net/adibih/Pcanb/
Example uses "current time" directive from the AngularJS Guide.
It shows how this directive (which runs in loop) causes $digest to run unrelated filter in different controller.
It reruns my filters, which I don't want it to do, and it causes "10 $digest() iterations reached." error.

This can be reproduced using other type of directives (such as AngularUI select2 directive).
I understand that $digest runs through whole ngApp, but I'm more interested in how I can remedy this particular problem.
How can I rewrite this directive so that it doesn't touch the rest of my app?

Andy Joslin

unread,
Nov 22, 2012, 10:23:23 AM11/22/12
to ang...@googlegroups.com
The problem is your filter - Angular will keep checking the result to see if it's changed, then if it's the same twice it will 'resolve' itself.  Your filter is random, so the only way angular will stop checking it is if your random() gets lucky and gives the same result twice in a row.

Adnan Ibrišimbegović

unread,
Nov 22, 2012, 11:00:35 AM11/22/12
to ang...@googlegroups.com
You are correct about the 'resolve' part, but that's not the real issue here.

That random() function is there just for the debugging purposes. If you take a look at the console, you'll notice that the filter function is being called every time the directive is 'refreshed' (every 1000ms), even if you change the filter and instruct it to return a fixed text instead of random number (as in here: http://jsfiddle.net/adibih/C7xMy/).

The problem is that the filter is re-evaluated repeatedly due to the $digest cycle which is being called again and again from within the directive.
Angular directives (as far as I know) are firing $digest cycle on $rootScope, and I'm trying to find a way of going around such behavior.

Andy Joslin

unread,
Nov 22, 2012, 11:50:07 AM11/22/12
to ang...@googlegroups.com
Ah.  Well you could do two things if you don't want the time directive to call a $digest on everything:

1. Change the $timeout to a setTimeout, and then it will never digest anything but just change the element's text.
2. Change $timeout to a setTimeout, and add a scope.$digest() in the timeout instead of $apply. $digest will only affect current and child scopes.

Peter Bacon Darwin

unread,
Nov 22, 2012, 1:21:41 PM11/22/12
to ang...@googlegroups.com
Third option: $timeout has a third parameter:

$timeout(fn[, delay][, invokeApply]);

You can call $timeout passing false to invokeApply then call $digest on your local scope inside the handler.

Pete

--
You received this message because you are subscribed to the Google Groups "AngularJS" group.
To post to this group, send email to ang...@googlegroups.com.
To unsubscribe from this group, send email to angular+u...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular?hl=en-US.
 
 

Adnan Ibrišimbegović

unread,
Nov 22, 2012, 2:10:46 PM11/22/12
to ang...@googlegroups.com
Andy, both of your suggestions worked. Thank you very much. Guess I'll have to learn more about $apply and $digest functions, and the differences between them. Thanks again.

Adnan Ibrišimbegović

unread,
Nov 22, 2012, 2:27:08 PM11/22/12
to ang...@googlegroups.com
Peter, you're right to the point. I'd never think of looking at the $timeout function.
Your solution (as well as Andy's) worked perfectly for this particular directive.

Now I wish someone could explain to me why the $digest, by default, runs from the $rootScope down instead from the current $scope down. This default behavior seems so wrong.

Anyway, thanks.

Peter Bacon Darwin

unread,
Nov 22, 2012, 2:54:57 PM11/22/12
to ang...@googlegroups.com
The $digest does work on the current scope down.  It is the $apply that runs from the $rootscope.  I believe there are a couple of reasons.  First, $rootScope is a service that can be injected into other services, such as $timeout, whereas the $timeout service cannot tell what scope it is being called from - unless the caller were to pass in some scope.  Second, and more important, if you don't run $digests from the $rootScope then there is a chance that you databindings will get out of synch.  There are only a small fraction of scenarios where you can guarantee that there is not some thing outside the current scope watching something that you current scope's $digest may change.
Pete

Pawel Kozlowski

unread,
Nov 22, 2012, 3:06:23 PM11/22/12
to ang...@googlegroups.com
Hi!

On Thu, Nov 22, 2012 at 8:27 PM, Adnan Ibrišimbegović <adi...@gmail.com> wrote:

> Now I wish someone could explain to me why the $digest, by default, runs
> from the $rootScope down instead from the current $scope down. This default
> behavior seems so wrong.

It is necessary. More info here:
http://stackoverflow.com/questions/12333410/why-scope-apply-calls-rootscope-digest-rather-than-this-digest/12359790#12359790

Cheers,
Pawel

Adnan Ibrišimbegović

unread,
Nov 23, 2012, 4:00:42 AM11/23/12
to ang...@googlegroups.com
"There are only a small fraction of scenarios ..."
- I have plenty to learn about AngularJS, but the fact is that I was hit by this behavior on my very first angular directive that I worked with. 
So I guess that we would benefit for having some sort of property defined on the directive object, through which we could instruct the directive whether it should or should not run the $apply function. Something similar (or exactly the same) to the 'invokeApply' flag that the $timeout service has. Such flag would practically define that directive as self-contained, or idempotent to the outside world, so to say. I bet many programmers who are learning AngularJS are new to the concept of $digest cycles, and would probably like to have more rigid control over such intrusive (and powerful) concept.
Adnan

Peter Bacon Darwin

unread,
Nov 23, 2012, 4:38:08 AM11/23/12
to ang...@googlegroups.com
How would this work in practice?  If you write your own directives then it is a simple matter not to call $apply from within it.  Simply call $digest on the directive's scope instead.
Pete

Adnan Ibrišimbegović

unread,
Nov 26, 2012, 3:32:47 PM11/26/12
to ang...@googlegroups.com
Well, only after my last post have I learned how the $apply function works:

  1. function $apply(expr) {
  2. try {
  3. return $eval(expr);
  4. } catch (e) {
  5. $exceptionHandler(e);
  6. } finally {
  7. $root.$digest();
  8. }
  9. }

So, if nothing else, it seems that this function could be extended with additional scope parameter, through which one could set a custom scope for the $digest to be called upon:

  1. function $apply(expr, scope) {
  2. ...
  3.  scope ||= $root;
  4. scope.$digest();
  5. }
  6. }

And than, one could extend the directive configuration object with one additional property (e.g. "domain" property), which would regulate the scope parameter of the $apply function.

For sure, if I'm writing my own directive I can do all this right now, but maybe this "technique" could also be described inside the AngularJS documentation, so that newcomers know what to do when they hit such an unexpected behavior as I did.
Adnan Ibrišimbegović
061 796 934

Reply all
Reply to author
Forward
0 new messages