Tuesday, March 19, 2013

How to handle DOM updates in AngularJS Directives in "link"

AngularJS directives are great. They provide a really wonderful mechnism "to teach HTML new tricks". But apparently there is no predefined mechanism for doing your DOM manipulation after the template has been loaded, cloned and transformed and rendered. Today I found a workaround for that.

var myModule = angular.module(myModule', []);
    myModule.directive('myDirective', function () {
        return {
            templateUrl: 'partials/timeline.html',
            link: function postLink(elem, attrs, transclude) {
                    // This code will run after
                    // templateUrl has been loaded and cloned
            }
        };
    );

The problem is divided in two steps, that must be handled each: The function "link" is called after the template has been cloned. This does not include any DOM manipulations that happen after this cloning, triggered by directives ng-repeat or ng-view. If you want to start your DOM manipulation after these directives have been handled by AngularJS (i.e. the DOM has been manipulated), you will need a setTimeout, or better the $timeout function from AngularJS.

So now you will get this code.
var myModule = angular.module(myModule', []);
    myModule.directive('myDirective', function ($timeout) {
        return {
            templateUrl: 'partials/timeline.html',
            link: function postLink(elem, attrs, transclude) {
                $timeout(function () {
                    // This code will run after
                    // templateUrl has been loaded, cloned
                    // and transformed by directives.
                }, 0);
            }
        };
    );
Yeah, that's better. But wait, we're not done yet. What if you want you want to do transformations of the DOM based on the positions of the newly rendered elements? Getting the position, offset, height, width of the newly created elements are not guaranteed to provide the correct values, because the browser had not enough time to render and layout those elements. The hack is to further delay these operations.
var myModule = angular.module(myModule', []);
    myModule.directive('myDirective', function ($timeout) {
        return {
            templateUrl: 'partials/timeline.html',
            link: function postLink(elem, attrs, transclude) {
                $timeout(function () {
                    $timeout(function () {
                        // This code will run after
                        // templateUrl has been loaded, cloned
                        // and transformed by directives.
                        // and properly rendered by the browser
                    }, 0);
                }, 0);
            }
        };
    );
If you wonder, why a timeout of 0 really helps, here's a great explanation by John Resig.

13 comments:

  1. I had a similar problem. I needed to search in element's inner DOM for certain selector.
    When I used $(element) it returned empty array.
    When I tried your solution with nested $timeout it worked, but I did not like it.
    Finally I tried angular.element(elem).find(selector) and this worked. I did not have to use $timeout.

    ReplyDelete
  2. Hi David,

    I have a similar problem. Can you please give me an example to use this.
    Thanks in advance.

    ReplyDelete
  3. Replies
    1. What should be the right solution in your opinion?

      Delete
  4. Ugly or not, it worked like a charm and I'll leave like so.

    After hours trying to find a nice solution here.

    I had to use it combined with a third-party core that I didn't want to refactor.

    Thanks a lot for sharing!

    ReplyDelete
  5. Great solution. When nothing solved my issue of default focus to an element, this code worked as a charm.

    ReplyDelete
  6. Instead of two timeouts, would it be better if we put a single timeout with some delays greater than 0?

    ReplyDelete
    Replies
    1. It actually would work if the timeout is long enough.
      The problem is indeed, finding the right value so that this would always work, even on the slowest mobile device, but also work really fast on some high-end desktop computer. So I decided not to slowing down desktop computer users and risking of my code not to work correctly and went with the double $timeout.

      Delete
  7. Thanks a lot .
    I have same type of issue
    and i searched a lot on web but i din get any usefull link
    later i found your link and i apply as you mention then it works nice.
    Good Job dear
    thank you so much...

    ReplyDelete
  8. This comment has been removed by the author.

    ReplyDelete
  9. I have some CSS transitions running during rerndering...the double timeouts do not seem to take care of this fact, and the code is run before/during the transitions

    ReplyDelete
    Replies
    1. Are your transitions defined to take longer than 0 s? If they ware, you should consider taking a timeout delay of a value greater than 0 for the innermost timeout.

      Delete