0

I want to attach a DOM event to a directive element so as to update a scope property. Unfortunately, I can't find a "clean" way to do this. The only way I can get this to work now is to explicitly invoke $apply() inside the handler. This is bad practice, I know, but it also prevents me from sharing this code with native angular directives, such as with ng-click="myDOMEventHandler()" (because it triggers an $apply already in progress exception.

Is there a way to add a DOM event to a directive element so that changes to the scope are picked up, but without having to call $apply()?

Here is a simple example of what I mean. You can edit this Plunker as well).

angular.module('myApp', [])
.directive('myDirective', function ($compile) {
  return {
    link: function (scope, element) {
      scope.keystrokes = 0;

      var report = angular.element('<div ng-click="increment()">keystrokes: {{keystrokes}}</div>');
      element.after(report);
      $compile(report)(scope);

      scope.increment = function () {
        scope.keystrokes += 1;
        scope.$apply();
      };

      element.on('keyup', scope.increment);
    }
  };
});

If you enter some text in the input, the counter increments. And if you click on the button, the counter also increments -- but if raises a $apply already in progress exception.

If you remove the scope.$apply(), then the exception goes away and the scope properties do change, but these changes are never displayed.

1 Answer 1

1

You don't have to use scope.$apply inside scope.increment function but you should use

$apply inside element.on('keyup'...)) as you binding there event outside angular scope

angular.module('myApp', [])
.directive('myDirective', function ($compile) {
  return {
    link: function (scope, element) {
      scope.keystrokes = 0;

      var report = angular.element('<div>keystrokes and clicks: {{keystrokes}} <button ng-click="increment()">Or click me</button></div>');
      element.after(report);
      $compile(report)(scope);

      scope.increment = function () {
        scope.keystrokes += 1;
      
      };
      
      element.on('keyup', function(){
        
        scope.$apply(function(){
          
          scope.increment();
          
        });
        
        
      });
    }
  };
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>

<body ng-app="myApp">
  <input my-directive="" />
</body>

Sign up to request clarification or add additional context in comments.

3 Comments

But is there a way to do this without putting markup into the parent document? Adding it to the <input> directly makes it look like an external dependency. It just feels to me like the directive itself should be able to handle this.
@Andrew in that case you can move scope.$apply scope.increment to element.on('keyup', ... because that event trigger outside angular scope and you can use $apply only here to perform scope life cycle
Thanks for your help. This does seem to be the only way to go. What a hassle, though. I think you also need to run scope.$on('$destroy') to cleanly remove the event listener and not have any leaks. I think this means you can't run the scope.$apply() inside an anonymous function, but instead you need an intermediate function, which 1) you bind keyup to, 2) you can bind scope.$on('$destroy') to, and 3) runs scope.$apply(). It really seems like something Angular should integrate natively because they are already doing all this in their own native directives already. But this does work!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.