4

I'm using an ng-repeat inside a directive's template.

myApp.directive("test", function () {
    return {
   restrict: 'C',
     scope: {
      bindVar: '='
    },
        template: '<div>\
<div class="item" ng-repeat="sel in bindVar">{{sel.display}}</div>\
     </div>',
     link: function ($scope, element, attrs) {


     //   setTimeout(function() { 
        alert($('.item').length); // <--- RETURNS 0, IF I ADD TIMEOUT RETURNS 3
   // },0);



     } // of link
    } // of return
});

http://jsfiddle.net/foreyez/t4590zbr/

However, when the link() function is called I don't seem to get access to the items that have been created. In order to do this I need to set a timeout of 0 (after that it works).

I read this in the following article: http://lorenzmerdian.blogspot.com/2013/03/how-to-handle-dom-updates-in-angularjs.html

I also saw a similar Stack Overflow answer where the OP marked Timeout as the answer: DOM elements not ready in AngularJS Directive's link() function

But c'mon, there's got to be another way!

I'm crossing my fingers that this hacky solution is wrong, and there's some way that angular provides a callback when the DOM has been created via a directive. Or do I really rely on .. timeouts? (really? :/)

Community
  • 1
  • 1
Shai UI
  • 50,568
  • 73
  • 204
  • 309
  • Not sure what you're asking... seems to work fine for me http://plnkr.co/edit/FQB4VPsJx7bDD4v1CixU?p=preview – yangli-io Feb 10 '15 at 01:58
  • sorry see my updated question. i'm basically trying to access dom nodes with jquery in the link() function. this only works when i do a timeout. another way that i found that works is putting a $( document ).ready(function() { }); inside my link() function... hacky I know. and btw, i know i should be using angular but i prefer jquery for dom manips. – Shai UI Feb 10 '15 at 02:02
  • 1
    Can you tell us what you intend to do once you get a hold of the dom element? Maybe we can come up a more _angular_ way of doing what are are trying to do. – Sylvain Feb 10 '15 at 02:21
  • Can you not just use jqlite element object provided to the link function? I'm guessing here but maybe jquery is looking for something in the page, which doesn't happen until after your directive is linked. – hassassin Feb 10 '15 at 02:22
  • Well, yes - you have `templateUrl`, which loads asynchronously. You are creating a race condition with the timeout - so that is not a solution – New Dev Feb 10 '15 at 02:26
  • forget templateUrl, even if I use "template" the problem occurs – Shai UI Feb 10 '15 at 02:26
  • i've been researching this problem for a couple hours now. seems like everyone is using this $timeout hack. i just need to access the dom after linking because i want to do javascript calculations for something. – Shai UI Feb 10 '15 at 02:27
  • Yes this will be a problem, you can either change your design or stick with putting the dom manipulation at the end of the event loop like you have done. I had the same issue before where I was trying to change the height and width depending on what ng-repeat spit out. I solved it by binding element an dom update event. This solved my issues because this way I could change the height whenever a dom item is detected. But yes, you will need to defer it to the end of the event loop since the ng-repeat will not operate before your directive. And stay away from jquery, use jqlite. – yangli-io Feb 10 '15 at 02:29
  • i've created a jsfiddle that better illustrates the problem see my question – Shai UI Feb 10 '15 at 02:32
  • @foreyez, then I'm affraid you have no other choice. – Sylvain Feb 10 '15 at 02:46
  • @Sylvain no other choice than to use timeout, is it 100% reliable for all devices? – Shai UI Feb 10 '15 at 02:48
  • @foreyez yes that is what I meant. it worked for me – Sylvain Feb 10 '15 at 03:02

3 Answers3

4

$timeout is, in fact, a legitimate way to solve this when you use inline template (as opposed to templateUrl). It would not create a race condition.

What happens is, Angular goes over the DOM and collects directives and their pre- and post-link functions (by compiling the directives). Then, the link functions for each directive for each node (i.e. DOM element) are executed.

Normally, the template for the node (to which the directive applies) is already part of the DOM. And so, if you have the following directive:

.directive("foo", function(){
  return {
    template: '<span class="fooClass">foo</span>',
    link: function(scope, element){
      // prints "<span class="fooClass">foo</span>"
      console.log(element.html()); 
    }
  }
}

it can find the $(".fooClass") element.

However, if a directive uses transclude: 'element', like ng-if (ngIf.js) and ng-repeat (ngRepeat.js) directives do, Angular rewrites the directive as a comment (compile.js), and so $(".item") (in your example) is not there until ng-repeat places it there. They do so in their scope.$watch function (ngIf.js), depending on the value they are watching, and this may happen at the next digest cycle. So, even when your post-link function runs, the actual element that you are search for is still not there.

.directive("foo", function(){
  return {
    template: '<span ng-if="true" class="fooClass">foo</span>',
    link: function(scope, element){
      // prints "<!-- ngIf: true -->"
      console.log(element.html());
    }
  }
}

But it will be there - definitely - when $timeout runs.

New Dev
  • 48,427
  • 12
  • 87
  • 129
  • thanks for the detailed answer. however if you look at the blogpost of lorenz i posted above it seems like he needed to do multiple settimeouts. one for the ng-repeat, and another for the actual dimensions of the dom elements. it seems like doing things this way is a big old hack. not that my solution is any better (getting a callback) when ng-repeat finishes still doesn't tell me if the dom node has been rendered. i really don't have a solution yet. – Shai UI Feb 10 '15 at 21:25
  • @foreyez, one `$timeout` assures that the element has been added to the DOM. Rendering comes later, as I understand it, and totally depends on the complexity of styles, computer speed, etc... This has nothing to do with Angular - you should not assume then, when element is added, that it is properly sized. So, second `$timeout` *is* a hack. – New Dev Feb 10 '15 at 22:33
  • yeah, but rendering is "kinda" important, no? why can't angular supply some kind of callback when rendering is done. just like document ready when you deal with jquery. is there a way to do this? – Shai UI Feb 11 '15 at 01:40
  • @foreyez, Angular is a framework that facilitates the MVVM paradigm, and unlike jQuery, not meant for general augmentation of DOM with JavaScript. Specifically, [there is no event](http://stackoverflow.com/a/9779171/968155) for when style rules have applied to an element. Additionally, unless I misunderstood you, your question only asks about when elements are inserted. – New Dev Feb 11 '15 at 02:54
  • I am not seeing the DOM there 100% of the time inside the `$timeout` callback. The comments after compiling `ng-repeat` are still there and the DOM is not because `ng-repeat` may defer creating it until the next `$digest` cycle... Using `$timeout` once does not ensure that the element has been added to the DOM with ng-repeat. You either need multiple `$timeout`s or `$emit` an event like the other answers. However, with other directives such as ng-class I am seeing the DOM there 100% of the time in a `$timeout` callback. It's just with `ng-repeat` that it does not work consistently. – jhiller Jun 21 '16 at 14:43
  • `$timeout(() => { $timeout(() => { var height = element.children(':first').outerHeight(); //find the height of the first item output by ng-repeat }, 0, false); }, 0, true);` – jhiller Jun 21 '16 at 15:04
  • So I just realized the reason this was happening was because the ng-repeat data source is being returned by a $http promise, and that was taking too long to resolve, so it didn't matter how many timeouts were added. – jhiller Jul 01 '16 at 16:09
1

The way I do this is with another directive. As an example:

.directive('elementReady', function() {
   return {
       restrict: 'A',
       link: function(scope, elem, attr) {
           //In here, you can do things like:
           if(scope.$last) {
              //this element is the last element in an ng-repeat
           }
           if(scope.$first) {
              //first element in ng-repeat
           }

           //do jQuery and javascript calculations (elem has been added to the DOM at this point)
       }
   };
});


<table class="box-table" width="100%">
        <thead>
            <tr>
                <th class='test' scope="col" ng-repeat="column in listcolumns" element-ready>{{column.title}}</th>
            </tr>
        </thead>
</table>

Obviously, you will need to customize how you propagate those events to your outer scope ($emit, through bound functions, etc).

Joe Enzminger
  • 11,110
  • 3
  • 50
  • 75
1

Taking Joe's Answer and another answer I found on stackoverflow I was able to do this by:

myApp.directive('myRepeatDirective', function() {
  return function(scope, element, attrs) {
    if (scope.$last){
      scope.$emit('LastElem');
    }
  };
});

and then in my original link function:

 $scope.$on('LastElem', function(event){
        alert($('.item').length);
    });

and template looks like:

<div>
<div class="item" ng-repeat="sel in bindVar" my-repeat-directive>{{sel.display}}</div>
</div>

http://jsfiddle.net/foreyez/t4590zbr/3/

but I'm still not loving this solution.. seems kind of blehhh

Shai UI
  • 50,568
  • 73
  • 204
  • 309