Llamar a una función cuando ng-repeat ha finalizado

249

Lo que estoy tratando de implementar es básicamente un controlador "en repetición finalizada de renderizado". Puedo detectar cuándo está hecho, pero no puedo entender cómo activar una función a partir de él.

Verifique el violín: http://jsfiddle.net/paulocoelho/BsMqq/3/

JS

var module = angular.module('testApp', [])
    .directive('onFinishRender', function () {
    return {
        restrict: 'A',
        link: function (scope, element, attr) {
            if (scope.$last === true) {
                element.ready(function () {
                    console.log("calling:"+attr.onFinishRender);
                    // CALL TEST HERE!
                });
            }
        }
    }
});

function myC($scope) {
    $scope.ta = [1, 2, 3, 4, 5, 6];
    function test() {
        console.log("test executed");
    }
}

HTML

<div ng-app="testApp" ng-controller="myC">
    <p ng-repeat="t in ta" on-finish-render="test()">{{t}}</p>
</div>

Respuesta : violín de trabajo desde el acabadomove: http://jsfiddle.net/paulocoelho/BsMqq/4/

PCoelho
fuente
Por curiosidad, ¿cuál es el propósito del element.ready()fragmento? Quiero decir ... ¿es algún tipo de complemento jQuery que tienes, o debería activarse cuando el elemento está listo?
gion_13 01 de
1
Uno podría hacerlo usando directivas integradas como ng-init
semiomant
Posible duplicado del evento de finalización ng-repeat
Damjan Pavlica
duplicado de stackoverflow.com/questions/13471129/… , vea mi respuesta allí
bresleveloper

Respuestas:

400
var module = angular.module('testApp', [])
    .directive('onFinishRender', function ($timeout) {
    return {
        restrict: 'A',
        link: function (scope, element, attr) {
            if (scope.$last === true) {
                $timeout(function () {
                    scope.$emit(attr.onFinishRender);
                });
            }
        }
    }
});

Tenga en cuenta que no lo usé, .ready()sino que lo envolví en un $timeout. $timeoutse asegura de que se ejecute cuando los elementos ng repetidos hayan REALIZADO REALMENTE la representación (porque $timeoutse ejecutará al final del ciclo de resumen actual, y también llamará $applyinternamente, a diferenciasetTimeout ). Entonces, una vez ng-repeatfinalizado, usamos $emitpara emitir un evento a ámbitos externos (ámbitos primarios y primarios).

Y luego en su controlador, puede atraparlo con $on:

$scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) {
    //you also get the actual event object
    //do stuff, execute functions -- whatever...
});

Con html que se parece a esto:

<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
    <div>{{item.name}}}<div>
</div>
principio holográfico
fuente
10
+1, pero usaría $ eval antes de usar un evento sin acoplamiento. Vea mi respuesta para más detalles.
Mark Rajcok
1
@PigalevPavel Creo que estás confundiendo $timeout(que es básicamente setTimeout+ Angular $scope.$apply()) con setInterval. $timeoutse ejecutará solo una vez por la ifcondición, y eso será al comienzo del próximo $digestciclo. Para obtener más información sobre los tiempos de espera de JavaScript, consulte: ejohn.org/blog/how-javascript-timers-work
holographic-principio
1
@finishingmove Tal vez no entiendo algo :) Pero mira, tengo ng-repeat en otra ng-repeat. Y quiero saber con certeza cuándo están todos terminados. Cuando uso su script con $ timeout solo para padres ng-repeat todo funciona bien. Pero si no uso $ timeout, recibo una respuesta antes de que los niños repitan. ¿Quiero saber por qué? ¿Y puedo estar seguro de que si uso su script con $ timeout para el ng-repeat padre siempre obtendré una respuesta cuando todos los ng-repites hayan terminado?
Pigalev Pavel
1
Por qué usaría algo así como setTimeout()con 0 de retraso es otra pregunta, aunque debo decir que nunca he encontrado una especificación real para la cola de eventos del navegador en ningún lado, solo lo que está implicado por un solo subproceso y la existencia de setTimeout().
rakslice
1
Gracias por esta respuesta Tengo una pregunta, ¿por qué necesitamos asignar on-finish-render="ngRepeatFinished"? Cuando asigno on-finish-render="helloworld", funciona igual.
Arnaud
89

Use $ evalAsync si desea que su devolución de llamada (es decir, test ()) se ejecute después de que se construya el DOM, pero antes de que se muestre el navegador. Esto evitará el parpadeo - ref .

if (scope.$last) {
   scope.$evalAsync(attr.onFinishRender);
}

Violín .

Si realmente desea llamar a su devolución de llamada después de renderizar, use $ timeout:

if (scope.$last) {
   $timeout(function() { 
      scope.$eval(attr.onFinishRender);
   });
}

Prefiero $ eval en lugar de un evento. Con un evento, necesitamos saber el nombre del evento y agregar código a nuestro controlador para ese evento. Con $ eval, hay menos acoplamiento entre el controlador y la directiva.

Mark Rajcok
fuente
¿Qué hace el cheque $last? No puedo encontrarlo en los documentos. ¿Fue eliminado?
Erik Aigner
2
@ErikAigner, $lastse define en el ámbito si un ng-repeatestá activo en el elemento.
Mark Rajcok
Parece que tu Fiddle está tomando una innecesaria $timeout.
bbodenmiller
1
Excelente. Esta debería ser la respuesta aceptada, ya que emitir / transmitir cuesta más desde el punto de vista del rendimiento.
Florin Vistig
29

Las respuestas que se han dado hasta ahora solo funcionarán la primera vez que ng-repeatse procese , pero si tiene una dinámica ng-repeat, lo que significa que va a agregar / eliminar / filtrar elementos, y debe ser notificado cada vez que ng-repeatse procesa, esas soluciones no funcionarán para usted.

Entonces, si necesita que le notifiquen CADA VEZ que ng-repeatse vuelve a reproducir y no solo la primera vez, he encontrado una manera de hacerlo, es bastante 'hacky', pero funcionará bien si sabe lo que es haciendo. Use esto $filteren su ng-repeat antes de usar cualquier otro$filter :

.filter('ngRepeatFinish', function($timeout){
    return function(data){
        var me = this;
        var flagProperty = '__finishedRendering__';
        if(!data[flagProperty]){
            Object.defineProperty(
                data, 
                flagProperty, 
                {enumerable:false, configurable:true, writable: false, value:{}});
            $timeout(function(){
                    delete data[flagProperty];                        
                    me.$emit('ngRepeatFinished');
                },0,false);                
        }
        return data;
    };
})

Este será $emitun evento llamado ngRepeatFinishedcada vez que ng-repeatse procesa.

Cómo usarlo:

<li ng-repeat="item in (items|ngRepeatFinish) | filter:{name:namedFiltered}" >

El ngRepeatFinishfiltro debe aplicarse directamente a uno Arrayo un Objectdefinido en su $scope, puede aplicar otros filtros después.

Cómo NO usarlo:

<li ng-repeat="item in (items | filter:{name:namedFiltered}) | ngRepeatFinish" >

No aplique otros filtros primero y luego aplique el ngRepeatFinishfiltro.

¿Cuándo debo usar esto?

Si desea aplicar ciertos estilos CSS en el DOM después de que la lista haya terminado de renderizarse, porque debe tener en cuenta las nuevas dimensiones de los elementos DOM que han sido reproducidos por el ng-repeat. (Por cierto: ese tipo de operaciones deben realizarse dentro de una directiva)

Qué NO HACER en la función que maneja el ngRepeatFinishedevento:

  • No realice una $scope.$applyfunción en esa función o colocará Angular en un bucle sin fin que Angular no podrá detectar.

  • No lo use para hacer cambios en las $scopepropiedades, porque esos cambios no se reflejarán en su vista hasta el próximo $digestciclo, y dado que no puede realizar una $scope.$apply, no serán de ninguna utilidad.

"¡¡Pero los filtros no están destinados a usarse así !!"

No, no lo son, esto es un truco, si no te gusta, no lo uses. Si conoce una mejor manera de lograr lo mismo, hágamelo saber.

Resumiendo

Este es un truco , y usarlo de manera incorrecta es peligroso, úselo solo para aplicar estilos después de que ng-repeathaya finalizado el renderizado y no debería tener ningún problema.

Josep
fuente
Thx 4 la punta. De todos modos, un plunkr con un ejemplo de trabajo sería muy apreciado.
holographix
2
Desafortunadamente, esto no funcionó para mí, al menos para el último AngularJS 1.3.7. Así que descubrí otra solución, no la mejor, pero si esto es esencial para ti, funcionará. Lo que hice es que cada vez que agrego / cambio / elimino elementos, siempre agrego otro elemento ficticio al final de la lista, por lo tanto, dado que el $lastelemento también se cambia, la ngRepeatFinishdirectiva simple al verificar $lastahora funcionará. Tan pronto como se llama ngRepeatFinish, elimino el elemento ficticio. (También lo oculto con CSS para que no aparezca brevemente)
darklow
Este truco es agradable pero no perfecto; cada vez que se
activa
El siguiente por Mark Rajcokfunciona perfectamente cada re-render de la ng-repeat (por ejemplo, debido al cambio de modelo) Entonces, esta solución hacky no es necesaria.
Dzhu
1
@Josep tienes razón: cuando los elementos se empalman / no se desplazan (es decir, la matriz se muta), no funciona. Mi prueba anterior probablemente fue con una matriz que se ha reasignado (usando sugarjs), por lo tanto, funcionó todo el tiempo ... Gracias por señalar esto
Dzhu
10

Si necesita llamar a diferentes funciones para diferentes repeticiones ng en el mismo controlador, puede intentar algo como esto:

La directiva:

var module = angular.module('testApp', [])
    .directive('onFinishRender', function ($timeout) {
    return {
        restrict: 'A',
        link: function (scope, element, attr) {
            if (scope.$last === true) {
            $timeout(function () {
                scope.$emit(attr.broadcasteventname ? attr.broadcasteventname : 'ngRepeatFinished');
            });
            }
        }
    }
});

En su controlador, capture eventos con $ on:

$scope.$on('ngRepeatBroadcast1', function(ngRepeatFinishedEvent) {
// Do something
});

$scope.$on('ngRepeatBroadcast2', function(ngRepeatFinishedEvent) {
// Do something
});

En su plantilla con múltiples ng-repeat

<div ng-repeat="item in collection1" on-finish-render broadcasteventname="ngRepeatBroadcast1">
    <div>{{item.name}}}<div>
</div>

<div ng-repeat="item in collection2" on-finish-render broadcasteventname="ngRepeatBroadcast2">
    <div>{{item.name}}}<div>
</div>
MCurbelo
fuente
5

Las otras soluciones funcionarán bien en la carga inicial de la página, pero llamar a $ timeout desde el controlador es la única forma de garantizar que se llame a su función cuando cambia el modelo. Aquí hay un violín de trabajo que usa $ timeout. Para su ejemplo sería:

.controller('myC', function ($scope, $timeout) {
$scope.$watch("ta", function (newValue, oldValue) {
    $timeout(function () {
       test();
    });
});

ngRepeat solo evaluará una directiva cuando el contenido de la fila sea nuevo, por lo que si elimina elementos de su lista, onFinishRender no se activará. Por ejemplo, intente ingresar valores de filtro en estos violines emitidos .

John Harley
fuente
Del mismo modo, test () no siempre se llama en la solución evalAsynch cuando el modelo cambia de violín
John Harley
2
lo que es taen $scope.$watch("ta",...?
Muhammad Adeel Zahid
4

Si no es reacio a usar accesorios de alcance de doble dólar y está escribiendo una directiva cuyo único contenido es una repetición, hay una solución bastante simple (suponiendo que solo le importe el renderizado inicial). En la función de enlace:

const dereg = scope.$watch('$$childTail.$last', last => {
    if (last) {
        dereg();
        // do yr stuff -- you may still need a $timeout here
    }
});

Esto es útil para los casos en los que tiene una directiva que necesita manipular DOM en función de los anchos o las alturas de los miembros de una lista representada (que creo que es la razón más probable por la que uno haría esta pregunta), pero no es tan genérica como las otras soluciones que se han propuesto.

Punto y coma
fuente
1

Una solución para este problema con un ngRepeat filtrado podría haber sido con eventos de mutación , pero están en desuso (sin reemplazo inmediato).

Entonces pensé en otra fácil:

app.directive('filtered',function($timeout) {
    return {
        restrict: 'A',link: function (scope,element,attr) {
            var elm = element[0]
                ,nodePrototype = Node.prototype
                ,timeout
                ,slice = Array.prototype.slice
            ;

            elm.insertBefore = alt.bind(null,nodePrototype.insertBefore);
            elm.removeChild = alt.bind(null,nodePrototype.removeChild);

            function alt(fn){
                fn.apply(elm,slice.call(arguments,1));
                timeout&&$timeout.cancel(timeout);
                timeout = $timeout(altDone);
            }

            function altDone(){
                timeout = null;
                console.log('Filtered! ...fire an event or something');
            }
        }
    };
});

Esto se engancha a los métodos Node.prototype del elemento padre con un tiempo de espera $ de una marca para observar modificaciones sucesivas.

Funciona principalmente correcto, pero obtuve algunos casos en los que se llamaría altDone dos veces.

Nuevamente ... agregue esta directiva al padre de ngRepeat.

Sjeiti
fuente
Esto funciona mejor hasta ahora, pero no es perfecto. Por ejemplo, voy de 70 artículos a 68, pero no dispara.
Gaui
1
Lo que podría funcionar es agregar appendChildtambién (ya que solo usé insertBeforey removeChild) en el código anterior.
Sjeiti
1

Muy fácil, así es como lo hice.

.directive('blockOnRender', function ($blockUI) {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            
            if (scope.$first) {
                $blockUI.blockElement($(element).parent());
            }
            if (scope.$last) {
                $blockUI.unblockElement($(element).parent());
            }
        }
    };
})

CPhelefu
fuente
Este código funciona, por supuesto, si tiene su propio servicio $ blockUI.
CPhelefu
0

Por favor, eche un vistazo al violín, http://jsfiddle.net/yNXS2/ . Como la directiva que creó no creó un nuevo ámbito, continué en el camino.

$scope.test = function(){... Hizo que eso sucediera.

Rajkamal Subramanian
fuente
Violín equivocado? Igual que el de la pregunta.
James M
humm, ¿actualizaste el violín? Actualmente muestra una copia de mi código original.
PCoelho
0

Estoy muy sorprendido de no ver la solución más simple entre las respuestas a esta pregunta. Lo que desea hacer es agregar una ngInitdirectiva en su elemento repetido (el elemento con la ngRepeatdirectiva) buscando $last(una variable especial establecida en el alcance por la ngRepeatcual indica que el elemento repetido es el último en la lista). Si $lastes cierto, representamos el último elemento y podemos llamar a la función que queramos.

ng-init="$last && test()"

El código completo para su marcado HTML sería:

<div ng-app="testApp" ng-controller="myC">
    <p ng-repeat="t in ta" ng-init="$last && test()">{{t}}</p>
</div>

No necesita ningún código JS adicional en su aplicación además de la función de alcance que desea llamar (en este caso test) , ya ngInitque Angular.js lo proporciona. Solo asegúrese de tener su testfunción en el alcance para que se pueda acceder desde la plantilla:

$scope.test = function test() {
    console.log("test executed");
}
Alejandro García Iglesias
fuente