32

I have a watch that triggers a DOM event:

scope.$watch(function() { return controller.selected; }, function(selected) {
    if (selected) {
        $input.trigger('focus');
    }
});

The issue is that I have a handler on 'focus' that does a scope.$apply.

$input.bind('focus', function() {
    scope.$apply(function() { controller.focused = true; });
});

So when my $watch is fired from inside a $digest it causes an error because it tries to trigger another $digest.

The workaround I have is to put the trigger in a $timeout.

scope.$watch(function() { return controller.selected; }, function(selected) {
    if (selected) {
        $timeout(function() { $input.trigger('focus'); });
    }
});

This works ... so far. Is this the proper way to handle this? I'm not sure if this catches every case and would like to see if there is an angular approved way to have a piece of code defer for after the digest.

Thanks!

anonymous
  • 6,825
  • 8
  • 47
  • 60

2 Answers2

66

$timeout is normally what is used to run something after a digest cycle (and after the browser renders).

$timeout will cause another digest cycle to be executed after the function is executed. If your trigger does not affect anything Angular, you can set the invokeApply argument to false to avoid running another digest cycle.

If you want your callback to run before the browser renders: If code is queued using $evalAsync from a directive, it should run after the DOM has been manipulated by Angular, but before the browser renders. However, if code is queued using $evalAsync from a controller, it will run before the DOM has been manipulated by Angular (and before the browser renders). See also https://stackoverflow.com/a/17303759/215945.

Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • "`$timeout` will cause another digest cycle to be executed." ... weird. I would think if that was the case and my event handler is calling `apply` I would be getting the same 'already in $digest' error. – anonymous Apr 17 '13 at 17:40
  • Oh okay, it calls `$apply` after the function is executed. So I should set the `invokeApply` argument to false to avoid a useless `$digest`. Thanks. – anonymous Apr 17 '13 at 17:44
  • @eyston, I forgot about the `invokeApply` argument. I'll update my answer with that instead of `setTimeout`. – Mark Rajcok Apr 17 '13 at 18:17
  • @mark-rajcok, Thank you! looking for that solution for a couple of hours! – FelikZ May 17 '13 at 14:53
  • damn, it is called exactly after render. Is there any option to setup callback before BROWSER render? – FelikZ May 17 '13 at 15:01
  • 1
    @FelikZ, try [$evalAsync](http://docs.angularjs.org/api/ng.$rootScope.Scope#$evalAsync) instead of $timeout. That should be called after the DOM updates, but before the browser renders. – Mark Rajcok May 17 '13 at 15:37
  • 2
    @FelikZ, if code is queued using $evalAsync from a _directive_, it should run _after_ the DOM has been manipulated by Angular, but _before_ the browser renders. However, if code is queued using $evalAsync from a _controller_, it will run _before_ the DOM has been manipulated by Angular (and _before_ the browser renders). See also http://stackoverflow.com/a/17303759/215945 – Mark Rajcok Jul 22 '13 at 17:30
  • @MarkRajcok +1 for $evalAsync. Should include a mention of it in the answer. – Atav32 Oct 30 '14 at 22:37
  • I tried using $scope.$evalSync but I still get the "[$rootScope:inprog] $digest already in progress" error I am trying to avoid. When I use $timeout to run the action, it might not work because it goes into a new event handler and some things (like opening the choose file dialog) must only be done in the initial event handler. – Dobes Vandermeer Dec 21 '16 at 20:42
0

Like everyone keeps saying, You should not be doing DOM stuff in your controller.

Here is a solution that applies two way data binding to focus. Now your focus is bound to a variable. So when you set the variable to true, it sets focus on the corresponding element and when the element gets focus the variable is set.

http://plnkr.co/edit/CvPCVxy4MfJEM1UksrrA?p=preview

We have now successfully separated the focus from the controller code. It also takes care of all the $timeout issues ( I guess ). The only thing you need to be aware of is that you should not use the same variable to bind to the focus of two different elements.

EDIT : Updated the plunk since the previous one wasnt working fine.

ganaraj
  • 26,841
  • 6
  • 63
  • 59
  • This is all in a directive. – anonymous Apr 17 '13 at 20:22
  • @eyston thats the point. All your DOM stuff now is in the directive. Also, note that the directive does not contain any application specific logic. This is proper separation of logic. See this thread : https://github.com/angular/angular.js/issues/1277 – ganaraj Apr 17 '13 at 20:36
  • i know this is older, but your answer didn't really speak to his question since you assumed he was doing everything in a controller. – gonzofish Jul 10 '14 at 16:45
  • @gonzofish Did you check the plunkr ? – ganaraj Jul 11 '14 at 08:31
  • @gonzofish It has everything to do with focus management - which is apparently what he is doing in his directive as well. – ganaraj Jul 13 '14 at 08:22