35

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.

pilau
  • 6,329
  • 4
  • 52
  • 64
  • 1
    According to angularJS docs, child directives is liked before parent directives. so ng-if is linked before parent directive. So parsing order is not your problem. Maybe your data is available asynchronously so ng-if is not parsed in link time. – Alborz Jan 24 '14 at 14:27
  • @Alborz That's interesting. Do you have a link? If that is the case, I have a feeling the directive is linked before the models for the child elements are populated, I'll perform a test and get back to you. Don't forget my link! – pilau Jan 24 '14 at 16:07
  • @Alborz The link function is definitely taking place *after* the model is populated. I even used an extra `$scope.$apply()`. – pilau Jan 24 '14 at 16:12
  • I've created a sample. you will see that child link is executed before parent one .http://jsbin.com/udaTEraM/1/edit – Alborz Jan 24 '14 at 17:22
  • 1
    @Alborz Thanks for taking the time to set that up. However I added a `console.log` at the `$watch` handler of `ng-if` - and it is clearly executed after the parent linker. Is there any way I can call the parent linker after the children are changed? Like, watch for DOM changes? (Watching for model changes is a problem because this directive is generic and used everywhere) – pilau Jan 24 '14 at 19:24
  • @Alborz I got it buddy, see my answer below :) – pilau Jan 26 '14 at 08:41

5 Answers5

74

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.

pilau
  • 6,329
  • 4
  • 52
  • 64
  • 1
    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.. – Elise Mar 23 '14 at 12:26
  • 8
    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 – pilau Mar 23 '14 at 18:07
  • 2
    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*/ }); – Simon Dragsbæk Oct 14 '15 at 13:22
  • 1
    @SimonPertersen that's a clever one for `contenteditable`s! – pilau Oct 14 '15 at 14:00
  • It's also suggested to compare `newValue` with `oldValue` using [`angular.equals(newValue, oldValue)`](https://docs.angularjs.org/api/ng/function/angular.equals), even comparable values are plain numbers in the use case of this answer. – Nik Sumeiko May 17 '16 at 19:09
18

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
            });
        }
    };
});
aaaaahaaaaa
  • 445
  • 5
  • 14
5

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.

ilovett
  • 2,734
  • 25
  • 35
2

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>
Jason
  • 29
  • 1
  • Typical .. exactly what I'm looking for, and the docs tell me not to use it .. https://docs.angularjs.org/api/ng/directive/ngInit – commonpike Aug 26 '15 at 21:18
  • This is the only thing that worked for me. I needed to run some code after a form was dynamically inserted into the dom. – Jeff Dunlop Mar 21 '17 at 00:10
0

You could try to compile the directive contents first inside your link function. For example:

angular.module('myApp').directive('autoCarousel', ['$compile', function ($compile) {

    return {
        templateUrl: 'views/auto-carousel.html',
        restrict: 'A',
        replace: true,
        link: function (scope, element, attr) {
            $compile(element.contents())(scope);

            // your code goes here
        }
    }
}]);
SergeL
  • 680
  • 5
  • 7