Richiamo di una funzione al termine di ng-repeat


249

Quello che sto cercando di implementare è fondamentalmente un gestore "on ng repeat repeat rendering". Sono in grado di rilevare quando è fatto, ma non riesco a capire come attivare una funzione da esso.

Controlla il violino: 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>

Risposta : violino di lavoro da finishingmove: http://jsfiddle.net/paulocoelho/BsMqq/4/


Per curiosità, qual è lo scopo dello element.ready()snippet? Voglio dire .. è una sorta di plugin jQuery che hai, o dovrebbe essere attivato quando l'elemento è pronto?
gion_13,

1
Si potrebbe fare usando direttive integrate come ng-init
semiomante


duplicato di stackoverflow.com/questions/13471129/… , vedi la mia risposta lì
bresleveloper

Risposte:


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

Si noti che non ho usato .ready()ma piuttosto avvolto in a $timeout. $timeoutsi assicura che venga eseguito quando gli elementi ng-ripetuti hanno REALMENTE terminato il rendering (perché $timeoutverrà eseguito alla fine del ciclo di digest corrente - e chiamerà anche $applyinternamente, a differenzasetTimeout ). Quindi, una volta ng-repeatterminato, usiamo $emitper emettere un evento su ambiti esterni (ambiti fratelli e genitori).

E poi nel tuo controller, puoi prenderlo con $on:

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

Con HTML che assomiglia a questo:

<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
    <div>{{item.name}}}<div>
</div>

10
+1, ma userei $ eval prima di usare un evento - meno accoppiamento. Vedi la mia risposta per maggiori dettagli.
Mark Rajcok,

1
@PigalevPavel Penso che tu sia confuso $timeout(che è fondamentalmente setTimeout+ di Angular $scope.$apply()) con setInterval. $timeoutverrà eseguito solo una volta per ifcondizione, e questo sarà all'inizio del $digestciclo successivo . Per maggiori informazioni sui timeout JavaScript, consultare: ejohn.org/blog/how-javascript-timers-work
olografico-principio

1
@finishingmove Forse non capisco qualcosa :) Ma vedi che ho ng-repeat in un'altra ng-repeat. E voglio sapere con certezza quando saranno finiti tutti. Quando uso il tuo script con $ timeout solo per il genitore-ripeti tutto funziona perfettamente. Ma se non uso $ timeout, ricevo una risposta prima che i bambini abbiano finito. Voglio sapere perché? E posso essere sicuro che se uso il tuo script con $ timeout per il ng-repeat del genitore otterrò sempre una risposta quando tutte le ng-ripetizioni saranno terminate?
Pigalev Pavel,

1
Perché useresti qualcosa del tipo setTimeout()con ritardo 0 è un'altra domanda, anche se devo dire che non ho mai incontrato una specifica effettiva per la coda degli eventi del browser da nessuna parte, solo ciò che è implicito dal thread singolo e dall'esistenza di setTimeout().
rakslice,

1
Grazie per questa risposta Ho una domanda, perché dobbiamo assegnare on-finish-render="ngRepeatFinished"? Quando lo assegno on-finish-render="helloworld", funziona allo stesso modo.
Arnaud,

89

Utilizzare $ evalAsync se si desidera che il callback (ovvero test ()) venga eseguito dopo la creazione del DOM, ma prima del rendering del browser. Ciò eviterà lo sfarfallio - rif .

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

Violino .

Se vuoi davvero chiamare il tuo callback dopo il rendering, usa $ timeout:

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

Preferisco $ eval invece di un evento. Con un evento, dobbiamo conoscere il nome dell'evento e aggiungere il codice al nostro controller per quell'evento. Con $ eval, c'è meno accoppiamento tra il controller e la direttiva.


A cosa serve il controllo $last? Non riesco a trovarlo nei documenti. È stato rimosso?
Erik Aigner,

2
@ErikAigner, $lastè definito nell'ambito se un ng-repeatelemento è attivo sull'elemento.
Mark Rajcok,

Sembra che il tuo violino stia assumendo un inutile $timeout.
bbodenmiller,

1
Grande. Questa dovrebbe essere la risposta accettata, visto che l'emissione / trasmissione costa di più dal punto di vista delle prestazioni.
Florin Vistig,

29

Le risposte che sono state fornite finora funzioneranno solo la prima volta che ng-repeatvengono rese , ma se hai una dinamica ng-repeat, il che significa che stai per aggiungere / eliminare / filtrare elementi e devi essere avvisato ogni volta che il ng-repeatviene reso, quelle soluzioni non funzioneranno per te.

Quindi, se hai bisogno di essere avvisato OGNI VOLTA che il ng-repeatrendering viene ridimensionato e non solo la prima volta, ho trovato un modo per farlo, è abbastanza "confuso", ma funzionerà bene se sai cosa sei facendo. Usa questo $filternel tuo ng-repeat prima di usare qualsiasi altro$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;
    };
})

Questo sarà $emitun evento chiamato ngRepeatFinishedogni volta che ng-repeatviene reso.

Come usarlo:

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

Il ngRepeatFinishfiltro deve essere applicato direttamente a un Arrayo un Objectdefinito nel tuo$scope , puoi applicare altri filtri dopo.

Come NON usarlo:

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

Non applicare prima altri filtri e quindi applicare il ngRepeatFinishfiltro.

Quando dovrei usare questo?

Se si desidera applicare determinati stili CSS nel DOM dopo che l'elenco ha terminato il rendering, poiché è necessario tenere conto delle nuove dimensioni degli elementi DOM che sono stati renderizzati nuovamente da ng-repeat. (A proposito: quel tipo di operazioni dovrebbe essere fatto all'interno di una direttiva)

Cosa NON FARE nella funzione che gestisce l' ngRepeatFinishedevento:

  • Non eseguire a $scope.$applyin quella funzione o inserirai Angular in un loop infinito che Angular non sarà in grado di rilevare.

  • Non utilizzarlo per apportare modifiche alle $scopeproprietà, poiché tali modifiche non si rifletteranno nella vista fino al $digestciclo successivo e poiché non è possibile eseguirle $scope.$apply, non saranno di alcuna utilità.

"Ma i filtri non sono fatti per essere usati così !!"

No, non lo sono, questo è un trucco, se non ti piace non usarlo. Se conosci un modo migliore per realizzare la stessa cosa, per favore fatemelo sapere.

Riassumendo

Questo è un trucco e usarlo nel modo sbagliato è pericoloso, usalo solo per applicare gli stili dopo che il ng-repeatrendering è terminato e non dovresti avere problemi.


Grazie 4 la mancia. Ad ogni modo, un plunkr con un esempio funzionante sarebbe molto apprezzato.
holographix,

2
Purtroppo questo non ha funzionato per me, almeno per l'ultimo AngularJS 1.3.7. Quindi ho scoperto un'altra soluzione alternativa, non la più bella, ma se questo è essenziale per te funzionerà. Quello che ho fatto è ogni volta che aggiungo / cambio / elimino elementi aggiungo sempre un altro elemento fittizio alla fine dell'elenco, quindi poiché anche l' $lastelemento è cambiato, una semplice ngRepeatFinishdirettiva verificando $lastora funzionerà. Non appena viene chiamato ngRepeatFinish rimuovo l'elemento fittizio. (Lo nascondo anche con CSS in modo che non appaia brevemente)
darklow

Questo hack è carino ma non perfetto; ogni volta che il filtro calcia nell'evento viene inviato cinque volte: - /
Sjeiti

Quello qui sotto Mark Rajcokfunziona perfettamente ogni nuovo rendering di ng-repeat (ad esempio a causa del cambio di modello). Quindi questa soluzione hacky non è richiesta.
Dzhu,

1
@Josep hai ragione: quando gli elementi sono giunti / non spostati (cioè l'array viene mutato), non funziona. I miei precedenti test probabilmente erano con array che sono stati riassegnati (usando sugarjs) quindi ha funzionato ogni volta ... Grazie per
averlo

10

Se devi chiamare funzioni diverse per diverse ripetizioni di ng sullo stesso controller, puoi provare qualcosa del genere:

La direttiva:

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

Nel controller, cattura gli eventi con $ on:

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

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

Nel modello con più 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>

5

Le altre soluzioni funzioneranno correttamente al caricamento della pagina iniziale, ma chiamare $ timeout dal controller è l'unico modo per garantire che la funzione venga chiamata quando il modello cambia. Ecco un violino funzionante che utilizza $ timeout. Per il tuo esempio sarebbe:

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

ngRepeat valuterà una direttiva solo quando il contenuto della riga è nuovo, quindi se rimuovi elementi dal tuo elenco, onFinishRender non verrà attivato. Ad esempio, prova a inserire i valori del filtro in questi emit di violini .


Allo stesso modo, test () non viene sempre chiamato nella soluzione evalAsynch quando il modello cambia violino
John Harley,

2
cosa c'è tadentro $scope.$watch("ta",...?
Muhammad Adeel Zahid,

4

Se non sei contrario all'utilizzo di oggetti di scena a doppio dollaro e stai scrivendo una direttiva il cui unico contenuto è una ripetizione, esiste una soluzione piuttosto semplice (supponendo che ti interessi solo al rendering iniziale). Nella funzione di collegamento:

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

Questo è utile per i casi in cui hai una direttiva che deve eseguire la manipolazione DOM in base alla larghezza o all'altezza dei membri di un elenco renderizzato (che ritengo sia il motivo più probabile per cui potresti porre questa domanda), ma non è così generico come le altre soluzioni che sono state proposte.


1

Una soluzione a questo problema con un ngRepeat filtrato avrebbe potuto essere con Mutazione eventi , ma sono obsoleti (senza sostituzione immediata).

Poi ho pensato a un altro semplice:

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

Questo si aggancia ai metodi Node.prototype dell'elemento genitore con un timeout $ di un segno di spunta per controllare le successive modifiche.

Funziona principalmente correttamente, ma ho avuto alcuni casi in cui altDone sarebbe stato chiamato due volte.

Ancora una volta ... aggiungi questa direttiva al genitore di ngRepeat.


Finora funziona al meglio, ma non è perfetto. Ad esempio vado da 70 articoli a 68, ma non si accende.
Gaui,

1
Ciò che potrebbe funzionare è anche l'aggiunta appendChild(poiché ho usato solo insertBeforee removeChild) nel codice sopra.
Sjeiti,

1

Molto facile, è così che l'ho fatto.

.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());
            }
        }
    };
})


Questo codice funziona ovviamente se hai il tuo servizio $ blockUI.
CPhelefu,

0

Dai un'occhiata al violino, http://jsfiddle.net/yNXS2/ . Dal momento che la direttiva che hai creato non ha creato un nuovo ambito, ho continuato.

$scope.test = function(){... fatto accadere.


Violino sbagliato? Come quello nella domanda.
James M

humm, hai aggiornato il violino? Attualmente mostra una copia del mio codice originale.
PCoelho

0

Sono molto sorpreso di non vedere la soluzione più semplice tra le risposte a questa domanda. Quello che vuoi fare è aggiungere una ngInitdirettiva sul tuo elemento ripetuto (l'elemento con la ngRepeatdirettiva) controllando $last(una variabile speciale impostata nell'ambito ngRepeatche indica che l'elemento ripetuto è l'ultimo nell'elenco). Se $lastè vero, stiamo eseguendo il rendering dell'ultimo elemento e possiamo chiamare la funzione che vogliamo.

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

Il codice completo per il markup HTML sarebbe:

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

Non è necessario alcun codice JS aggiuntivo nella tua app oltre alla funzione ambito che desideri chiamare (in questo caso, test) poiché ngInitfornita da Angular.js. Assicurati solo di avere la tua testfunzione nell'ambito in modo che sia possibile accedervi dal modello:

$scope.test = function test() {
    console.log("test executed");
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.