13

I have a directive attached to a dynamically generated <table> element inside a template. The directive manipulates the DOM of that table inside a link function. The problem is that the directive runs before the table is rendered (by evaluating ng-repeat directives) - the table is empty then.

Question

How can I make sure that the directive is ran after the table has been fully rendered?

<table directive-name>
    <tr ng-repeat="...">
        <td ng-repeat="..."></td>
    </tr>
</table>


module.directive("directiveName", function() {
    return {
        scope: "A",
        link: function(scope, element, attributes) {
            /* I need to be sure that the table is already fully
               rendered when this code runs */
        }
    };
});
Robert Kusznier
  • 6,471
  • 9
  • 50
  • 71
  • 1
    You can add ng-if in the table tag and condition can be when table data is loaded – Hmahwish Aug 18 '15 at 13:41
  • is your data coming from `$http` request or just hard coded data – Kalhan.Toress Aug 18 '15 at 13:42
  • @K.Toress Data is coming from HTTP request, but it's already loaded when that template is processed - routing and controller takes care of that using `resolve` attribute in the `$routeProvider` configuration. – Robert Kusznier Aug 18 '15 at 13:44
  • @neda Can you write the code that would use your `ng-if` solution? I don't know what should be the condition inside `ng-if`. – Robert Kusznier Aug 18 '15 at 13:44
  • ooh oky then i think you can wrap your content in `link` function by a `$timeout` – Kalhan.Toress Aug 18 '15 at 13:45
  • Normally, this should be the case. Have a look at this blog post : http://www.jvandemo.com/the-nitty-gritty-of-compile-and-link-functions-inside-angularjs-directives/ ps: your link function is the post-link – Deblaton Jean-Philippe Aug 18 '15 at 13:46
  • you can try this ,it may work – Hmahwish Aug 18 '15 at 13:46
  • @K.Toress I can do the timeout, but such solution is hacky. I was looking for something cleaner. Doesn't Angular provide any mechanisms to resolve such situations? – Robert Kusznier Aug 18 '15 at 13:46
  • 2
    can put a directive on ng-repeat rows and check for `$last` as trigger for your manipulation – charlietfl Aug 18 '15 at 13:47
  • @Robert you could place a directive on inner `ng-repeat` http://stackoverflow.com/questions/13471129/ng-repeat-finish-event that will give you notification event that the rendering has been completed..then inside event listener you could run your code.. – Pankaj Parkar Aug 18 '15 at 13:52
  • @DeblatonJean-Philippe Thanks for the article. I've just finished reading it and it was a really informative and clearly presented. I only wonder why in my case a `link` fuction still operates on an empty table, if you say it's a post-link? Shouldn't all the inner directives be executed by then? – Robert Kusznier Aug 18 '15 at 14:18
  • @Robert I think so. But there is still something in angular I don't really know about : in a directive, you can specify a priority. Maybe setting a lower priority could also do the trick. Maybe you can try with `ng-transclude`. This is unfortunately not in my knowledge. But if you investigate, do not hesitate to answer this question and answer my comment so that I'll read it. – Deblaton Jean-Philippe Aug 19 '15 at 06:45
  • 1
    @DeblatonJean-Philippe I still don't fully comprehend why directives run in the order they do, but it surelt isn't because of directive's priority. Priority matters only between directive applied to the same element, as described here under *Directive Definition Object*'s *priority* : https://docs.angularjs.org/api/ng/service/$compile#directive-definition-object – Robert Kusznier Aug 21 '15 at 19:43
  • @Robert, thx, I remember having read that but you reminded it to me. – Deblaton Jean-Philippe Aug 21 '15 at 22:08

3 Answers3

6

Try wrapping in a $timeout the code from your link function as it will execute after the DOM is rendered.

$timeout(function () {
    //do your stuff here as the DOM has finished rendering already
});

Don't forget to inject $timeout in your directive:

.directive("directiveName", function($timeout) {

There are plenty of alternatives but I think this one is cleaner as the $timeout executes after the rendering engine has finished its job.

bosch
  • 1,089
  • 11
  • 16
6

You can't, in a general sense, be ever "fully sure" by just having a directive on the <table> element.

But you can be sure in certain cases. In your case, if the inner content is ng-repeat-ed, then if the array of items over which ngRepeat works is ready, then the actual DOM elements will be ready at the end of the digest cycle. You can capture it after $timeout with 0 delay:

link: function(scope, element){
  $timeout(function(){
    console.log(element.find("tr").length); // will be > 0
  })
}

But, in a general sense, you can't be certain to capture the contents. What if the ngRepeated array is not there yet? Or what if there is an ng-include instead?

<table directive-name ng-include="'templates/tr.html'">
</table>

Or, what if there was a custom directive that worked differently than ngRepeat does?

But if you have full control of the contents, one possible way to know is to include some helper directive as the innermost/last element, and have it contact its parent directiveName when it's linked:

<table directive-name>
    <tr ng-repeat="...">
        <td ng-repeat="...">
          <directive-name-helper ng-if="$last">
        </td>
    </tr>
</table>
.directive("directiveNameHelper", function(){
  return {
    require: "?^directiveName",
    link: function(scope, element, attrs, ctrl){
      if (!ctrl) return;

      ctrl.notifyDone();
    }
  }
})
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • 2
    That is the most comprehensive answer here, I think. Thanks. I still don't get why `$timeout` with 0 delay guarantees that DOM will be ready, but I guess I'll find that in Angular's `$timeout` docs. – Robert Kusznier Aug 18 '15 at 14:35
  • 2
    @Robert, `ng-repeat` has a `$scope.$watchCollection` - this fires after the link phase, and if the array is ready, then it transcludes the `ng-repeat`-ed template and places it in the DOM. `$timeout` with 0 delay executes right after that – New Dev Aug 18 '15 at 14:37
0

A clean way would be to use something like lodash's _.defer method.

You can call it with _.defer(your_func, your_func_arg1, your_func_arg2, ...) inside your link to execute the method, when the current call stack has cleared and everything is ready.

This way, you don't have to estimate a $timeout by yourself.

Martin Seeler
  • 6,874
  • 3
  • 33
  • 45
  • 1
    `_.defer` simply calls `setTimout` with no delay, albeit it does some run-time checks on the parameters you pass in. Unless you're using lodash already, you might as well use NG's built in `$timeout` with no delay. – jusopi Aug 18 '15 at 15:26