AngularJS: Linking to elements in a directive that uses ng-repeat

21,858

Solution 1

I found that if created another directive that I added to the element where the ng-repeat was being created it would be notified for each element the repeat created. Then I could simply $emit an event that the parent directive could listen for. At which point it could perform the linking to the repeated elements there. This worked quite nicely especially for multiple ng-repeats within the dom because I could separate them by their event type which would be passed to the new directive.

Here is my directive:

App.directive("onRepeatDone", function() {
    return {
        restrict: 'A',
        link: function($scope, element, attributes ) {
            $scope.$emit(attributes["onRepeatDone"] || "repeat_done", element);
        }
    }
});

Here is the usage of that new directive in the template:

<ul>
    <li on-repeat-done="domain_done" ng-repeat="domain in domains">...</li>
</ul>

Then inside the parent directive I could do the following:

link: function( $scope, element, attributes ) {
    $scope.$on('domain_done', function( domainElement ) {
        domainElement.find('.someElementInsideARepeatedElement').click(...);
    } );
}

Solution 2

$watch should be able to achieve what you want to do:

link: function(scope, elem, attrs) {
    var init = function() {
        // your initialization code
    };
    var ulElem = elem.children()[0]; // ul element

    var unreg = scope.$watch(function() {
        return ulElem.children.length === scope.domains.length; // check if all "li" are generated
    }, function() {
        // at this point, the ul is rendered
        init();
        unreg(); // unregister the watcher for performance, since the init function only need to be called once
    });
}

I have created a plunk to demonstrate this solution.

Solution 3

I just ran across this exact issue and found what I believe is a better solution. Basically, you can use $timeout to queue a task for after the ng-repeats have finished rendering.

Note: this doesn't mean that you're actually utilizing the timer or any sort of time delay; it is just a simple way to append a function to the end of the $digest loop.

Below is an excerpt of my code. The directive uses ng-repeat to display the tabs, thus, I have to wait until after it has been rendered to run my tabSelected() method which adds the active class to the HTML element.

link($scope:ng.IScope, element:JQuery, attributes:ng.IAttributes):void {
  if (this.autoActivateFirstTab && this.tabs && this.tabs.length > 0) {
    var self = this;

    var selectFirstTab = function() {
      self.tabSelected(self.tabs[0]);
    };

    // Add a $timeout task to the end of the $digest loop to select the
    // first tab after the loop finishes
    self.$timeout(selectFirstTab, 0);
  }
}

I found the solution from http://blog.brunoscopelliti.com/run-a-directive-after-the-dom-has-finished-rendering which has a live demo that shows it in action.

Parting words: If you are testing your code (like you should) with Karma, you will need to call $timeout.flush() in your test to ensure that it runs.

I wound up having to run the following within my test to get it to work properly:

// Re-digest to ensure we see the changes
scope.$digest();

// Flush any pending $timeout tasks to ensure we see changes
try {
  this.$timeout.verifyNoPendingTasks();
} catch ( aException ) {
  this.$timeout.flush();
}

Solution 4

I think this is because of the angular bug.You can see this here

One workaround is to remove the template url and use the template in the directive itself or use $templateCache.get('/app/partials/my_directive.html')

This worked for me :)

Share:
21,858

Related videos on Youtube

chubbsondubs
Author by

chubbsondubs

Updated on April 26, 2020

Comments

  • chubbsondubs
    chubbsondubs about 4 years

    I have a simple directive where the template uses ng-repeat inside it. I need to run some code to instantiate a jquery component against some of the elements created by the ng-repeat directive. The problem is that if I put this code in the link function. The ng-repeat hasn't built those elements yet so nothing is instantiated.

    App.directive('myDirective', ['$compile', '$timeout', function($compile, $timeout) {
      return {
        scope: {
            domains: '='
        },
        templateUrl: '/app/partials/my_directive.html',
        link: function($scope, element, attributes) {
            element.find('.standard-help').tooltip('destroy');
            element.find('.standard-help').tooltip({placement: 'top', trigger: 'click hover focus'});
        }
      };
    }
    

    The template would look like the following. I'm trying to attach

    <ul class="media-list domainList">
      <li class="media" style="position:relative;" ng-repeat="domain in domains">
        <a class="domainHeader" href="javascript://">
            <span class="domainHeader">{{domain.tag}}</span>
        </a>
        <div class="media-body" style="margin-left: 52px;">
            <ul class="standardsList">
                <li ng-class="{ standardDisplayed: lessonLayout == 'standards' }" ng-hide="standard.lessons.length == 0" ng-repeat="standard in domain.standards">
                    <a href="javascript://" title="{{standard.description}}" ng-show="lessonLayout == 'standards'" class="standard-help pull-right"><i class="icon-question-sign"></i></a>
                    <h6 ng-show="lessonLayout == 'standards'">{{standard.tag}}</h6>
                    <ul class="lessonsList">
                        <li ng-class="{ lesson: true }" ng-repeat="lesson in standard.lessons" ng-click="onLessonSelected(lesson)">
                            <i class="icon-lock lesson-locked"></i>
                            <div>{{lesson.title}}</div>
                        </li>
                    </ul>
                </li>
            </ul>
        </div>
      </li>
    </ul>
    

    I've tried using $watch() and $observe() to register a callback when the domains change and instantiate the tooltip code then. However, I can't seem to get it to call me at the right time. Any ideas what I'm missing?

    • dimirc
      dimirc almost 11 years
      Maybe you could force compile from linking function. element = angular.element($compile(template)(scope));
    • rGil
      rGil almost 11 years
      If the issue is waiting for the dom to render, you can wrap your link function items in a $timeout. $timeout(function(){element.find(blabla);element.find(foofoo‌​);},0). If this isn't the fix, then the problem does not lie with dom rendering.
    • chubbsondubs
      chubbsondubs almost 11 years
      Seems like this would be a common problem so I have to think there are better ways to accomplish this without resorting to hacks like using timeouts.
  • chubbsondubs
    chubbsondubs almost 11 years
    Thanks for your answer, but I found a more robust way of handling this.
  • mariachimike
    mariachimike almost 11 years
    Thanks, this is a very useful answer. I have a related question: is there a standard pattern for detecting that all the repeats are done? I may not want to fire an event for every added child, but just once on completion.
  • chubbsondubs
    chubbsondubs almost 11 years
    You could put an if statement in the directive to fire only on the final scope.it == scope.$last.
  • Phil Thomas
    Phil Thomas almost 11 years
    You don't need to inject $compile in the directive declaration. ;)
  • scottt732
    scottt732 almost 10 years
    Thanks. I was trying to track when 4 ng-repeats were finished rendering and this was a better fit than the directive chubbsondubs' directive approach.
  • net.uk.sweet
    net.uk.sweet over 9 years
    I liked this approach better too - seemed overkill to me to have another directive. Thanks.
  • chubbsondubs
    chubbsondubs over 9 years
    Using $timeout is never a better solution when you can use events properly. Using timers depends so much on the underlying platform running the code. This means it can be very unreliable because a phone CPU is not the same as a desktop and depending on what's going on the CPU it could still screw up. So it might work once then not work because the CPU was doing something intense. Bottom line is it's not reliable. Not to mention you usually have to play it very conservative to make sure you don't accidentally fire it in the middle of the repeat render so it's always slower.
  • Topher Fangio
    Topher Fangio over 9 years
    @chubbsondubs I agree in general, but the point of this $timeout is not actually to wait any particular amount of time; it simply appends an event to the end of the current $digest loop, so it should work on any platform as it's not dependent upon any actual time (thus why the number of milliseconds is 0). Does this change your view of this particular solution? I always love feedback :-)
  • jjmontes
    jjmontes about 8 years
    Shouldn't it be restrict instead of restriction?
  • returnvoid
    returnvoid almost 8 years
    This looks great but when I try to get the element in the parent directive I'm not sure what kind of elements I'm getting and when I try to run find() method on (in your case) domainElement says: find is not a function
  • chubbsondubs
    chubbsondubs almost 8 years
    find() method comes from jquery. If you don't have jquery loaded that could be the reason.
  • oscar.fimbres
    oscar.fimbres almost 8 years
    Also, you can add a condition to fire the emit when last item has been executed: ($scope.$last) with priority >= 1.
  • JayIsTooCommon
    JayIsTooCommon over 6 years
    This is much better solution imo.