Why is the post link function executed before the transcluded child link functions?

All we need is an easy explanation of the problem, so here it is.

The timing of (pre/post)link functions in AngularJS are well defined in the documentation

Pre-linking function

Executed before the child elements are linked. Not safe to do DOM
transformation since the compiler linking function will fail to locate
the correct elements for linking.

Post-linking function

Executed after the child elements are linked. It is safe to do DOM
transformation in the post-linking function.

and this blog post clearly illustrates this expected order.

But this order does not seem to apply when using ng-transclude and nested directives.

Here is an example for a dropright element (See the Plunkr)

<!-- index.html -->
<dropright>
  <col1-item name="a">
    <col2-item>1</col2-item>
    <col2-item>2</col2-item>
  </col1-item>
  <col1-item name="b">
    ...
  </col1-item>
</dropright>

// dropright-template.html
<div id="col1-el" ng-transclude></div>
<div id="col2-el">
  <!-- Only angularJS will put elements in there -->
</div>

// col1-item-template.html
<p ng-transclude></p>

// col2-item-template.html
<div ng-transclude></div>

The dropright looks like

dropright

The directives write a log in the console when their link and controller functions are called.
It usually displays:

expected

But sometimes (after few refreshes), the order is not as expected:

sometimes happen

The dropright post-link function is executed before the post-link function of its children.

It may be because, in my particular case, I am calling the dropright controller in the children’s directives (See the Plunkr)

angular.module('someApp', [])

.directive('dropright', function() {
    return {
        restrict: 'E',
        transclude: 'true',
        controller: function($scope, $element, $attrs) {
            console.info('controller - dropright');

            $scope.col1Tab = [];
            $scope.col2Tab = [];

            this.addCol1Item = function(el) {
                console.log('(col1Tab pushed)');
                $scope.col1Tab.push(el);
            };

            this.addCol2Item = function(el) {
                console.log('(col2Tab pushed)');
                $scope.col2Tab.push(el);
            };
        },
        link: {
            post: function(scope, element, attrs) {
                console.info('post-link - dropright');
                // Here, I want to move some of the elements of #col1-el
                // into #col2-el
            }
        },
        templateUrl: 'dropright-tpl.html'
    };
})

.directive('col1Item', function($interpolate) {
    return {
        require: '^dropright',
        restrict: 'E',
        transclude: true,
        controller: function() {
            console.log('-- controller - col1Item');
        },
        link: {
            post: function(scope, element, attrs, droprightCtrl) {
                console.log('-- post-link - col1Item');
                droprightCtrl.addCol1Item(element.children()[0]);
            }
        },
        templateUrl: 'col1-tpl.html'
    };      
})

.directive('col2Item', function() {
    var directiveDefinitionObject = {
        require: '^dropright',
        restrict: 'E',
        transclude: true,
        controller: function() {
            console.log('---- controller - col2Item');
        },
        link: {
            post: function(scope, element, attrs, droprightCtrl) {
                console.log('---- post-link - col2Item');
                droprightCtrl.addCol2Item(element.children()[0]);
            }
        },
        templateUrl: 'col2-tpl.html'
    };
    return directiveDefinitionObject;
});

Is there any clean way to execute the link function of a directive after all the link functions of its children while using transclusion?

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

This is my theory – its not the transclude aspect that is causing the sequence issue but rather the template being a templateUrl. The template needs to be resolved before the post link function get to act on it – hence we say post link function is safe to do DOM manipulation. While we are getting 304s for all the 3 templates – we do have to read them and it ultimately resolves the template promise.

I created a plunker with template instead of templateUrl to prove the corollary. I have hot refresh/plunker Stop/Run many times but I always get link - dropright at the end.

Plunker with template instead of templateUrl

I don’t pretend to understand the compile.js code fully. However it does appear that in
compileTemplateUrl function $http.success() resolves the template and then on success the applyDirectivesToNode function is called passing in postLinkFn.

https://github.com/angular/angular.js/blob/master/src/ng/compile.js

Method 2

This may be just weirdness with Plunker. I tried copying the files to my local IIS, and was not able to replicate the issue.

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply