ng-repeat finish event
Asked Answered
F

15

212

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...

Frig answered 20/11, 2012 at 10:23 Comment(0)
P
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.

Penal answered 20/11, 2012 at 11:51 Comment(12)
Can I make myMainDirective's code execute only after end of loop? I need to update scrollbar of parent div.Frig
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.Ailsun
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 groups.google.com/d/msg/angular/6A3Skwm59Z4/oJ0WhKGAFK0JRemoved
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-repeatEmbolism
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.Ailsun
Also, checkout the select2 directive, from angular-ui: github.com/angular-ui/ui-select2 - its code may give you some ideas.Ailsun
Any1 have any idea how to get this to work for subsequent renders on the ngRepeat directive? i.e: linkComplication
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?Detainer
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): plnkr.co/edit/LzFP1Maee0gWuEOxFahl?p=previewAilsun
Why is the directive name in html written using dashed (-) but in JavaScript you used uppercase letters? my-repeat-directive vs. myRepeatDirectiveTanka
This is a naming convention for all angular directives. See docs.angularjs.org/guide/directive#normalizationAilsun
Does this work if i utilize controller as syntax? Please? In not, are there any other way that works with controller as?Locoweed
C
70

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.

Chamonix answered 8/12, 2012 at 6:36 Comment(1)
Does this work if i utilize controller as syntax? Please? In not, are there any other way that works with controller as?Locoweed
S
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()">
Suzy answered 17/6, 2016 at 10:42 Comment(4)
What if "things" turn out to be an empty array?Carpi
@FranePoljak I have add the solution in the answer.Suzy
In case of empty array, handle it in controllerPyramidal
Fix Vikas Bansal answer: //use first div if you want check if the array is not having any value and call the function accordingly. <div ng-if="!things.length" ng-hide="true" ng-init="finished()"></div> <div ng-repeat="thing in things" ng-init="$last && finished()">Spirelet
C
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...
        }

    })
Crabwise answered 21/4, 2013 at 18:18 Comment(2)
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.Korwun
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.Boot
O
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.

Offend answered 18/7, 2014 at 5:34 Comment(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.Ribose
T
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...

Typo answered 2/7, 2015 at 14:38 Comment(2)
Why use a ternary when you can use boolean logic: ng-init="$last && doSomething( )"Pentheas
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 codeCrosscut
S
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.

Sales answered 21/11, 2014 at 10:6 Comment(4)
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?Nombril
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.Sales
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.Sales
<div ng-repeat="i in [1,2,4]" ng-init="doSomething($last)"></div> $scope.doSomething = function(lastElement) { if(lastElem) blah blah blah }Pyramidal
U
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.

Uriah answered 20/9, 2013 at 19:43 Comment(1)
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.Litton
L
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.

Luciferous answered 17/5, 2016 at 4:32 Comment(0)
C
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.

Cuspidor answered 25/5, 2017 at 21:41 Comment(0)
J
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!");
      }
    }
  };
});
Juristic answered 23/1, 2015 at 18:21 Comment(2)
Wouldn't it be better idea to use directives as attribs than elements ? i.e. restrict: 'A' ?Janice
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.Juristic
J
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

Jacquijacquie answered 28/12, 2015 at 15:39 Comment(0)
H
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>
Historiated answered 3/10, 2016 at 17:43 Comment(0)
W
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); });
Wang answered 20/4, 2018 at 16:32 Comment(0)
S
-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.

Spavin answered 20/4, 2015 at 4:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.