why ng-repeat changes order of link function execution
Asked Answered
P

1

6

The usual order of execution of compile and link function on nested directives is as below

Markup

<dir1>
  <div dir2="">
  </div>
</dir1>

Order of execution

1) compile of directive 1
2) compile of directive 2
3) link of directive 2
4) link of directive 1

Assuming dir1 has restrict property set to 'E' and dir2 has restrict set to 'A'

Now if you use a ng-repeat directive in the same markup, the order of execution changes

Markup

<dir1>
  <div ng-repeat="item in items">
    <div dir2="">
    </div>
  </div>
</dir1>

assuming items is defined on scope, the order of execution changes to

1) compile of directive 1
2) link of directive 1
3) compile of directive 2
4) link of directive 2

Plunker - https://plnkr.co/edit/fRGHS1Bqu3rrY5NW2d97?p=preview

Why does this happen? Is is because ng-repeat has transclude property set to element. If that is the case, why should it alter the order of execution of dir1 which is outside ng-repeat.

Any help would be much appreciated.

Pedestrianize answered 2/5, 2016 at 4:51 Comment(4)
you can use priority to set execution of directive in particular order.Headstall
@ShailendraSinghDeol i tried doing that. Apparently priority helps only if multiple directives are defined on the same element but in this case, the directives are nested.Pedestrianize
From your plnkr, your console output for ng-repeat is wrong. Output is printed in this order: 1) dir1 compile, 2) dir1 link, 3) dir2 compile, 4) dir2 link, 5) dir2 linkNonchalance
@KhalidHussain i guess i made a mistake while writing the...thanks for the correction....will edit...The problem still remains that why is link of dir1 executed before link of dir2 even though dir2 is nested inside dir1.Pedestrianize
P
6

First of all, nice question! I used to use angular to develop several webapps, but I never realized this.

This is because inside of ngRepeat implementation, the google team use the $scope.$watchCollection to watch the variables and update the element.(With some other optimizations.) With invoking the $watchCollection, it calls the setTimeout to eval the changes asynchronously.

Then you can write down your own version of ngRepeat. Let's call it myRepeat.

//mock ng-repeat : )
app.directive('myRepeat', function ($compile) {
    return {
        restrict:'A',
        transclude: 'element',
        priority: 1000,
        terminal: true,
        $$tlb: true,
        compile: function ($element, $attr) {
            var expression = $attr.myRepeat;
            var ngRepeatEndComment = $compile.$$createComment('end myRepeat', expression);

            //parse the ngRepeat expressions.
            var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);


            var rhs = match[2]; //this would get items in your example

            return function ($scope, $element, $attr, ctrl, $transclude) {

                //$watch $scope[rhs] which rhs would be items in your example.

                $scope.$watchCollection(rhs, function myRepeatAction(collection) {
                  $transclude(function(clone, scope) {

                    clone[clone.length++] = clone; //append element
                  });

                });   
            }
        }
    }
});

If you comment out the watchCollection statement, you will get the output of your first example. And you can replace the $watchCollection with setTimeout to reproduce the same logs also.

If we look into the source code of angular.js, the callstack would be like watchCollection => $watch => $evalAsync => $browser.defer => setTimeout

$watch source code.

$browser.defer source code.

Hope this would solve your problem. : )

This is the fork of your example, with myRepeat implementation. For more detail, you can check the github of angular.js.

P.S Seems the angular version of your example is 1.5.3, so all the source code would be in 1.5.3.


update for async demo

More details about the setTimeout.

Basically you can regard your example as some functions below,

function dir1(callback) {

   console.log('compile dir1');
   callback();
   console.log('link dir1');
}

function dir2() {
   console.log('compile dir2');
   console.log('link dir2');
}

dir1(dir2);
//compile dir1
//compile dir2
//link dir2
//link dir1

And after added the custom version of ngRepeat, the code would be,

function dir1(callback) {
   console.log('compile dir1');
   callback();
   console.log('link dir1');
}

function dir2() {
   console.log('compile dir2');
   console.log('link dir2');
}
function myRepeat(callback) {
   return function() {
       setTimeout(callback, 0);
   }
}

dir1(myRepeat(dir2));
//compile dir1
//link dir1
//compile dir2
//link dir2

Sample Code for example 2. Seems pretty funny, isn't it?

The callback in setTimeout would be invoked after specific seconds, (would be 0 in our case).

But the callback would not be invoked until the current block of code completes its execution, which means in our case will output the link dir1 first.

1. compile dir1
2. setTimeout(execute after 0 second)
3. link dir1(current block is running, so do this first) 
4. compile dir2 (it's free now, invoke the callback)
5. link dir2

So that's what I mean asynchronously. For more details about setTimeout, you can check John Resig's How javascript timers work.

Protest answered 2/5, 2016 at 6:55 Comment(4)
Thanks for the explanation Tyler. Apologies but i am still not able to understand as to why the async eval in watchCollection would alter the link function timing of a directive that is outside ng-repeat which is dir1 in this case and cause it to execute before link of dir2 :(Pedestrianize
@YatinGera My bad, didn't explain it clearly. Just updated the answer with adding more details about the setTimeout part. Hope this would solve your question. : )Protest
@tyler-z-yang This makes complete sense now. thanks for explaining it. Have wasted countless hours trying to understand why my directives start behaving in a weird say after using ng-repeat. just out of curiosity, is there some way we could make the dir1's link execute after dir2's link without removing ng-repeat from the scenario?Pedestrianize
Yes, just like the implementation of ngRepeat, you can try to add your own timer to transclude the sub element. That should work.Protest

© 2022 - 2024 — McMap. All rights reserved.