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

Ce que j'essaie d'implémenter est essentiellement un gestionnaire "on ng repeat finished rendering". Je suis capable de détecter quand c'est fait mais je ne peux pas comprendre comment déclencher une fonction à partir de celui-ci.

Vérifier 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 de finishingmove: http://jsfiddle.net/paulocoelho/BsMqq/4/

244
demandé sur PCoelho 2013-03-04 21:51:48

9 réponses

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 N'ai pas utilisé .ready() mais plutôt l'enveloppé dans un $timeout. $timeout s'assure qu'il est exécuté lorsque les éléments ng-repeated ont vraiment terminé le rendu (car le $timeout s'exécutera à la fin du cycle de digestion en cours - et il appellera également $apply en interne, contrairement àsetTimeout). Donc, une fois le ng-repeat terminé, nous utilisons $emit pour émettre un événement vers des étendues externes (étendues sœurs et parentes).

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>
391
répondu holographic-principle 2016-06-27 12:16:35

Utilisez $ evalAsync Si vous voulez que votre rappel (c'est-à-dire, test()) soit exécuté après la construction du DOM, mais avant le rendu du navigateur. Cela empêchera le scintillement -- ref .

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.

87
répondu Mark Rajcok 2013-03-04 22:30:14

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

Donc, Si vous devez être averti chaque fois que le ng-repeat est rendu à nouveau et pas seulement la première fois, j'ai trouvé un moyen de le faire, c'est tout à fait 'hacky", mais il fonctionnera bien si vous savez ce que vous faites. Utilisez ce $filter dans 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;
    };
})

Ce sera $emit un événement appelé ngRepeatFinished chaque fois que l' ng-repeat est rendu.

Comment l'utiliser:

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

Le ngRepeatFinish filtre doit être appliqué directement sur une Array ou Object définis dans votre $scope, vous pouvez appliquer d'autres filtres après.

Comment ne pas l'utiliser:

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

N'appliquez pas d'autres filtres appliquez d'abord le filtre ngRepeatFinish.

Quand devrais-je utiliser ceci?

Si vous souhaitez appliquer certains styles css dans le DOM après la fin du rendu de la liste, vous devez tenir compte des nouvelles dimensions des éléments DOM qui ont été rendus par le ng-repeat. (BTW: ce genre d'opérations devrait être fait dans une directive)

Que ne pas faire dans la fonction qui gère l'événement ngRepeatFinished:

  • Ne pas effectuer un $scope.$apply dans ce fonction ou vous mettrez angulaire dans une boucle sans fin que angulaire ne sera pas en mesure de détecter.

  • Ne l'utilisez pas pour apporter des modifications dans les propriétés $scope, car ces modifications ne seront pas reflétées dans votre vue avant la prochaine boucle $digest, Et puisque vous ne pouvez pas effectuer un $scope.$apply, elles 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 un meilleur façon de faire la même chose merci de me le faire savoir.

Résumant

C'est un hack, et l'utiliser de la mauvaise façon est dangereux, utilisez-le Uniquement pour appliquer des styles après que le ng-repeat a terminé le rendu et vous ne devriez pas avoir de problèmes.

27
répondu Josep 2014-09-29 03:14:23

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, capturer des événements avec $sur:

$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>
11
répondu MCurbelo 2015-06-05 16:43:05

Les autres solutions fonctionneront bien lors du chargement initial de la page, mais l'appel de $timeout à partir du contrôleur est le seul moyen de s'assurer que votre fonction est appelée lorsque le modèle change. Voici un fiddle de travail 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 violons émettent.

5
répondu John Harley 2014-01-31 01:44:06

Si vous n'êtes pas opposé à l'utilisation d'Accessoires de portée à double dollar 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 lien:

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

Ceci est utile pour les cas où vous avez une directive qui doit faire DOM manip en fonction des largeurs ou des hauteurs des membres d'une liste rendue (ce qui, je pense, est la raison la plus probable pour poser cette question), mais ce n'est pas aussi générique que le d'autres solutions qui ont été proposées.

3
répondu Semicolon 2016-09-26 04:14:44

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

Puis j'ai pensé à un 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');
            }
        }
    };
});

Ce crochet dans le nœud.prototyper les méthodes de l'élément parent avec un délai d'attente $à cocher pour surveiller les modifications successives.

Cela fonctionne principalement correctement mais j'ai eu des cas où l'altDone serait appelé deux fois.

Encore... ajouter cette directive au parent du ngRepeat.

1
répondu Sjeiti 2015-04-22 08:57:13

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());
            }
        }
    };
})
1
répondu CPhelefu 2016-01-28 16:26:20

Regardez le violon, http://jsfiddle.net/yNXS2 / . puisque la directive que vous avez créée n'a pas créé une nouvelle portée, j'ai continué dans le chemin.

$scope.test = function(){... c'est arrivé.

0
répondu Rajkamal Subramanian 2013-03-04 18:20:43