Aufruf einer Funktion, wenn ng-repeat beendet ist

249

Was ich zu implementieren versuche, ist im Grunde ein Handler, der das Rendern beendet. Ich kann erkennen, wann es fertig ist, aber ich kann nicht herausfinden, wie ich eine Funktion daraus auslösen kann.

Überprüfen Sie die Geige: 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>

Antwort : Working Fiddle von FinishingMove: http://jsfiddle.net/paulocoelho/BsMqq/4/

PCoelho
quelle
Was ist aus Neugier der Zweck des element.ready()Snippets? Ich meine ... ist es eine Art jQuery-Plugin, das Sie haben, oder sollte es ausgelöst werden, wenn das Element bereit ist?
gion_13
1
Man könnte es mit eingebauten Anweisungen wie ng-init
semiomant
Mögliches Duplikat des Ng-Repeat-Finish-Events
Damjan Pavlica
Duplikat von stackoverflow.com/questions/13471129/… , siehe meine Antwort dort
bresleveloper

Antworten:

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

Beachten Sie, dass ich es nicht verwendet .ready(), sondern in a eingewickelt habe $timeout. $timeoutstellt sicher, dass es ausgeführt wird, wenn die ng-wiederholten Elemente das Rendern WIRKLICH beendet haben (da das $timeoutam Ende des aktuellen Digest-Zyklus ausgeführt wird - und im $applyGegensatz dazu auch intern aufgerufen wirdsetTimeout ). Nachdem das ng-repeatbeendet ist, $emitsenden wir ein Ereignis an äußere Bereiche (Geschwister- und übergeordnete Bereiche).

Und dann können Sie es in Ihrem Controller abfangen mit $on:

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

Mit HTML sieht das ungefähr so ​​aus:

<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
    <div>{{item.name}}}<div>
</div>
holographisches Prinzip
quelle
10
+1, aber ich würde $ eval verwenden, bevor ich eine ereignislose Kopplung verwende. Siehe meine Antwort für weitere Details.
Mark Rajcok
1
@PigalevPavel Ich denke, Sie verwechseln $timeout(was im Grunde setTimeout+ Angulars ist $scope.$apply()) mit setInterval. $timeoutwird nur einmal pro ifBedingung ausgeführt, und das wird am Anfang des nächsten $digestZyklus sein. Weitere Informationen zu JavaScript-Zeitüberschreitungen finden Sie unter: ejohn.org/blog/how-javascript-timers-work
holographisches Prinzip
1
@finishingmove Vielleicht verstehe ich etwas nicht :) Aber siehe, ich habe ng-repeat in einem anderen ng-repeat. Und ich möchte sicher wissen, wann alle fertig sind. Wenn ich Ihr Skript mit $ timeout nur für übergeordnetes ng-repeat verwende, funktioniert alles einwandfrei. Wenn ich jedoch kein $ timeout verwende, erhalte ich eine Antwort, bevor die ng-Wiederholungen für Kinder beendet sind. Ich möchte wissen warum? Und kann ich sicher sein, dass ich immer eine Antwort bekomme, wenn ich Ihr Skript mit $ timeout für die übergeordnete ng-Wiederholung verwende, wenn alle ng-Wiederholungen abgeschlossen sind?
Pigalev Pavel
1
Warum Sie so etwas wie setTimeout()mit 0 Verzögerung verwenden würden, ist eine andere Frage, obwohl ich sagen muss, dass ich nirgendwo eine tatsächliche Spezifikation für die Ereigniswarteschlange des Browsers gefunden habe, nur was durch Single-Threading und die Existenz von impliziert wird setTimeout().
Rakslice
1
Danke für diese Antwort. Ich habe eine Frage, warum müssen wir zuweisen on-finish-render="ngRepeatFinished"? Wenn ich zuweise on-finish-render="helloworld", funktioniert es genauso.
Arnaud
89

Verwenden Sie $ evalAsync, wenn Ihr Rückruf (dh test ()) ausgeführt werden soll, nachdem das DOM erstellt wurde, aber bevor der Browser gerendert wird. Dies verhindert ein Flimmern - ref .

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

Geige .

Wenn Sie Ihren Rückruf nach dem Rendern wirklich aufrufen möchten, verwenden Sie $ timeout:

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

Ich bevorzuge $ eval anstelle eines Ereignisses. Bei einem Ereignis müssen wir den Namen des Ereignisses kennen und unserem Controller Code für dieses Ereignis hinzufügen. Mit $ eval besteht weniger Kopplung zwischen dem Controller und der Direktive.

Mark Rajcok
quelle
Wofür macht der Scheck $last? Kann es nicht in den Dokumenten finden. Wurde es entfernt?
Erik Aigner
2
@ErikAigner, $lastwird im Bereich definiert, wenn ein ng-repeatim Element aktiv ist.
Mark Rajcok
Sieht so aus, als würde deine Geige ein unnötiges aufnehmen $timeout.
bbodenmiller
1
Toll. Dies sollte die akzeptierte Antwort sein, da das Senden / Senden unter Leistungsgesichtspunkten mehr kostet.
Florin Vistig
29

Die bisher gegebenen Antworten funktionieren nur beim ersten ng-repeatRendern . Wenn Sie jedoch eine Dynamik haben ng-repeat, bedeutet dies, dass Sie Elemente hinzufügen / löschen / filtern, und Sie müssen jedes Mal benachrichtigt werden, wenn die ng-repeatWird gerendert, funktionieren diese Lösungen nicht für Sie.

Also, wenn Sie JEDES MAL benachrichtigt werden müssen , dass die ng-repeatbekommt erneut gerendert und nicht nur das erste Mal habe ich einen Weg gefunden , das zu tun, es ist ziemlich ‚Hacky‘, aber es wird gut funktionieren , wenn Sie wissen , was Sie sind tun. Verwenden Sie dies $filterin Ihrem, ng-repeat bevor Sie andere verwenden$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;
    };
})

Dies ist $emitein Ereignis, das ngRepeatFinishedjedes Mal aufgerufen wird, wenn das ng-repeatgerendert wird.

Wie man es benutzt:

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

Der ngRepeatFinishFilter muss direkt auf einen Arrayoder einen Objectin Ihrem definierten Filter angewendet werden. Anschließend $scopekönnen Sie andere Filter anwenden.

Wie man es NICHT benutzt:

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

Wenden Sie nicht zuerst andere Filter an und wenden Sie dann den ngRepeatFinishFilter an.

Wann soll ich das benutzen?

Wenn Sie nach Abschluss des Renderns der Liste bestimmte CSS-Stile auf das DOM anwenden möchten, müssen Sie die neuen Dimensionen der DOM-Elemente berücksichtigen, die vom erneut gerendert wurden ng-repeat. (Übrigens: Diese Art von Operationen sollten innerhalb einer Richtlinie durchgeführt werden.)

Was Sie in der Funktion, die das ngRepeatFinishedEreignis behandelt, NICHT tun sollten :

  • Führen Sie $scope.$applyin dieser Funktion keine aus, da sonst Angular in eine Endlosschleife versetzt wird, die Angular nicht erkennen kann.

  • Verwenden Sie es nicht, um Änderungen an den $scopeEigenschaften vorzunehmen , da diese Änderungen erst in der nächsten $digestSchleife in Ihrer Ansicht wiedergegeben werden und da Sie keine Änderungen vornehmen können, sind $scope.$applysie nicht von Nutzen.

"Aber Filter sollen nicht so verwendet werden !!"

Nein, das sind sie nicht. Dies ist ein Hack. Wenn Sie ihn nicht mögen, verwenden Sie ihn nicht. Wenn Sie einen besseren Weg kennen, um dasselbe zu erreichen, lassen Sie es mich bitte wissen.

Zusammenfassen

Dies ist ein Hack , und die falsche Verwendung ist gefährlich. Verwenden Sie ihn nur zum Anwenden von Stilen, nachdem das ng-repeatRendern abgeschlossen ist und Sie keine Probleme haben sollten.

Josep
quelle
Thx 4 die Spitze. Auf jeden Fall wäre ein Plunkr mit einem funktionierenden Beispiel sehr dankbar.
Holographix
2
Leider hat dies bei mir nicht funktioniert, zumindest bei AngularJS 1.3.7. Also habe ich eine andere Problemumgehung entdeckt, nicht die schönste, aber wenn dies für Sie wichtig ist, wird es funktionieren. Was ich getan habe, ist, wann immer ich Elemente hinzufüge / ändere / lösche, füge ich immer ein anderes Dummy-Element am Ende der Liste hinzu. Da also auch das $lastElement geändert wird, funktioniert eine einfache ngRepeatFinishAnweisung, die $lastjetzt überprüft wird. Sobald ngRepeatFinish aufgerufen wird, entferne ich Dummy-Elemente. (Ich verstecke es auch mit CSS, damit es nicht kurz erscheint)
darklow
Dieser Hack ist nett, aber nicht perfekt; Jedes Mal, wenn der Filter in der Veranstaltung tritt, wird er fünfmal versandt: - /
Sjeiti
Das folgende Beispiel Mark Rajcokfunktioniert perfekt bei jedem erneuten Rendern der ng-Wiederholung (z. B. aufgrund eines Modellwechsels). Diese hackige Lösung ist also nicht erforderlich.
Dzhu
1
@Josep Sie haben Recht - wenn Elemente gespleißt / nicht verschoben werden (dh das Array wird mutiert), funktioniert es nicht. Meine vorherigen Tests waren wahrscheinlich mit Arrays, die neu zugewiesen wurden (unter Verwendung von Sugarjs), daher funktionierte es jedes Mal ... Vielen Dank für den Hinweis
Dzhu
10

Wenn Sie verschiedene Funktionen für verschiedene ng-Wiederholungen auf demselben Controller aufrufen müssen, können Sie Folgendes versuchen:

Die Richtlinie:

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

Fangen Sie in Ihrem Controller Ereignisse mit $ on ab:

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

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

In Ihrer Vorlage mit mehreren 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
quelle
5

Die anderen Lösungen funktionieren beim ersten Laden der Seite einwandfrei. Der Aufruf von $ timeout vom Controller aus ist jedoch die einzige Möglichkeit, um sicherzustellen, dass Ihre Funktion aufgerufen wird, wenn sich das Modell ändert. Hier ist eine funktionierende Geige , die $ timeout verwendet. Für Ihr Beispiel wäre es:

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

ngRepeat wertet eine Direktive nur aus, wenn der Zeileninhalt neu ist. Wenn Sie also Elemente aus Ihrer Liste entfernen, wird onFinishRender nicht ausgelöst. Versuchen Sie beispielsweise, Filterwerte in diese ausgegebenen Geigen einzugeben .

John Harley
quelle
In ähnlicher Weise wird test () in der evalAsynch-Lösung nicht immer aufgerufen, wenn das Modell die Geige wechselt
John Harley
2
Was ist tain $scope.$watch("ta",...?
Muhammad Adeel Zahid
4

Wenn Sie nicht abgeneigt sind, Requisiten im Doppel-Dollar-Bereich zu verwenden, und eine Direktive schreiben, deren einziger Inhalt eine Wiederholung ist, gibt es eine ziemlich einfache Lösung (vorausgesetzt, Sie kümmern sich nur um das anfängliche Rendern). In der Link-Funktion:

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

Dies ist nützlich für Fälle, in denen Sie eine Direktive haben, die DOM-Manipulationen basierend auf den Breiten oder Höhen der Mitglieder einer gerenderten Liste durchführen muss (was meiner Meinung nach der wahrscheinlichste Grund ist, warum man diese Frage stellen würde), aber nicht so allgemein als die anderen vorgeschlagenen Lösungen.

Semikolon
quelle
1

Eine Lösung für dieses Problem mit einem gefilterten ngRepeat könnten Mutationsereignisse gewesen sein , die jedoch veraltet sind (ohne sofortigen Ersatz).

Dann dachte ich an eine andere einfache:

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

Dadurch werden die Node.prototype-Methoden des übergeordneten Elements mit einem Timeout von einem Tick $ verknüpft, um nach aufeinanderfolgenden Änderungen zu suchen.

Es funktioniert meistens richtig, aber ich habe einige Fälle bekommen, in denen das altDone zweimal aufgerufen wurde.

Nochmals ... fügen Sie diese Anweisung dem übergeordneten Element von ngRepeat hinzu.

Sjeiti
quelle
Dies funktioniert bisher am besten, ist aber nicht perfekt. Zum Beispiel gehe ich von 70 auf 68 Gegenstände, aber es wird nicht ausgelöst.
Gaui
1
Was funktionieren könnte, ist auch das Hinzufügen appendChild(da ich nur insertBeforeund verwendet habe removeChild) im obigen Code.
Sjeiti
1

Sehr einfach, so habe ich es gemacht.

.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
quelle
Dieser Code funktioniert natürlich, wenn Sie einen eigenen $ blockUI-Dienst haben.
CPhelefu
0

Bitte werfen Sie einen Blick auf die Geige http://jsfiddle.net/yNXS2/ . Da die von Ihnen erstellte Direktive keinen neuen Bereich erstellt hat, habe ich den Weg fortgesetzt.

$scope.test = function(){... hat das möglich gemacht.

Rajkamal Subramanian
quelle
Falsche Geige? Gleich wie der in der Frage.
James M
Humm, hast du die Geige aktualisiert? Derzeit wird eine Kopie meines Originalcodes angezeigt.
PCoelho
0

Ich bin sehr überrascht, nicht die einfachste Lösung unter den Antworten auf diese Frage zu sehen. Sie möchten ngInitIhrem wiederholten Element (dem Element mit der ngRepeatDirektive) eine Direktive hinzufügen, nach der gesucht wird $last(eine spezielle Variable, deren Gültigkeitsbereich ngRepeatangibt, dass das wiederholte Element das letzte in der Liste ist). Wenn dies $lastzutrifft, rendern wir das letzte Element und können die gewünschte Funktion aufrufen.

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

Der vollständige Code für Ihr HTML-Markup lautet:

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

Sie benötigen keinen zusätzlichen JS-Code in Ihrer App außer der Bereichsfunktion, die Sie aufrufen möchten (in diesem Fall test), da ngInitdiese von Angular.js bereitgestellt wird. Stellen Sie einfach sicher, dass Ihre testFunktion im Bereich enthalten ist, damit Sie über die Vorlage darauf zugreifen können:

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