Wywoływanie funkcji po zakończeniu ng-repeat


249

To, co próbuję wdrożyć, to w zasadzie moduł obsługi „po powtórzeniu renderowania zakończonego”. Jestem w stanie wykryć, kiedy to się skończy, ale nie mogę wymyślić, jak wywołać z niej funkcję.

Sprawdź skrzypce: 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>

Odpowiedź : Działające skrzypce od finishmove: http://jsfiddle.net/paulocoelho/BsMqq/4/


Z ciekawości, jaki jest cel tego element.ready()fragmentu? Mam na myśli ... czy jest to jakaś wtyczka jQuery, czy masz ją uruchomić, gdy element będzie gotowy?
gion_13

1
Można to zrobić za pomocą wbudowanych dyrektyw, takich jak ng-init
semiomant


duplikat stackoverflow.com/questions/13471129/… , zobacz moją odpowiedź tam
bresleveloper

Odpowiedzi:


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

Zauważ, że nie użyłem, .ready()ale raczej zapakowałem w $timeout. $timeoutupewnia się, że jest wykonywany, gdy elementy powtarzające się ng NAPRAWDĘ $timeoutzakończą renderowanie (ponieważ wykona się pod koniec bieżącego cyklu podsumowania - i zadzwoni również $applywewnętrznie, w przeciwieństwie dosetTimeout ). Więc po ng-repeatzakończeniu używamy $emitdo wysyłania zdarzenia do zewnętrznych zakresów (zakresów rodzeństwa i rodzica).

A potem w kontrolerze możesz to złapać za pomocą $on:

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

Z HTML, który wygląda mniej więcej tak:

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

10
+1, ale użyłbym $ eval przed użyciem sprzężenia bez zdarzenia. Zobacz moją odpowiedź, aby uzyskać więcej informacji.
Mark Rajcok

1
@PigalevPavel Myślę, że mylisz się $timeout(co w zasadzie setTimeout+ Angulara $scope.$apply()) z setInterval. $timeoutwykona się tylko raz dla danego ifwarunku i będzie to na początku następnego $digestcyklu. Aby uzyskać więcej informacji na temat limitów czasu JavaScript, patrz: ejohn.org/blog/how-javascript-timers-work
zasada holograficzna

1
@finishingmove Może czegoś nie rozumiem :) Ale zobacz, mam powtórzenie ng w innym powtórzeniu ng. I chcę wiedzieć na pewno, kiedy wszystkie z nich skończą. Kiedy używam twojego skryptu z $ timeout tylko dla rodzica-powtórz wszystko działa dobrze. Ale jeśli nie użyję $ limitu czasu, otrzymam odpowiedź, zanim dzieci powtórzą ng. Chcę wiedzieć dlaczego? I czy mogę być pewien, że jeśli użyję twojego skryptu z $ timeout dla rodzica powtórzenia ng, zawsze otrzymam odpowiedź, gdy wszystkie powtórzenia ng zostaną zakończone?
Pigalev Pavel

1
Dlaczego użyjesz czegoś setTimeout()z opóźnieniem 0, to kolejne pytanie, ale muszę powiedzieć, że nigdy nie spotkałem się z rzeczywistą specyfikacją kolejki zdarzeń przeglądarki, tylko co sugeruje jednowątkowość i istnienie setTimeout().
rakslice,

1
Dziękuję za tę odpowiedź. Mam pytanie, dlaczego musimy to przypisać on-finish-render="ngRepeatFinished"? Kiedy przydzielam on-finish-render="helloworld", działa tak samo.
Arnaud

89

Użyj $ evalAsync, jeśli chcesz, aby Twoje wywołanie zwrotne (tj. Test ()) było wykonywane po zbudowaniu DOM, ale przed renderowaniem przeglądarki. Zapobiegnie to migotaniu - ref .

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

Fiddle .

Jeśli naprawdę chcesz oddzwonić po renderowaniu, użyj $ timeout:

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

Wolę $ eval zamiast zdarzenia. W przypadku zdarzenia musimy znać nazwę zdarzenia i dodać kod do naszego kontrolera dla tego zdarzenia. Dzięki $ eval istnieje mniej sprzężenia między sterownikiem a dyrektywą.


Do czego służy czek $last? Nie można tego znaleźć w dokumentacji. Czy został usunięty?
Erik Aigner,

2
@ErikAigner, $lastjest zdefiniowany w zakresie, jeśli element ng-repeatjest aktywny w elemencie.
Mark Rajcok

Wygląda na to, że twoje skrzypce przyjmują niepotrzebne $timeout.
bbodenmiller

1
Wspaniały. To powinna być zaakceptowana odpowiedź, ponieważ emisja / emisja kosztuje więcej z punktu widzenia wydajności.
Florin Vistig

29

Odpowiedzi, które zostały podane do tej pory, zadziałają tylko przy pierwszym ng-repeatrenderowaniu , ale jeśli masz dynamikę ng-repeat, co oznacza, że ​​będziesz dodawać / usuwać / filtrować elementy i za każdym razem musisz otrzymywać powiadomienia ng-repeatzostanie renderowany, te rozwiązania nie będą działać.

Tak więc, jeśli chcesz być powiadamiany za każdym razem, że ng-repeatwystąpią ponownie wydanego właśnie po raz pierwszy i nie znalazłem sposób na to, że to dość „hacky”, ale to będzie działać dobrze, jeśli wiesz, kim jesteś robić. Użyj tego $filterw swoim, ng-repeat zanim użyjesz innego$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;
    };
})

Będzie $emitto wydarzenie wywoływane za ngRepeatFinishedkażdym razem, gdy ng-repeatzostanie zrenderowany.

Jak tego użyć:

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

ngRepeatFinishPotrzebuje filtra należy stosować bezpośrednio na Arraylub Objectzdefiniowanych w $scope, można zastosować inne filtry po.

Jak NIE używać:

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

Nie stosuj najpierw innych filtrów, a następnie zastosuj ngRepeatFinishfiltr.

Kiedy powinienem tego użyć?

Jeśli chcesz zastosować pewne style css do DOM po zakończeniu renderowania listy, ponieważ musisz wziąć pod uwagę nowe wymiary elementów DOM, które zostały ponownie renderowane przez ng-repeat. (BTW: tego rodzaju operacje powinny być wykonywane w ramach dyrektywy)

Czego NIE ZROBIĆ w funkcji obsługującej ngRepeatFinishedzdarzenie:

  • Nie wykonuj $scope.$applyw tej funkcji, bo umieścisz Angulara w niekończącej się pętli, której Angular nie będzie w stanie wykryć.

  • Nie należy go używać do wprowadzania zmian we $scopewłaściwościach, ponieważ zmiany te nie zostaną odzwierciedlone w widoku do następnej $digestpętli, a ponieważ nie można ich wykonać, $scope.$applynie będą one przydatne.

„Ale filtrów nie należy tak używać !!”

Nie, nie są, to jest hack, jeśli ci się nie podoba, nie używaj go. Jeśli znasz lepszy sposób na osiągnięcie tego samego, daj mi znać.

Zreasumowanie

To jest włamanie , a użycie go w niewłaściwy sposób jest niebezpieczne, używaj go tylko do stosowania stylów po ng-repeatzakończeniu renderowania i nie powinieneś mieć żadnych problemów.


Dzięki 4 końcówka. W każdym razie doceniony zostanie działacz z działającym przykładem.
holographix

2
Niestety to nie działało dla mnie, przynajmniej dla najnowszej wersji AngularJS 1.3.7. Odkryłem więc inne obejście, nie najładniejsze, ale jeśli jest to dla Ciebie niezbędne, zadziała. To, co zrobiłem, to dodawanie / zmiana / usuwanie elementów i zawsze dodawanie kolejnego elementu zastępczego na końcu listy, dlatego ponieważ $lastelement również się zmienia, prosta ngRepeatFinishdyrektywa poprzez sprawdzenie $lastteraz będzie działać. Jak tylko zadzwoni ngRepeatFinish, usuwam atrapę. (Ukryłem go również za pomocą CSS, więc nie pojawia się na krótko)
darklow,

Ten hack jest fajny, ale nie idealny; za każdym razem, gdy filtr uruchamia się w wydarzeniu jest wysyłany pięć razy: - /
Sjeiti

Poniższy z nich Mark Rajcokdziała idealnie przy każdym ponownym renderowaniu powtórzenia ng (np. Z powodu zmiany modelu). Więc to hacky rozwiązanie nie jest wymagane.
Dzhu

1
@Josep masz rację - kiedy elementy są splatane / nie przesuwane (tj. Mutowana tablica), to nie działa. Moje poprzednie testy prawdopodobnie dotyczyły tablic, które zostały ponownie przypisane (przy użyciu sugarjs), dlatego działały za każdym razem ... Dzięki za zwrócenie na to uwagi
Dzhu

10

Jeśli chcesz wywołać różne funkcje dla różnych powtórzeń ng na tym samym kontrolerze, możesz spróbować czegoś takiego:

Dyrektywa:

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

W swoim kontrolerze łap wydarzenia za pomocą $ on:

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

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

W szablonie z wieloma powtórzeniami ng

<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

Inne rozwiązania będą działały poprawnie przy początkowym ładowaniu strony, ale wywołanie $ timeout z kontrolera jest jedynym sposobem, aby upewnić się, że twoja funkcja jest wywoływana, gdy model się zmienia. Oto działające skrzypce, które wykorzystują limit czasu $. Na przykład będzie to:

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

ngRepeat oceni dyrektywę tylko wtedy, gdy zawartość wiersza jest nowa, więc jeśli usuniesz elementy z listy, onFinishRender nie uruchomi się. Na przykład spróbuj wprowadzić wartości filtrów w tych skrzypcach emitowanych .


Podobnie test () nie zawsze jest wywoływany w rozwiązaniu evalAsynch, gdy zmienia się model skrzypce
John Harley

2
co jest taw$scope.$watch("ta",... środku
Muhammad Adeel Zahid

4

Jeśli nie masz nic przeciwko używaniu rekwizytów za dwa dolary i piszesz dyrektywę, której jedyną treścią jest powtórzenie, istnieje całkiem proste rozwiązanie (zakładając, że zależy ci tylko na początkowym renderowaniu). W funkcji łącza:

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

Jest to przydatne w przypadkach, gdy masz dyrektywę, która musi wykonywać manipulacje DOM na podstawie szerokości lub wysokości elementów renderowanej listy (co moim zdaniem jest najbardziej prawdopodobnym powodem, dla którego można by zadać to pytanie), ale nie jest tak ogólne jak inne zaproponowane rozwiązania.


1

Rozwiązaniem tego problemu może być filtrowane ngRepeat zdarzenia mutacji , ale są one przestarzałe (bez natychmiastowej zamiany).

Potem pomyślałem o innym łatwym:

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

To łączy się z metodami Node.prototype elementu nadrzędnego z limitem czasu jednego kliknięcia $, aby obserwować kolejne modyfikacje.

Działa głównie poprawnie, ale dostałem kilka przypadków, w których altDone byłby wywoływany dwukrotnie.

Znowu ... dodaj tę dyrektywę do elementu nadrzędnego ngRepeat.


Działa to jak dotąd najlepiej, ale nie jest idealne. Na przykład przechodzę z 70 przedmiotów do 68, ale nie strzela.
Gaui

1
To, co może działać, to także dodawanie appendChild(ponieważ użyłem tylko insertBeforei removeChild) w powyższym kodzie.
Sjeiti

1

Bardzo łatwo, tak to zrobiłem.

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


Ten kod działa oczywiście, jeśli masz własną usługę $ blockUI.
CPhelefu

0

Proszę spojrzeć na skrzypce, http://jsfiddle.net/yNXS2/ . Ponieważ tworzona dyrektywa nie stworzyła nowego zakresu, kontynuowałem.

$scope.test = function(){... sprawiło, że tak się stało.


Złe skrzypce? Taki sam jak w pytaniu.
James M

humm, czy zaktualizowałeś skrzypce? Obecnie wyświetla kopię mojego oryginalnego kodu.
PCoelho,

0

Jestem bardzo zaskoczony, że nie znalazłem najprostszego rozwiązania wśród odpowiedzi na to pytanie. To, co chcesz zrobić, to dodać ngInitdyrektywę do powtarzanego elementu (elementu z ngRepeatdyrektywą) sprawdzając $last(specjalną zmienną ustawioną w zakresie, ngRepeatktóra wskazuje, że powtarzany element jest ostatnim na liście). Jeśli $lastto prawda, renderujemy ostatni element i możemy wywołać żądaną funkcję.

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

Pełny kod znaczników HTML to:

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

Nie potrzebujesz dodatkowego kodu JS w swojej aplikacji oprócz funkcji zasięgu, którą chcesz wywołać (w tym przypadku test), ponieważ ngInitjest ona dostarczana przez Angular.js. Po prostu upewnij się, że masz testfunkcję w zakresie, aby można było uzyskać do niej dostęp z szablonu:

$scope.test = function test() {
    console.log("test executed");
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.