211

I want to call some jQuery function targeting div with table. That table is populated with ng-repeat.

When I call it on

$(document).ready()

I have no result.

Also

$scope.$on('$viewContentLoaded', myFunc);

doesn't help.

Is there any way to execute function right after ng-repeat population completes? I've read an advice about using custom directive, but I have no clue how to use it with ng-repeat and my div...

Community
  • 1
  • 1
ChruS
  • 3,707
  • 5
  • 29
  • 40

15 Answers15

244

Indeed, you should use directives, and there is no event tied to the end of a ng-Repeat loop (as each element is constructed individually, and has it's own event). But a) using directives might be all you need and b) there are a few ng-Repeat specific properties you can use to make your "on ngRepeat finished" event.

Specifically, if all you want is to style/add events to the whole of the table, you can do so using in a directive that encompasses all the ngRepeat elements. On the other hand, if you want to address each element specifically, you can use a directive within the ngRepeat, and it will act on each element, after it is created.

Then, there are the $index, $first, $middle and $last properties you can use to trigger events. So for this HTML:

<div ng-controller="Ctrl" my-main-directive>
  <div ng-repeat="thing in things" my-repeat-directive>
    thing {{thing}}
  </div>
</div>

You can use directives like so:

angular.module('myApp', [])
.directive('myRepeatDirective', function() {
  return function(scope, element, attrs) {
    angular.element(element).css('color','blue');
    if (scope.$last){
      window.alert("im the last!");
    }
  };
})
.directive('myMainDirective', function() {
  return function(scope, element, attrs) {
    angular.element(element).css('border','5px solid red');
  };
});

See it in action in this Plunker.

starball
  • 20,030
  • 7
  • 43
  • 238
Tiago Roldão
  • 10,629
  • 3
  • 29
  • 28
  • Can I make `myMainDirective`'s code execute only after end of loop? I need to update scrollbar of parent div. – ChruS Nov 20 '12 at 12:39
  • 2
    Of course! Just change the "window.alert" to an event firing function, and catch it with the main directive. I updated de Plunker to do this, as an example. – Tiago Roldão Nov 20 '12 at 14:28
  • 9
    It is recommended to include the jQuery library first (before Angular). This will cause all Angular elements to be wrapped with jQuery (instead of jqLite). Then instead of angular.element(element).css() and $(element).children().css() you can simply write element.css() and element.children().css(). See also https://groups.google.com/d/msg/angular/6A3Skwm59Z4/oJ0WhKGAFK0J – Mark Rajcok Nov 20 '12 at 18:04
  • Can you tell me if there is a similar method for ng-options? I need to know when angular has built my select box but there is no ng-repeat – Neil Aug 02 '13 at 15:25
  • ng-options is a optional helper attibute, and too simple to have this functionality. You can use ng-repeat to create your options (if you are happy with only text values for your options), and trigger your event the same way as described here. It would halp to know what you need to do with the select element. – Tiago Roldão Aug 02 '13 at 17:32
  • Also, checkout the select2 directive, from angular-ui: https://github.com/angular-ui/ui-select2 - its code may give you some ideas. – Tiago Roldão Aug 02 '13 at 17:33
  • 11
    Any1 have any idea how to get this to work for subsequent renders on the ngRepeat directive? i.e: [link](http://stackoverflow.com/questions/21270845/how-do-i-create-a-callback-for-ng-repeat-orderby) – RavenHursT Jan 22 '14 at 00:01
  • This work fine if you need fire function after last ng-repeat item is loaded, but last is true before data is rendered in html, if you put a breakpoint in your window.alert line notese that data has not yet been propertly rendered, is there an implementation to fire a function when data was rendered in browser browser? – xzegga Dec 27 '14 at 16:08
  • There aren't any events for when the element is rendered, as this framework assumes a continuous updating system. If you move the $emit function inside the $watch for 'thing', however, it will get called whenever that changes. If you only need to call this once, you may try using element.ready(). It really depends on what you need, specifically. See this updated plunker (and try uncommenting the code in $watch): http://plnkr.co/edit/LzFP1Maee0gWuEOxFahl?p=preview – Tiago Roldão Dec 30 '14 at 16:25
  • Why is the directive name in html written using dashed (-) but in JavaScript you used uppercase letters? my-repeat-directive vs. myRepeatDirective – Timo Ernst Apr 12 '15 at 19:07
  • 1
    This is a naming convention for all angular directives. See https://docs.angularjs.org/guide/directive#normalization – Tiago Roldão Apr 13 '15 at 00:01
  • Does this work if i utilize controller as syntax? Please? In not, are there any other way that works with controller as? – Dan Nguyen Apr 06 '17 at 10:23
69

If you simply want to execute some code at the end of the loop, here's a slightly simpler variation that doesn't require extra event handling:

<div ng-controller="Ctrl">
  <div class="thing" ng-repeat="thing in things" my-post-repeat-directive>
    thing {{thing}}
  </div>
</div>
function Ctrl($scope) {
  $scope.things = [
    'A', 'B', 'C'  
  ];
}

angular.module('myApp', [])
.directive('myPostRepeatDirective', function() {
  return function(scope, element, attrs) {
    if (scope.$last){
      // iteration is complete, do whatever post-processing
      // is necessary
      element.parent().css('border', '1px solid black');
    }
  };
});

See a live demo.

animuson
  • 53,861
  • 28
  • 137
  • 147
karlgold
  • 7,970
  • 2
  • 29
  • 22
  • Does this work if i utilize controller as syntax? Please? In not, are there any other way that works with controller as? – Dan Nguyen Apr 06 '17 at 10:23
66

There is no need of creating a directive especially just to have a ng-repeat complete event.

ng-init does the magic for you.

  <div ng-repeat="thing in things" ng-init="$last && finished()">

the $last makes sure, that finished only gets fired, when the last element has been rendered to the DOM.

Do not forget to create $scope.finished event.

Happy Coding!!

EDIT: 23 Oct 2016

In case you also want to call the finished function when there is no item in the array then you may use the following workaround

<div style="display:none" ng-init="things.length < 1 && finished()"></div>
//or
<div ng-if="things.length > 0" ng-init="finished()"></div>

Just add the above line on the top of the ng-repeat element. It will check if the array is not having any value and call the function accordingly.

E.g.

<div ng-if="things.length > 0" ng-init="finished()"></div>
<div ng-repeat="thing in things" ng-init="$last && finished()">
Vikas Bansal
  • 10,662
  • 14
  • 58
  • 100
41

Here is a repeat-done directive that calls a specified function when true. I have found that the called function must use $timeout with interval=0 before doing DOM manipulation, such as initializing tooltips on the rendered elements. jsFiddle: http://jsfiddle.net/tQw6w/

In $scope.layoutDone, try commenting out the $timeout line and uncommenting the "NOT CORRECT!" line to see the difference in the tooltips.

<ul>
    <li ng-repeat="feed in feedList" repeat-done="layoutDone()" ng-cloak>
    <a href="{{feed}}" title="view at {{feed | hostName}}" data-toggle="tooltip">{{feed | strip_http}}</a>
    </li>
</ul>

JS:

angular.module('Repeat_Demo', [])

    .directive('repeatDone', function() {
        return function(scope, element, attrs) {
            if (scope.$last) { // all are rendered
                scope.$eval(attrs.repeatDone);
            }
        }
    })

    .filter('strip_http', function() {
        return function(str) {
            var http = "http://";
            return (str.indexOf(http) == 0) ? str.substr(http.length) : str;
        }
    })

    .filter('hostName', function() {
        return function(str) {
            var urlParser = document.createElement('a');
            urlParser.href = str;
            return urlParser.hostname;
        }
    })

    .controller('AppCtrl', function($scope, $timeout) {

        $scope.feedList = [
            'http://feeds.feedburner.com/TEDTalks_video',
            'http://feeds.nationalgeographic.com/ng/photography/photo-of-the-day/',
            'http://sfbay.craigslist.org/eng/index.rss',
            'http://www.slate.com/blogs/trending.fulltext.all.10.rss',
            'http://feeds.current.com/homepage/en_US.rss',
            'http://feeds.current.com/items/popular.rss',
            'http://www.nytimes.com/services/xml/rss/nyt/HomePage.xml'
        ];

        $scope.layoutDone = function() {
            //$('a[data-toggle="tooltip"]').tooltip(); // NOT CORRECT!
            $timeout(function() { $('a[data-toggle="tooltip"]').tooltip(); }, 0); // wait...
        }

    })
Joseph Oster
  • 5,507
  • 1
  • 20
  • 11
  • I didn't want to use a $timeout, but when you said it could be set to 0, I decided to give it a try and it worked well. I wonder if this is a matter of digestion, and if there is a way to do what the $timeout is doing without using $timeout. – Jameela Huq Jun 13 '16 at 23:13
  • Can you please explain why does this work? I'm trying to access dynamic ID's and it only works after having a timeout with 0 delay. I saw this "hacky" solution in several posts but no one explains why it works. – Jin Jul 06 '18 at 21:05
30

Here's a simple approach using ng-init that doesn't even require a custom directive. It's worked well for me in certain scenarios e.g. needing to auto-scroll a div of ng-repeated items to a particular item on page load, so the scrolling function needs to wait until the ng-repeat has finished rendering to the DOM before it can fire.

<div ng-controller="MyCtrl">
    <div ng-repeat="thing in things">
        thing: {{ thing }}
    </div>
    <div ng-init="fireEvent()"></div>
</div>

myModule.controller('MyCtrl', function($scope, $timeout){
    $scope.things = ['A', 'B', 'C'];

    $scope.fireEvent = function(){

        // This will only run after the ng-repeat has rendered its things to the DOM
        $timeout(function(){
            $scope.$broadcast('thingsRendered');
        }, 0);

    };
});

Note that this is only useful for functions you need to call one time after the ng-repeat renders initially. If you need to call a function whenever the ng-repeat contents are updated then you'll have to use one of the other answers on this thread with a custom directive.

dshap
  • 1,382
  • 14
  • 19
  • 1
    Beautiful, this did the job. I wanted to auto-select text in a textbox, and the timeout did the trick. Otherwise, the {{model.value}} text got selected and then deselected when the data-bound model.value was injected. – JoshGough May 14 '15 at 16:12
28

Complementing Pavel's answer, something more readable and easily understandable would be:

<ul>
    <li ng-repeat="item in items" 
        ng-init="$last ? doSomething() : angular.noop()">{{item}}</li>
</ul>

Why else do you think angular.noop is there in the first place...?

Advantages:

You don't have to write a directive for this...

Community
  • 1
  • 1
deostroll
  • 11,661
  • 21
  • 90
  • 161
  • 20
    Why use a ternary when you can use boolean logic: `ng-init="$last && doSomething( )"` – Nate Whittaker Sep 10 '15 at 20:48
  • Its' easier to read @NateWhittaker (and being harder to read than a ternary operator is impressive). It's good to have clean, and simple to understand code – Justin Mar 12 '20 at 23:38
21

Maybe a bit simpler approach with ngInit and Lodash's debounce method without the need of custom directive:

Controller:

$scope.items = [1, 2, 3, 4];

$scope.refresh = _.debounce(function() {
    // Debounce has timeout and prevents multiple calls, so this will be called 
    // once the iteration finishes
    console.log('we are done');
}, 0);

Template:

<ul>
    <li ng-repeat="item in items" ng-init="refresh()">{{item}}</li>
</ul>

Update

There is even simpler pure AngularJS solution using ternary operator:

Template:

<ul>
    <li ng-repeat="item in items" ng-init="$last ? doSomething() : null">{{item}}</li>
</ul>

Be aware that ngInit uses pre-link compilation phase - i.e. the expression is invoked before child directives are processed. This means that still an asynchronous processing might be required.

Pavel Horal
  • 17,782
  • 3
  • 65
  • 89
  • Should note that you need the lodash.js for this to work :) Also: the ",20" part of the $scope.refresh = , is that number of millis before this method is called after the last itteration? – Jørgen Skår Fischer Nov 22 '14 at 18:40
  • Added *Lodash* in front of the *debounce* link. Yes, 20 is delay before the method is executed - changed to 0 as it does not matter as long as the call is async. – Pavel Horal Nov 22 '14 at 19:06
  • 1
    Also thinking about this, as ternary operator is allowed in expressions simple `ng-init="$last ? refresh() : false"` would work as well. And that does not even require Lodash. – Pavel Horal Nov 22 '14 at 20:42
  • $scope.doSomething = function(lastElement) { if(lastElem) blah blah blah } – JSAddict Oct 24 '16 at 11:14
4

It may also be necessary when you check the scope.$last variable to wrap your trigger with a setTimeout(someFn, 0). A setTimeout 0 is an accepted technique in javascript and it was imperative for my directive to run correctly.

Cody
  • 9,785
  • 4
  • 61
  • 46
  • i found same issue. both in backbone + angular, if you need to do something like read css property values applied to a DOM element, I could not figure out a way w/o the timeout hack. – chovy Jan 10 '15 at 04:25
4

I did it this way.

Create the directive

function finRepeat() {
    return function(scope, element, attrs) {
        if (scope.$last){
            // Here is where already executes the jquery
            $(document).ready(function(){
                $('.materialboxed').materialbox();
                $('.tooltipped').tooltip({delay: 50});
            });
        }
    }
}

angular
    .module("app")
    .directive("finRepeat", finRepeat);

After you add it on the label where this ng-repeat

<ul>
    <li ng-repeat="(key, value) in data" fin-repeat> {{ value }} </li>
</ul>

And ready with that will be run at the end of the ng-repeat.

4
<div ng-repeat="i in items">
        <label>{{i.Name}}</label>            
        <div ng-if="$last" ng-init="ngRepeatFinished()"></div>            
</div>

My solution was to add a div to call a function if the item was the last in a repeat.

scotty3
  • 153
  • 8
3

This is an improvement of the ideas expressed in other answers in order to show how to gain access to the ngRepeat properties ($index, $first, $middle, $last, $even, $odd) when using declarative syntax and isolate scope (Google recommended best practice) with an element-directive. Note the primary difference: scope.$parent.$last.

angular.module('myApp', [])
.directive('myRepeatDirective', function() {
  return {
    restrict: 'E',
    scope: {
      someAttr: '='
    },
    link: function(scope, element, attrs) {
      angular.element(element).css('color','blue');
      if (scope.$parent.$last){
        window.alert("im the last!");
      }
    }
  };
});
JoshuaDavid
  • 8,861
  • 8
  • 47
  • 55
  • Wouldn't it be better idea to use directives as attribs than elements ? i.e. restrict: 'A' ? – Tejasvi Hegde Jul 12 '15 at 14:01
  • 2
    The point was to show declarative syntax + isolate scope... yes, you are welcome to use an attribute directive here if you desire. There is a time and place for either depending on your purpose. – JoshuaDavid Jul 13 '15 at 17:01
2

i would like to add another answer, since the preceding answers takes it that the code needed to run after the ngRepeat is done is an angular code, which in that case all answers above give a great and simple solution, some more generic than others, and in case its important the digest life cycle stage you can take a look at Ben Nadel's blog about it, with the exception of using $parse instead of $eval.

but in my experience, as the OP states, its usually running some JQuery plugins or methods on the finnaly compiled DOM, which in that case i found that the most simple solution is to create a directive with a setTimeout, since the setTimeout function gets pushed to the end of the queue of the browser, its always right after everything is done in angular, usually ngReapet which continues after its parents postLinking function

angular.module('myApp', [])
.directive('pluginNameOrWhatever', function() {
  return function(scope, element, attrs) {        
    setTimeout(function doWork(){
      //jquery code and plugins
    }, 0);        
  };
});

for whoever wondering that in that case why not to use $timeout, its that it causes another digest cycle that is completely unnecessary

bresleveloper
  • 5,940
  • 3
  • 33
  • 47
1

I had to render formulas using MathJax after ng-repeat ends, none of the above answers solved my problem, so I made like below. It's not a nice solution, but worked for me...

<div ng-repeat="formula in controller.formulas">
    <div>{{formula.string}}</div>
    {{$last ? controller.render_formulas() : ""}}
</div>
João Paulo
  • 6,300
  • 4
  • 51
  • 80
0

I found an answer here well practiced, but it was still necessary to add a delay

Create the following directive:

angular.module('MyApp').directive('emitLastRepeaterElement', function() {
return function(scope) {
    if (scope.$last){
        scope.$emit('LastRepeaterElement');
    }
}; });

Add it to your repeater as an attribute, like this:

<div ng-repeat="item in items" emit-last-repeater-element></div>

According to Radu,:

$scope.eventoSelecionado.internamento_evolucoes.forEach(ie => {mycode});

For me it works, but I still need to add a setTimeout

$scope.eventoSelecionado.internamento_evolucoes.forEach(ie => {
setTimeout(function() { 
    mycode
}, 100); });
Blomersi
  • 29
  • 7
-4

If you simply wants to change the class name so it will rendered differently, below code would do the trick.

<div>
<div ng-show="loginsuccess" ng-repeat="i in itemList">
    <div id="{{i.status}}" class="{{i.status}}">
        <div class="listitems">{{i.item}}</div>
        <div class="listitems">{{i.qty}}</div>
        <div class="listitems">{{i.date}}</div>
        <div class="listbutton">
            <button ng-click="UpdateStatus(i.$id)" class="btn"><span>Done</span></button>
            <button ng-click="changeClass()" class="btn"><span>Remove</span></button>
        </div>
    <hr>
</div>

This code worked for me when I had a similar requirement to render the shopped item in my shopping list in Strick trough font.

ryanyuyu
  • 6,366
  • 10
  • 48
  • 53