19

I created a directive for showing tooltips:

app.directive('tooltip',function(){
    return{
        restrict: 'A',
        link: function(scope,element,attr){
            element.bind('mouseenter',function(e){

                scope.setStyle(e);

            });
        }
    }
});

The corresponding setStyle() function:

$scope.setStyle = function(e){
    $scope.style = {
        position: 'absolute',
        // some other styles
    };

    $scope.$digest();
};

$scope.style is applied to this:

<span ng-style="style">I am a tooltip</span>

which is part of my view, handled by the controller who owns $scope.style

Why do i have to call $digest() in order to apply the changes to $scope.style, which was declared and initialized earlier?

Damjan Pavlica
  • 31,277
  • 10
  • 71
  • 76
user2422960
  • 1,476
  • 6
  • 16
  • 28

3 Answers3

27

Because the callback attached to the mouseenter event is outside of angular's scope; angular has no idea when that function runs/ends so the digest cycle is never ran.

Calling $digest or $apply tells angular to update bindings and fire any watches.

cnmuc
  • 6,025
  • 2
  • 24
  • 29
noj
  • 6,740
  • 1
  • 25
  • 30
  • Thank you very much so far. Are there any disadvantages when solving this problem that way? Should i consider another architecture? – user2422960 Sep 13 '13 at 11:47
  • 5
    No, this is what you have to do when outside of angular's scope. A better approach would be to include the `$digest/$apply` call in your directive: `scope.$apply(function(){ scope.setStyle(e); });` – noj Sep 13 '13 at 11:49
13

element.bind() means listen for specific browser event and execute callback when this event is dispatched on element. Nowhere in this chain of events Angular is included - it does not know that the event happened. Therefore you must tell it about the event explicitly. However, in most cases you should use $scope.$apply() not $scope.$digest(), especially when you're not sure about it.

Here is more appropriate code for your situation:

app.directive('tooltip',function(){
    return{
        restrict: 'A',
        link: function(scope,element,attr){
            element.bind('mouseenter',function(e){
                scope.setStyle(e);
                scope.$apply();
            });
        }
    }
});

and setStyle():

$scope.setStyle = function(e){
    $scope.style = {
        position: 'absolute',
        // some other styles
    };
};
package
  • 4,741
  • 1
  • 25
  • 35
  • $scope.$apply() calls $rootScope.$digest(), so indeed you probably want to either call $scope.$apply() or $rootScope.$digest(). Calling $scope.$digest() means you're restricting the digest to the local scope. – user276648 Oct 31 '14 at 07:21
1

$scope.apply() has two implementations, one that has no arguments and other one takes a function as argument, evaluates it and trigger digest(). Using the latter one has advantages as it wraps up your function in try/catch clause which is handled by $exceptionHandler service.

Although, you can do like this as well:

$scope.$apply(scope.setStyle(e));

This will wrap your function call to apply and auto triggers the digest.

SSC
  • 2,956
  • 3
  • 27
  • 43
  • 1
    Technically this is not wrapping your `setStyle` call inside the $apply invocation. `setStyle` returns and passes its return value to $apply. It'll still work in most cases, but it might be a bit misleading. – jlb May 24 '16 at 13:02