ng-repetir evento de finalización

207

Quiero llamar a alguna función jQuery dirigida a div con tabla. Esa tabla está poblada con ng-repeat.

Cuando lo llamo

$(document).ready()

No tengo resultado

también

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

no ayuda

¿Hay alguna forma de ejecutar la función justo después de que se complete la población ng-repeat? He leído un consejo sobre el uso personalizado directive, pero no tengo idea de cómo usarlo con ng-repeat y mi div ...

ChruS
fuente

Respuestas:

241

De hecho, debe usar directivas, y no hay ningún evento vinculado al final de un ciclo ng-Repeat (ya que cada elemento se construye individualmente y tiene su propio evento). Pero a) usar directivas podría ser todo lo que necesita yb) hay algunas propiedades específicas de ng-Repeat que puede usar para hacer que su evento "on ngRepeat finalice".

Específicamente, si todo lo que desea es diseñar / agregar eventos a toda la tabla, puede hacerlo utilizando una directiva que abarque todos los elementos ngRepeat. Por otro lado, si desea abordar cada elemento específicamente, puede usar una directiva dentro de ngRepeat, y actuará en cada elemento, después de que se haya creado.

Entonces, hay el $index, $first, $middley $lastpropiedades que se pueden utilizar para eventos de activación. Entonces para este HTML:

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

Puede usar directivas así:

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');
  };
});

Véalo en acción en este Plunker . ¡Espero eso ayude!

Tiago Roldão
fuente
¿Puedo hacer que myMainDirectiveel código se ejecute solo después del final del ciclo? Necesito actualizar la barra de desplazamiento del padre div.
ChruS
2
¡Por supuesto! Simplemente cambie el "window.alert" a una función de activación de eventos y agárrelo con la directiva principal. Actualicé a De Plunker para hacer esto, como ejemplo.
Tiago Roldão
9
Se recomienda incluir primero la biblioteca jQuery (antes de Angular). Esto hará que todos los elementos angulares se envuelvan con jQuery (en lugar de jqLite). Luego, en lugar de angular.element (element) .css () y $ (element) .children (). Css () simplemente puede escribir element.css () y element.children (). Css (). Ver también groups.google.com/d/msg/angular/6A3Skwm59Z4/oJ0WhKGAFK0J
Mark Rajcok
11
¿Any1 tiene alguna idea de cómo hacer que esto funcione para renderizaciones posteriores en la directiva ngRepeat? es decir: enlace
RavenHursT
1
Esta es una convención de nomenclatura para todas las directivas angulares. Ver docs.angularjs.org/guide/directive#normalization
Tiago Roldão
68

Si simplemente desea ejecutar algo de código al final del ciclo, aquí hay una variación un poco más simple que no requiere manejo adicional de eventos:

<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');
    }
  };
});

Vea una demostración en vivo.

karlgold
fuente
¿Funciona esto si utilizo el controlador como sintaxis? ¿Por favor? En no, ¿hay alguna otra forma que funcione con el controlador como?
Dan Nguyen
63

No es necesario crear una directiva, especialmente solo para tener un ng-repeatevento completo.

ng-init hace la magia por ti

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

el $lastse asegura de que finishedsolo se dispara cuando el último elemento se ha presentado al DOM.

No te olvides de crear $scope.finishedevento.

Feliz codificación !!

EDITAR: 23 oct 2016

En caso de que también desee llamar a la finishedfunción cuando no hay ningún elemento en la matriz, puede usar la siguiente solución

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

Simplemente agregue la línea anterior en la parte superior del ng-repeatelemento. Verificará si la matriz no tiene ningún valor y llamará a la función en consecuencia.

P.ej

<div ng-if="things.length > 0" ng-init="finished()"></div>
<div ng-repeat="thing in things" ng-init="$last && finished()">
Vikas Bansal
fuente
¿Qué pasa si las "cosas" resultan ser una matriz vacía?
Frane Poljak
1
@FranePoljak He agregado la solución en la respuesta.
Vikas Bansal
1
En el caso de matriz vacía, manejarlo de controlador
JSAddict
Repare la respuesta de Vikas Bansal: // use el primer div si desea verificar si la matriz no tiene ningún valor y llame a la función en consecuencia. <div ng-if = "! things.length" ng-hide = "true" ng-init = "terminado ()"> </div> <div ng-repeat = "cosa en las cosas" ng-init = "$ último && terminado () ">
Alex Teslenko
41

Aquí hay una directiva de repetición que llama a una función específica cuando es verdadera. Descubrí que la función llamada debe usar $ timeout con intervalo = 0 antes de realizar la manipulación DOM, como inicializar información sobre herramientas en los elementos renderizados. jsFiddle: http://jsfiddle.net/tQw6w/

En $ scope.layoutDone, intente comentar la línea $ timeout y descomente el mensaje "¡NO CORRECTO!" línea para ver la diferencia en la información sobre herramientas.

<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
fuente
No quería usar un tiempo de espera de $, pero cuando dijiste que podía establecerse en 0, decidí probarlo y funcionó bien. Me pregunto si esto es una cuestión de digestión, y si hay una manera de hacer lo que hace $ timeout sin usar $ timeout.
Jameela Huq
¿Puede explicar por qué funciona esto? Estoy tratando de acceder a las ID dinámicas y solo funciona después de un tiempo de espera con 0 de retraso. Vi esta solución "hacky" en varias publicaciones, pero nadie explica por qué funciona.
Jin
30

Aquí hay un enfoque simple ng-initque ni siquiera requiere una directiva personalizada. Me funcionó bien en ciertos escenarios, por ejemplo, la necesidad de desplazar automáticamente un div de elementos repetidos por ng a un elemento particular en la carga de la página, por lo que la función de desplazamiento debe esperar hasta que el ng-repeatrenderizado termine en el DOM antes de que pueda dispararse.

<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);

    };
});

Tenga en cuenta que esto solo es útil para las funciones que necesita llamar una vez después de que la ng-repeat se muestre inicialmente. Si necesita llamar a una función cada vez que se actualizan los contenidos de ng-repeat, entonces tendrá que usar una de las otras respuestas en este hilo con una directiva personalizada.

dshap
fuente
1
Hermoso, esto hizo el trabajo. Quería seleccionar automáticamente el texto en un cuadro de texto, y el tiempo de espera funcionó. De lo contrario, el texto {{model.value}} se seleccionó y luego se deseleccionó cuando se inyectó el modelo.value vinculado a datos.
JoshGough
27

Complementando la respuesta de Pavel , algo más legible y fácilmente comprensible sería:

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

¿Por qué crees que angular.noophay en primer lugar ...?

Ventajas:

No tiene que escribir una directiva para esto ...

deostroll
fuente
20
¿Por qué utilizar un ternario cuando se puede usar la lógica booleana:ng-init="$last && doSomething( )"
Nate Whittaker
Es más fácil de leer @NateWhittaker (y es más difícil de leer que un operador ternario es impresionante). Es bueno tener un código limpio y fácil de entender
Justin
21

Tal vez un enfoque un poco más simple con ngInitel debouncemétodo de Lodash sin la necesidad de una directiva personalizada:

Controlador:

$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);

Modelo:

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

Actualizar

Incluso hay una solución AngularJS pura más simple que usa un operador ternario:

Modelo:

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

Tenga en cuenta que ngInit utiliza la fase de compilación previa al enlace, es decir, la expresión se invoca antes de que se procesen las directivas secundarias. Esto significa que aún puede ser necesario un procesamiento asincrónico.

Pavel Horal
fuente
Debería tener en cuenta que necesita lodash.js para que esto funcione :) Además: la parte ", 20" de $ scope.refresh =, ¿es esa cantidad de millis antes de que se llame a este método después de la última iteración?
Jørgen Skår Fischer
Se agregó Lodash frente al enlace de rebote . Sí, 20 es demora antes de que se ejecute el método; se cambió a 0 ya que no importa mientras la llamada sea asíncrona.
Pavel Horal
1
También pensando en esto, ya que el operador ternario está permitido en expresiones simples ng-init="$last ? refresh() : false", también funcionaría. Y eso ni siquiera requiere Lodash.
Pavel Horal
<div ng-repeat = "i en [1,2,4]" ng-init = "doSomething ($ last)"> </div> $ scope.doSomething = function (lastElement) {if (lastElem) blah blah blah }
JSAddict
4

También puede ser necesario cuando marca la scope.$lastvariable para ajustar su disparador con unsetTimeout(someFn, 0) . A setTimeout 0es una técnica aceptada en javascript y era imprescindible para mi directiveejecutar correctamente.

Cody
fuente
Encontré el mismo problema. tanto en backbone + angular, si necesita hacer algo como leer valores de propiedad css aplicados a un elemento DOM, no podría encontrar una manera sin el pirateo del tiempo de espera.
chovy
3

Esta es una mejora de las ideas expresadas en otras respuestas para mostrar cómo obtener acceso a las propiedades ngRepeat ($ index, $ first, $ middle, $ last, $ even, $ impar) cuando se usa la sintaxis declarativa y el alcance de aislamiento ( Google recomienda las mejores prácticas) con una directiva de elemento. Tenga en cuenta la diferencia principal: 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
fuente
¿No sería mejor idea usar directivas como atributos que elementos? es decir, restringir: 'A'?
Tejasvi Hegde
2
El punto era mostrar la sintaxis declarativa + el alcance de aislamiento ... sí, puede usar una directiva de atributos aquí si lo desea. Hay un momento y un lugar para cada uno según su propósito.
JoshuaDavid
3

Lo hice de esta manera.

Crea la directiva

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);

Después de agregarlo en la etiqueta donde esta ng-repeat

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

Y listo con eso se ejecutará al final de la repetición ng.

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

Mi solución fue agregar un div para llamar a una función si el elemento fue el último en una repetición.

scotty3
fuente
2

Me gustaría agregar otra respuesta, ya que las respuestas anteriores suponen que el código necesario para ejecutarse después de que se realiza ngRepeat es un código angular, que en ese caso todas las respuestas anteriores dan una solución excelente y simple, algunas más genéricas que otras, y en caso de que sea importante la etapa del ciclo de vida del resumen, puede consultar el blog de Ben Nadel al respecto, con la excepción de usar $ parse en lugar de $ eval.

pero en mi experiencia, como dice el OP, generalmente ejecuta algunos complementos o métodos JQuery en el DOM finalmente compilado, que en ese caso encontré que la solución más simple es crear una directiva con un setTimeout, ya que la función setTimeout se empuja hasta el final de la cola del navegador, siempre es justo después de que todo se hace en angular, generalmente ngReapet que continúa después de la función de enlace posterior de sus padres

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

para quien se pregunte que en ese caso por qué no usar $ timeout, es que causa otro ciclo de digestión que es completamente innecesario

bresleveloper
fuente
1

Tuve que renderizar fórmulas usando MathJax después de que ng-repeat finaliza, ninguna de las respuestas anteriores resolvió mi problema, así que hice lo siguiente. No es una buena solución , pero funcionó para mí ...

<div ng-repeat="formula in controller.formulas">
    <div>{{formula.string}}</div>
    {{$last ? controller.render_formulas() : ""}}
</div>
Joao Paulo
fuente
0

He encontrado una respuesta aquí bien practicado, pero aún así era necesario añadir un retraso

Cree la siguiente directiva:

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

Agréguelo a su repetidor como un atributo, así:

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

Según Radu ,:

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

Para mí funciona, pero aún necesito agregar un setTimeout

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

Si simplemente desea cambiar el nombre de la clase para que se represente de manera diferente, el siguiente código sería suficiente.

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

Este código funcionó para mí cuando tuve un requisito similar para mostrar el artículo comprado en mi lista de compras en letra Strick.

usuario4808836
fuente