Appel d'une fonction lorsque ng-repeat est terminé

249

Ce que j'essaye d'implémenter est fondamentalement un gestionnaire "on ng repeat done rendering". Je suis capable de détecter quand c'est fait mais je n'arrive pas à comprendre comment en déclencher une fonction.

Vérifiez le violon: 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>

Réponse : Travailler le violon à partir de finishmove: http://jsfiddle.net/paulocoelho/BsMqq/4/

PCoelho
la source
Par curiosité, quel est le but de l' element.ready()extrait? Je veux dire .. est-ce une sorte de plugin jQuery que vous avez, ou devrait-il être déclenché lorsque l'élément est prêt?
gion_13
1
On pourrait le faire en utilisant des directives intégrées comme ng-init
semiomant
Éventuel doublon de l' événement d'arrivée ng-repeat
Damjan Pavlica
doublon de stackoverflow.com/questions/13471129/… , voir ma réponse là
bresleveloper

Réponses:

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

Notez que je ne l'ai pas utilisé .ready()mais plutôt enveloppé dans un $timeout. $timeouts'assure qu'il est exécuté lorsque les éléments ng-répétés ont VRAIMENT fini le rendu (car le $timeouts'exécutera à la fin du cycle de résumé en cours - et il appellera également en $applyinterne, contrairement àsetTimeout ). Ainsi, une fois la ng-repeatfin de, nous utilisons $emitpour émettre un événement vers les étendues externes (étendues frère et parent).

Et puis dans votre contrôleur, vous pouvez l'attraper avec $on:

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

Avec html qui ressemble à ceci:

<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
    <div>{{item.name}}}<div>
</div>
principe holographique
la source
10
+1, mais j'utiliserais $ eval avant d'utiliser un couplage sans événement. Voir ma réponse pour plus de détails.
Mark Rajcok
1
@PigalevPavel Je pense que vous confondez $timeout(ce qui est essentiellement setTimeout+ Angular $scope.$apply()) avec setInterval. $timeoutne s'exécutera qu'une seule fois par ifcondition, et ce sera au début du $digestcycle suivant . Pour plus d'informations sur les délais d' expiration
holographic-
1
@finishingmove Peut-être que je ne comprends pas quelque chose :) Mais voyez que j'ai ng-repeat dans un autre ng-repeat. Et je veux savoir avec certitude quand tous seront terminés. Lorsque j'utilise votre script avec $ timeout uniquement pour le parent ng-repeat, tout fonctionne bien. Mais si je n'utilise pas $ timeout, j'obtiens une réponse avant que les répétitions ng des enfants ne soient terminées. Je veux savoir pourquoi? Et puis-je être sûr que si j'utilise votre script avec $ timeout pour le parent ng-repeat, j'obtiendrai toujours une réponse lorsque toutes les ng-répétitions seront terminées?
Pigalev Pavel
1
La raison pour laquelle vous utiliseriez quelque chose comme setTimeout()avec 0 retard est une autre question, mais je dois dire que je n'ai jamais rencontré de spécification réelle pour la file d'attente des événements du navigateur, ce qui est impliqué par le thread unique et l'existence de setTimeout().
rakslice
1
Merci pour cette réponse. J'ai une question, pourquoi devons-nous assigner on-finish-render="ngRepeatFinished"? Lorsque j'attribue on-finish-render="helloworld", cela fonctionne de la même manière.
Arnaud
89

Utilisez $ evalAsync si vous souhaitez que votre rappel (c'est-à-dire test ()) soit exécuté après la construction du DOM, mais avant le rendu du navigateur. Cela évitera le scintillement - réf .

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

Violon .

Si vous voulez vraiment appeler votre rappel après le rendu, utilisez $ timeout:

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

Je préfère $ eval au lieu d'un événement. Avec un événement, nous devons connaître le nom de l'événement et ajouter du code à notre contrôleur pour cet événement. Avec $ eval, il y a moins de couplage entre le contrôleur et la directive.

Mark Rajcok
la source
À quoi sert le chèque $last? Je ne le trouve pas dans les documents. A-t-il été supprimé?
Erik Aigner
2
@ErikAigner, $lastest défini sur la portée si un ng-repeatest actif sur l'élément.
Mark Rajcok
On dirait que votre violon prend un superflu $timeout.
bbodenmiller
1
Génial. Cela devrait être la réponse acceptée, car l'émission / la diffusion coûte plus cher du point de vue des performances.
Florin Vistig
29

Les réponses qui ont été données jusqu'à présent ne fonctionneront que la première fois que le sera ng-repeatrendu , mais si vous avez une dynamique ng-repeat, ce qui signifie que vous allez ajouter / supprimer / filtrer des éléments, et vous devez être averti chaque fois que le ng-repeatest rendu, ces solutions ne fonctionneront pas pour vous.

Donc, si vous devez être averti à CHAQUE FOIS que le ng-repeatrendu est rendu et pas seulement la première fois, j'ai trouvé un moyen de le faire, c'est assez `` hacky '', mais cela fonctionnera bien si vous savez ce que vous êtes Faire. Utilisez-le $filterdans votre ng-repeat avant d'utiliser tout autre$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;
    };
})

Il s'agira d' $emitun événement appelé à ngRepeatFinishedchaque fois que le sera ng-repeatrendu.

Comment l'utiliser:

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

Le ngRepeatFinishfiltre doit être appliqué directement à un Arrayou à un Objectdéfini dans votre $scope, vous pouvez ensuite appliquer d'autres filtres.

Comment NE PAS l'utiliser:

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

N'appliquez pas d'autres filtres en premier, puis appliquez le ngRepeatFinishfiltre.

Quand dois-je l'utiliser?

Si vous souhaitez appliquer certains styles CSS dans le DOM une fois le rendu de la liste terminé, car vous devez prendre en compte les nouvelles dimensions des éléments DOM qui ont été restitués par le ng-repeat. (BTW: ce type d'opérations doit être effectué dans une directive)

Ce qu'il NE FAUT PAS FAIRE dans la fonction qui gère l' ngRepeatFinishedévénement:

  • N'effectuez pas de fonction $scope.$applydans cette fonction ou vous mettrez Angular dans une boucle sans fin qu'Angular ne pourra pas détecter.

  • Ne l'utilisez pas pour apporter des modifications aux $scopepropriétés, car ces modifications ne seront pas reflétées dans votre vue jusqu'à la prochaine $digestboucle, et puisque vous ne pouvez pas effectuer une, $scope.$applyelles ne seront d'aucune utilité.

"Mais les filtres ne sont pas destinés à être utilisés comme ça !!"

Non, ils ne le sont pas, c'est un hack, si vous ne l'aimez pas, ne l'utilisez pas. Si vous connaissez une meilleure façon d'accomplir la même chose, faites-le moi savoir.

Résumer

C'est un hack , et l'utiliser de la mauvaise façon est dangereux, utilisez-le uniquement pour appliquer des styles une fois le ng-repeatrendu terminé et vous ne devriez avoir aucun problème.

Josep
la source
Thx 4 la pointe. Quoi qu'il en soit, un plunkr avec un exemple de travail serait très apprécié.
holographix
2
Malheureusement, cela n'a pas fonctionné pour moi, du moins pour la dernière AngularJS 1.3.7. J'ai donc découvert une autre solution de contournement, pas la plus agréable, mais si cela est essentiel pour vous, cela fonctionnera. Ce que j'ai fait, c'est que chaque fois que j'ajoute / modifie / supprime des éléments, j'ajoute toujours un autre élément factice à la fin de la liste, par conséquent, puisque l' $lastélément est également modifié, une simple ngRepeatFinishdirective en vérifiant $lastmaintenant fonctionnera. Dès que ngRepeatFinish est appelé, je supprime l'élément factice. (Je le cache également avec CSS pour qu'il n'apparaisse pas brièvement)
darklow
Ce hack est sympa mais pas parfait; chaque fois que le filtre se déclenche dans l'événement est envoyé cinq fois: - /
Sjeiti
Celui ci-dessous Mark Rajcokfonctionne parfaitement à chaque rendu de la répétition ng (par exemple en raison d'un changement de modèle). Cette solution hacky n'est donc pas nécessaire.
Dzhu
1
@Josep vous avez raison - lorsque les éléments sont épissés / non décalés (c'est-à-dire que le tableau est muté), cela ne fonctionne pas. Mes tests précédents étaient probablement avec des tableaux qui ont été réaffectés (en utilisant sugarjs), donc cela a fonctionné à chaque fois ... Merci de l'avoir signalé
Dzhu
10

Si vous devez appeler différentes fonctions pour différentes répétitions ng sur le même contrôleur, vous pouvez essayer quelque chose comme ceci:

La directive:

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

Dans votre contrôleur, capturez les événements avec $ on:

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

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

Dans votre modèle avec plusieurs 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
la source
5

Les autres solutions fonctionneront correctement lors du chargement initial de la page, mais l'appel de $ timeout à partir du contrôleur est le seul moyen de garantir que votre fonction est appelée lorsque le modèle change. Voici un violon qui utilise $ timeout. Pour votre exemple, ce serait:

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

ngRepeat n'évaluera une directive que lorsque le contenu de la ligne est nouveau, donc si vous supprimez des éléments de votre liste, onFinishRender ne se déclenchera pas. Par exemple, essayez d'entrer des valeurs de filtre dans ces émetteurs violons .

John Harley
la source
De même, test () n'est pas toujours appelé dans la solution evalAsynch lorsque le modèle change de violon
John Harley
2
ce qui est taen $scope.$watch("ta",...?
Muhammad Adeel Zahid
4

Si vous n'êtes pas opposé à l'utilisation d'accessoires de portée à deux dollars et que vous écrivez une directive dont le seul contenu est une répétition, il existe une solution assez simple (en supposant que vous ne vous souciez que du rendu initial). Dans la fonction de liaison:

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

Ceci est utile dans les cas où vous avez une directive qui doit effectuer une manipulation DOM en fonction de la largeur ou de la hauteur des membres d'une liste rendue (ce qui, je pense, est la raison la plus probable pour laquelle vous poseriez cette question), mais ce n'est pas aussi générique comme les autres solutions qui ont été proposées.

Point-virgule
la source
1

Une solution à ce problème avec un ngRepeat filtré aurait pu être avec les événements de mutation , mais ils sont obsolètes (sans remplacement immédiat).

Puis j'ai pensé à une autre facile:

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

Cela se connecte aux méthodes Node.prototype de l'élément parent avec un délai d'attente $ à une seule tick pour surveiller les modifications successives.

Cela fonctionne généralement correctement, mais j'ai eu quelques cas où l'altDone serait appelé deux fois.

Encore une fois ... ajoutez cette directive au parent de ngRepeat.

Sjeiti
la source
Cela fonctionne le mieux jusqu'à présent, mais ce n'est pas parfait. Par exemple, je passe de 70 éléments à 68, mais cela ne se déclenche pas.
Gaui
1
Ce qui pourrait fonctionner, c'est appendChildaussi l' ajout (puisque je n'ai utilisé insertBeforeque removeChild) et dans le code ci-dessus.
Sjeiti
1

Très facile, c'est comme ça que je l'ai fait.

.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
la source
Ce code fonctionne bien sûr si vous avez votre propre service $ blockUI.
CPhelefu
0

Veuillez jeter un œil au violon, http://jsfiddle.net/yNXS2/ . Étant donné que la directive que vous avez créée n'a pas créé de nouvelle portée, j'ai continué dans la voie.

$scope.test = function(){... rendu cela possible.

Rajkamal Subramanian
la source
Mauvais violon? Identique à celui de la question.
James M
humm, avez-vous mis à jour le violon? Son montrant actuellement une copie de mon code d'origine.
PCoelho
0

Je suis très surpris de ne pas voir la solution la plus simple parmi les réponses à cette question. Ce que vous voulez faire, c'est ajouter une ngInitdirective sur votre élément répété (l'élément avec la ngRepeatdirective) en vérifiant $last(une variable spéciale définie dans la portée par ngRepeatlaquelle indique que l'élément répété est le dernier de la liste). Si $lastest vrai, nous rendons le dernier élément et nous pouvons appeler la fonction que nous voulons.

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

Le code complet de votre balisage HTML serait:

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

Vous n'avez pas besoin de code JS supplémentaire dans votre application en plus de la fonction scope que vous souhaitez appeler (dans ce cas, test) car elle ngInitest fournie par Angular.js. Assurez-vous simplement d'avoir votre testfonction dans la portée afin qu'elle soit accessible à partir du modèle:

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