139

I 've just gotten my directive to pull in a template to append to its element like this:

# CoffeeScript
.directive 'dashboardTable', ->
  controller: lineItemIndexCtrl
  templateUrl: "<%= asset_path('angular/templates/line_items/dashboard_rows.html') %>"
  (scope, element, attrs) ->
    element.parent('table#line_items').dataTable()
    console.log 'Just to make sure this is run'

# HTML
<table id="line_items">
    <tbody dashboard-table>
    </tbody>
</table>

I am also using a jQuery Plugin called DataTables. The general usage of it is like this: $('table#some_id').dataTable(). You can pass in the JSON data into the dataTable() call to supply the table data OR you can have the data already on the page and it will do the rest.. I am doing the latter, having the rows already on the HTML page.

But the problem is that I have to call the dataTable() on the table#line_items AFTER DOM ready. My directive above calls the dataTable() method BEFORE the template is appended to the directive's element. Is there a way that I can call functions AFTER the append?

Thank you for your help!

UPDATE 1 after Andy's answer:

I want to make sure that the link method does only get called AFTER everything is on the page so I altered the directive for a little test:

# CoffeeScript
#angular.module(...)
.directive 'dashboardTable', ->
    {
      link: (scope,element,attrs) -> 
        console.log 'Just to make sure this gets run'
        element.find('#sayboo').html('boo')

      controller: lineItemIndexCtrl
      template: "<div id='sayboo'></div>"

    }

And I do indeed see "boo" in the div#sayboo.

Then I try my jquery datatable call

.directive 'dashboardTable',  ->
    {
      link: (scope,element,attrs) -> 
        console.log 'Just to make sure this gets run'
        element.parent('table').dataTable() # NEW LINE

      controller: lineItemIndexCtrl
      templateUrl: "<%= asset_path('angular/templates/line_items/dashboard_rows.html') %>"
    }

No luck there

Then I try adding a time out :

.directive 'dashboardTable', ($timeout) ->
    {
      link: (scope,element,attrs) -> 
        console.log 'Just to make sure this gets run'
        $timeout -> # NEW LINE
          element.parent('table').dataTable()
        ,5000
      controller: lineItemIndexCtrl
      templateUrl: "<%= asset_path('angular/templates/line_items/dashboard_rows.html') %>"
    }

And that works. So I wonder what goes wrong in the non-timer version of the code?

Nik So
  • 16,683
  • 21
  • 74
  • 108
  • 1
    @adardesign No I never did, I had to use a timer. For some reason, callback isn't a callback here, really. I have a table with 11 columns and 100's of rows, so naturally angular looks like a good bet to use for data binding; but I also need to use the jquery Datatables plugin which is as simple as $('table').datatable(). Using directive or just have a dumb json object with all the rows and use ng-repeat to iterate, I cannot get my $().datatable() to run AFTER the table html element is rendered, so I my trick currently is to timer to check if $('tr').length > 3 (b/c of header/footer) – Nik So Aug 07 '12 at 18:02
  • 2
    @adardesign And yes, I tried all compile method, compile method returning an object containing methods postLink/preLink, compile method returning just a function (namely the linking function), linking method (without the compile method because as far as I can tell, if you have a compile method that returns a linking method, the linking function is ignored).. None worked so have to rely on good old $timeout. Will update this post if I find anything that work better or simply when I find that the callback really acts like callback – Nik So Aug 08 '12 at 03:35

10 Answers10

217

If the second parameter, "delay" is not provided, the default behaviour is to execute the function after the DOM has completed rendering. So instead of setTimeout, use $timeout:

$timeout(function () {
    //DOM has finished rendering
});
parliament
  • 21,544
  • 38
  • 148
  • 238
  • 8
    Why isn't it explained in the [docs](https://docs.angularjs.org/api/ng/service/$timeout) ? – Gaui Dec 23 '14 at 11:25
  • 23
    You're right, my answer is a bit misleading because I tried to make it simple. The full answer is that this effect is not a result of Angular but rather the browser. `$timeout(fn)` ultimately calls `setTimeout(fn, 0)` which has the effect interrupting the Javascript execution and allowing the browser to render the content first, before continuing the execution of that Javascript. – parliament Dec 25 '14 at 17:48
  • 7
    Think of the browser as queue'ing certain tasks such as "javascript execution" and "DOM rendering" separately, and what setTimeout(fn,0) does it push the currently running "javascript execution" to the back of the queue, after rendering. – parliament Dec 25 '14 at 17:50
  • Thanks, looks like this is like [Underscore's defer](http://underscorejs.org/#defer), I used `_.defer(myFunction);` and did the trick :) "Defers invoking the function until the current call stack has cleared" – GabLeRoux Aug 06 '15 at 19:25
  • 2
    @GabLeRoux yup, that will have the same effect except $timeout has the added benefit of calling $scope.$apply() after it runs. With _.defer() you will need to call it manually if myFunction changes variables on the scope. – parliament Aug 10 '15 at 04:07
  • 2
    I'm having a scenario where this does not help where on page1 ng-repeat renders bunch of elements, then I go to page2 and then I go back to page1 and I try to get the high of ng-repeat elements parent... it returns the wrong height. If I do timeout for like 1000ms, then it works. – yodalr Oct 18 '15 at 22:08
  • 1
    @yodalr I suspect when you return to page1 there is an ajax call that reloads some data. using $timeout will not wait for the ajax call to complete. Make sure your request callbacks fire and update the data on the scope first, THEN try using $timeout to let angular re-render the new dat before you query the DOM. (Note: $http callbacks success,error,etc automatically run the $digest cycle after the callback logic, so it's not necessary to use $timeout in most cases). – parliament Oct 19 '15 at 14:36
  • You can test this by setting a 3 second delay on your server for the ajax call. If your 1 second timeout no longer fixes the problem, it's because you're not waiting for the request to complete before proceeding with the logic. – parliament Oct 19 '15 at 14:44
  • Helpful answer, but do not forget to cancel the timeout: `var myTimeout = $timeout(function () { $timeout.cancel(myTimeout); //DOM has finished rendering ... and do your job... });` – Halayem Anis Sep 23 '16 at 09:40
  • @HalayemAnis I dont think this is necessary as the timeout gets resolved after it runs, cancel should have no effect. cancel() it more useful for $interval service. – parliament Sep 23 '16 at 19:06
  • unfortunately $timeout does not always help in that condition, for example if we have child directives or ng-repeat – Pikachu Feb 08 '17 at 12:45
  • Not a deterministic solution. Most of the time it will work, but not always will it be pushed until "after the DOM has loaded", as you said. It will usually wait in the execute queue until its turn, which can or cannot be after the dom.. – chesscov77 Apr 22 '17 at 15:54
  • In my case I had to compute the position of some elements with position: relative. Thus, I had to use $timeout with a timeout of > 0, inside the link function of the directive. Otherwise, the relatively positioned elements wouldn't be at their definitive position. – Juangui Jordán Aug 18 '17 at 12:48
  • Yes this answer seems to be the consensus . At least one other link : https://blog.brunoscopelliti.com/run-a-directive-after-the-dom-has-finished-rendering/ – Nate Anderson Oct 08 '17 at 00:25
  • 1
    this doesn't work for me in IE 11 unless you put a large value for the timeout (2000 etc) Otherwise the timeout happens before the directive rendering has finished. – Matthew Aug 10 '18 at 04:07
  • this doesn't work for me in google chrome unless you put a large value for the timeout (333ms etc) Otherwise the timeout happens before the directive rendering has finished. – Alexey Sh. Feb 12 '20 at 14:37
14

I had the same problem and I believe the answer really is no. See Miško's comment and some discussion in the group.

Angular can track that all of the function calls it makes to manipulate the DOM are complete, but since those functions could trigger async logic that's still updating the DOM after they return, Angular couldn't be expected to know about it. Any callback Angular gives might work sometimes, but wouldn't be safe to rely on.

We solved this heuristically with a setTimeout, as you did.

(Please keep in mind that not everyone agrees with me - you should read the comments on the links above and see what you think.)

Roy Truelove
  • 22,016
  • 18
  • 111
  • 153
7

You can use the 'link' function, also known as postLink, which runs after the template is put in.

app.directive('myDirective', function() {
  return {
    link: function(scope, elm, attrs) { /*I run after template is put in */ },
    template: '<b>Hello</b>'
  }
});

Give this a read if you plan on making directives, it's a big help: http://docs.angularjs.org/guide/directive

Andrew Joslin
  • 43,033
  • 21
  • 100
  • 75
  • Hi Andy, thank you so much for answering; I did try the link function but I wouldn't mind giving it a try again exactly as you code it; I spent the last 1.5 days reading up on that directive page; and looking at the examples on angular's site as well. Will try your code now. – Nik So Jun 20 '12 at 19:02
  • Ah, I see now you were trying to do link but you were doing it wrong. If you just return a function, it's assumed to be link. If you return an object, you have to return it with the key as 'link'. You can also return a linking function from your compile function. – Andrew Joslin Jun 20 '12 at 19:09
  • Hi Andy, got my results back; I almost lost my sanity, because I really did basically what your answer here is. Please see my update – Nik So Jun 20 '12 at 19:37
  • Humm, try something like:
    Then in your link, do $(attrs.dashboardTable).dataTable() to make sure it's being selected right. Or I guess you already tried that.. I'm really not sure if the link isn't working.
    – Andrew Joslin Jun 20 '12 at 20:28
  • This one worked for me , i wanted to move elements within dom post rendering of template for my requirement , did that in the link function.Thanks – abhi Jan 03 '17 at 10:05
7

Although my answer is not related to datatables it addresses the issue of DOM manipulation and e.g. jQuery plugin initialization for directives used on elements which have their contents updated in async manner.

Instead of implementing a timeout one could just add a watch that will listen to content changes (or even additional external triggers).

In my case I used this workaround for initializing a jQuery plugin once the ng-repeat was done which created my inner DOM - in another case I used it for just manipulating the DOM after the scope property was altered at controller. Here is how I did ...

HTML:

<div my-directive my-directive-watch="!!myContent">{{myContent}}</div>

JS:

app.directive('myDirective', [ function(){
    return {
        restrict : 'A',
        scope : {
            myDirectiveWatch : '='
        },
        compile : function(){
            return {
                post : function(scope, element, attributes){

                    scope.$watch('myDirectiveWatch', function(newVal, oldVal){
                        if (newVal !== oldVal) {
                            // Do stuff ...
                        }
                    });

                }
            }
        }
    }
}]);

Note: Instead of just casting the myContent variable to bool at my-directive-watch attribute one could imagine any arbitrary expression there.

Note: Isolating the scope like in the above example can only be done once per element - trying to do this with multiple directives on the same element will result in a $compile:multidir Error - see: https://docs.angularjs.org/error/$compile/multidir

conceptdeluxe
  • 3,753
  • 3
  • 25
  • 29
7

May be am late to answer this question. But still someone may get benefit out of my answer.

I had similar issue and in my case I can not change the directive since, it is a library and change a code of the library is not a good practice. So what I did was use a variable to wait for page load and use ng-if inside my html to wait render the particular element.

In my controller:

$scope.render=false;

//this will fire after load the the page

angular.element(document).ready(function() {
    $scope.render=true;
});

In my html (in my case html component is a canvas)

<canvas ng-if="render"> </canvas>
rnrneverdies
  • 15,243
  • 9
  • 65
  • 95
Madura Pradeep
  • 2,378
  • 1
  • 30
  • 34
3

I had the same issue, but using Angular + DataTable with a fnDrawCallback + row grouping + $compiled nested directives. I placed the $timeout in my fnDrawCallback function to fix pagination rendering.

Before example, based on row_grouping source:

var myDrawCallback = function myDrawCallbackFn(oSettings){
  var nTrs = $('table#result>tbody>tr');
  for(var i=0; i<nTrs.length; i++){
     //1. group rows per row_grouping example
     //2. $compile html templates to hook datatable into Angular lifecycle
  }
}

After example:

var myDrawCallback = function myDrawCallbackFn(oSettings){
  var nTrs = $('table#result>tbody>tr');
  $timeout(function requiredRenderTimeoutDelay(){
    for(var i=0; i<nTrs.length; i++){
       //1. group rows per row_grouping example
       //2. $compile html templates to hook datatable into Angular lifecycle
    }
  ,50); //end $timeout
}

Even a short timeout delay was enough to allow Angular to render my compiled Angular directives.

JJ Zabkar
  • 3,792
  • 7
  • 45
  • 65
  • Just curious, do you have a rather large table with many columns? because I did find that I need an annoying a lot of milliseconds (>100) so not to let the dataTable() call to choke – Nik So Feb 05 '13 at 10:10
  • I found the issue happened upon [DataTable](http://www.datatables.net/ref) page navigation for result sets of 2 rows to over 150 rows. So, no--I don't think the size of the table was the issue, but perhaps DataTable added enough rendering overhead to chew some of those milliseconds away. My focus was on getting row-grouping to work in DataTable with minimal AngularJS integration. – JJ Zabkar Feb 05 '13 at 23:36
2

None of the solutions worked for me accept from using a timeout. This is because I was using a template that was dynamically being created during the postLink.

Note however, there can be a timeout of '0' as the timeout adds the function being called to the browser's queue which will occur after the angular rendering engine as this is already in the queue.

Refer to this: http://blog.brunoscopelliti.com/run-a-directive-after-the-dom-has-finished-rendering

0

I got this working with the following directive:

app.directive('datatableSetup', function () {
    return { link: function (scope, elm, attrs) { elm.dataTable(); } }
});

And in the HTML:

<table class="table table-hover dataTable dataTable-columnfilter " datatable-setup="">

trouble shooting if the above doesnt work for you.

1) note that 'datatableSetup' is the equivalent of 'datatable-setup'. Angular changes the format into camel case.

2) make sure that app is defined before the directive. e.g. simple app definition and directive.

var app = angular.module('app', []);
app.directive('datatableSetup', function () {
    return { link: function (scope, elm, attrs) { elm.dataTable(); } }
});
rnrneverdies
  • 15,243
  • 9
  • 65
  • 95
Anton
  • 7,709
  • 5
  • 31
  • 33
0

Here is a directive to have actions programmed after a shallow render. By shallow I mean it will evaluate after that very element rendered and that will be unrelated to when its contents get rendered. So if you need some sub element doing a post render action, you should consider using it there:

define(['angular'], function (angular) {
  'use strict';
  return angular.module('app.common.after-render', [])
    .directive('afterRender', [ '$timeout', function($timeout) {
    var def = {
        restrict : 'A', 
        terminal : true,
        transclude : false,
        link : function(scope, element, attrs) {
            if (attrs) { scope.$eval(attrs.afterRender) }
            scope.$emit('onAfterRender')
        }
    };
    return def;
    }]);
});

then you can do:

<div after-render></div>

or with any useful expression like:

<div after-render="$emit='onAfterThisConcreteThingRendered'"></div>

Seth
  • 10,198
  • 10
  • 45
  • 68
Sebastian Sastre
  • 2,034
  • 20
  • 21
  • This is not really after the content is rendered. If I had an expression inside the element
    {{blah}}
    at this point the expression is not evaluated yet. The contents of the div is still {{blah}} inside the link function. So technically you are firing the event before the content is rendered.
    – Edward Olamisan Mar 25 '15 at 16:39
  • This is a shallow after render action, I've never claimed it to be deep – Sebastian Sastre Mar 25 '15 at 19:15
0

Following the fact that the load order cannot be anticipated, a simple solution can be used.

Let's look at the directive-'user of directive' relationship. Usually the user of the directive will supply some data to the directive or use some functionality ( functions ) the directive supplies. The directive on the other hand expects some variables to be defined on its scope.

If we can make sure that all players have all their action requirements fulfilled before they attempt to execute those actions - everything should be well.

And now the directive:

app.directive('aDirective', function () {
    return {
        scope: {
            input: '=',
            control: '='
        },
        link: function (scope, element) {
            function functionThatNeedsInput(){
                //use scope.input here
            }
            if ( scope.input){ //We already have input 
                functionThatNeedsInput();
            } else {
                scope.control.init = functionThatNeedsInput;
            }
          }

        };
})

and now the user of the directive html

<a-directive control="control" input="input"></a-directive>

and somewhere in the controller of the component that uses the directive:

$scope.control = {};
...
$scope.input = 'some data could be async';
if ( $scope.control.functionThatNeedsInput){
    $scope.control.functionThatNeedsInput();
}

That's about it. There is a lot of overhead but you can lose the $timeout. We also assume that the component that uses the directive is instantiated before the directive because we depend on the control variable to exist when the directive is instantiated.

isherwood
  • 58,414
  • 16
  • 114
  • 157
Eli
  • 1,670
  • 1
  • 20
  • 23