Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

angularjs: ngModel controller $parser & $formatter still fire after element is removed

In the exmple below, I don't understand why angular is still firing the parsers and formatters after the element is removed. Should I be cleaning up the ngModel controller manually within the directive? If so, how should I do that?

To see what I'm talking about, check out the plunker, and

  1. open a console
  2. click the 'remove' button
  3. click the 'change model' button
  4. notice the formatter still firing

plunker: http://plnkr.co/edit/R7v5nB8JaQ91WcDGU8BC?p=preview

JS

angular.module('testMod', [])
.controller('testCtrl', function($scope){
  $scope.test = "test";
  $scope.removeElem = function(id) {
    var elem = document.getElementById(id);
    angular.element(elem).remove();
  }
}).directive('testDir',[function() {
  return {
    require: 'ngModel',
    scope:true,
    link: function(scope, elem, attr, ctrl) {
      console.log('in directive');
      ctrl.$parsers.unshift(function (newValue) {
          console.log('directive parser');
          return newValue;
      });
      ctrl.$formatters.unshift(function (newValue) {
          console.log('directive formatter');
          return newValue;
      });
    }
  }
}]);

HTML

<body ng-controller='testCtrl'>
    <input id='test' test-dir ng-model='test'/>
    <button ng-click="removeElem('test')">remove</button>
    <button ng-click="test = test + 'a'">change model</button>
    <div>{{test}}</div>
</body>
like image 265
Tim Mac Avatar asked Jul 11 '14 15:07

Tim Mac


1 Answers

Your directive creates its own child scope - that's a good thing. It owns all the $watches on it, and should clean up after itself when its scope is destroyed.

It is good practice to:

  1. Create a child or isolate scope when you set up $watches and plan to destroy the scope later. Since the directive created the scope, it should also be the one to destroy it when necessary and release all its $watches.

  2. Detect when the element is being removed and destroy the scope if necessary. Sometimes this makes sense to do if your $watches are heavy, and you don't want them hanging around when the element that the directive is bound to is removed from the DOM. This may not make sense to do if the removal is temporary - i.e. toggle visibility of an element

  3. A directive should never destroy a scope belonging to another. i.e if you inherited the scope from the parent controller then let the parent controller clean up after itself - it is not the job of the child directive to do it.

  4. If the directive has set up any $watches, it should listen for the scope $destroy event so that it can unregister them.

  5. If the directive has registered any javascript event listeners with on(), it should unregister them with off() when the scope is destroyed.

It is possible to perform clean-up when an element is removed from the DOM by handling the $destroy event on the jQuery element itself:

// creates a child scope owned by the directive
scope: true,
link: function(scope, element, attr) {

    // set up a click handler
    element.on('click', function() { 
        ...
    });

    // set up a $watch on the child scope. It returns a 
    // function that you can call to unregister the $watch
    var unregister = scope.$watch('test', function() {
         ...
    });

    // when the element is removed from the DOM, 
    // destroy the scope and all its $watches.
     element.on('$destroy', function() {
          // remove the $watch
          unregister();          

          // remove the click handler
          element.off('click');    

          // call $destroy on the child scope
          // so that it can propagate to any children
          scope.$destroy();
     });
}

You should not have to clean up after ng-model. It will clean up after itself when the $destroy event is propagated to the child scopes. If all directives cleaned up after themselves, there would be a lot less to worry about when it comes to memory leaks and orphaned $watches.

Here is an updated Plunker that cleans up its $watches when the element is removed.

like image 197
pixelbits Avatar answered Nov 07 '22 06:11

pixelbits