AngularJS watch DOM change

65,239

Solution 1

So here's what I ended up doing:

I discovered you could pass a function to $scope.$watch. From there, it's pretty straightforward to return the value of the expression you want to watch for changes. It will work exactly like passing a key string for a property on the scope.

link: function ($scope, $el, $attrs) {
  $scope.$watch(
    function () { return $el[0].childNodes.length; },
    function (newValue, oldValue) {
      if (newValue !== oldValue) {
        // code goes here
      }
    }
  );
}

I am watching childNodes, not children, because the childNodes list holds elements as well as text nodes and comments. This is priceless because Angular uses comment placeholders for directives like ng-repeat, ng-if, ng-switch and ng-include which perform transclusion and alter the DOM, while children only holds elements.

Solution 2

If you need to watch for any changes deeper in the element's dom, MutationObserver is the way to go :

.directive('myDirective', function() {
    return {
        ...
        link: function(scope, element, attrs) {
            var observer = new MutationObserver(function(mutations) {
                // your code here ...
            });
            observer.observe(element[0], {
                childList: true,
                subtree: true
            });
        }
    };
});

Solution 3

I created a directive module for this angular-dom-events

In your case you could

    <ul class="unstyled" auto-carousel>
      <li class="slide" ng-if="name" dom-on-create="nameCreated()">{{name}}</li>
      <li class="slide" ng-if="email" dom-on-destroy="emailDestroyed()">{{email}}</li>
    </ul>

Currently only supports dom-on-create and dom-on-destroy, but has better performance then the accepted answer because it will only fire once for each dom event, rather than repeatedly check the $watch callback.

Solution 4

Although I don't think it is with angular's recommendations, you could use ng-init which fires upon the initialization of the element:

<ul class="unstyled" auto-carousel>
    <li class="slide" ng-if="name" ng-init="recheck()">{{name}}</li>
    <li class="slide" ng-if="email" ng-init="recheck()">{{email}}</li>
</ul>
Share:
65,239
pilau
Author by

pilau

Updated on August 11, 2020

Comments

  • pilau
    pilau almost 4 years

    I have one auto-carousel directive which iterates through the linked element's children.

    The children however are not yet loaded in the DOM, because their ng-ifs expressions have not been parsed yet.

    How can I make sure the parent directive knows there have been changes to it's DOM tree?

            <ul class="unstyled" auto-carousel>
              <li class="slide" ng-if="name">{{name}}</li>
              ...
              <li class="slide" ng-if="email">{{email}}</li>
            </ul>
    

    I could use $timeout but that feels unreliable. I could also use ng-show instead of ng-if but that does not answer the question and not what I need.

  • Elise
    Elise over 10 years
    This is nice! I had a situation where I needed to run a function every time an input element was added or removed inside a parent. So, $scope.$watch(function () { return $(".parent input").length; }, .... This works awesomely, but I am wondering about performance..
  • pilau
    pilau over 10 years
    Well with that you'll be calling at least one jQuery lookup, (in this case, a very bad one too) on every $digest cycle. That's quite bad in terms of performances. You could cache the node and then use jQuery's find() to improve on that
  • Simon Thum
    Simon Thum almost 9 years
    Exactly what I was looking for. Thanks!
  • Mario Levrero
    Mario Levrero almost 9 years
    Is a nice one if you can modify your children, not if you are waiting for a third party element to appear
  • IProblemFactory
    IProblemFactory almost 9 years
    That is interesting, however requires IE 11.
  • commonpike
    commonpike almost 9 years
    Typical .. exactly what I'm looking for, and the docs tell me not to use it .. docs.angularjs.org/api/ng/directive/ngInit
  • Simon Dragsbæk
    Simon Dragsbæk over 8 years
    I did like this instead to listen for the content length on a contenteditable like this: scope.$watch(function () { return element[0].innerHTML.length;}, function (n, o) { /*Do a operation*/ });
  • pilau
    pilau over 8 years
    @SimonPertersen that's a clever one for contenteditables!
  • Nik Sumeiko
    Nik Sumeiko about 8 years
    It's also suggested to compare newValue with oldValue using angular.equals(newValue, oldValue), even comparable values are plain numbers in the use case of this answer.
  • Jeff Dunlop
    Jeff Dunlop over 7 years
    This is the only thing that worked for me. I needed to run some code after a form was dynamically inserted into the dom.