web-dev-qa-db-fra.com

Appeler une fonction lorsque ng-repeat est terminé

Ce que je tente d’implémenter est essentiellement un gestionnaire de rendu "le rendu terminé". Je suis capable de détecter quand cela 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>

Answer: Le violon en train de finirmove: http://jsfiddle.net/paulocoelho/BsMqq/4/

244
PCoelho
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 que je l’ai enveloppé dans un $timeout. $timeout s'assure qu'il est exécuté lorsque le rendu de tous les éléments ng-Répétés est VRAIMENT terminé (car le $timeout sera exécuté à la fin du cycle de résumé en cours - et appellera également $apply en interne, contrairement à setTimeout). Ainsi, une fois que ng-repeat est terminé, nous utilisons $emit pour é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 du HTML qui ressemble à quelque chose comme ça:

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

Utilisez $ evalAsync si vous voulez que votre callback (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);
}

Fiddle .

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 à la place 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
Mark Rajcok

Les réponses données jusqu'à présent ne fonctionneront que la première fois que le ng-repeat sera rendu , mais si vous avez un ng-repeat dynamique, ce qui signifie que vous allez ajouter/supprimer/filtrer des éléments et que 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 soit restitué et pas seulement la première fois, j'ai trouvé un moyen de le faire, c'est assez 'hacky', mais cela fonctionnera bien si Tu sais ce que tu fais. Utilisez ce $filter dans votre ng-repeat avant d’utiliser un 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;
    };
})

Ceci $emit un événement appelé ngRepeatFinished chaque fois que le ng-repeat sera rendu.

Comment l'utiliser:

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

Le filtre ngRepeatFinish doit être appliqué directement à une Array ou à une Object définie 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'abord d'autres filtres, puis appliquez le filtre ngRepeatFinish.

Quand devrais-je l'utiliser?

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

Quoi NE PAS FAIRE dans la fonction qui gère l'événement ngRepeatFinished:

  • N'effectuez pas de $scope.$apply dans cette fonction, sinon vous placerez Angular dans une boucle infinie qu'il ne pourra pas détecter.

  • Ne l'utilisez pas pour apporter des modifications aux propriétés $scope, car ces modifications ne seront reflétées dans votre vue que lors de la prochaine boucle $digest et, étant donné que vous ne pouvez pas effectuer un $scope.$apply, elles ne seront d'aucune utilité.

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

Non, ce n'est pas un hack, si vous n'aimez pas, ne l'utilisez pas. Si vous connaissez un meilleur moyen d'accomplir la même chose, faites-le-moi savoir.

Résumant

Ceci est un hack , et l'utiliser de manière inappropriée est dangereux, utilisez-le uniquement pour appliquer des styles après le rendu du ng-repeat et vous ne devriez pas rencontrer de problèmes.

27
Josep

Si vous avez besoin d'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 $ 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
MCurbelo

Les autres solutions fonctionnent 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 est modifié. Voici un travail 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'évalue une directive que lorsque le contenu de la ligne est nouveau. Par conséquent, si vous supprimez des éléments de votre liste, onFinishRender ne se déclenchera pas. Par exemple, essayez de saisir des valeurs de filtre dans ces fiddles emit .

5
John Harley

Si vous n’êtes pas opposé à l’utilisation d’accessoires d’objet à 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 préoccupiez 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
    }
});

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

3
Semicolon

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
CPhelefu

Une solution à ce problème avec ngRepeat filtré aurait pu être avec Mutation events, mais ils sont déconseillés (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');
            }
        }
    };
});

Cela s'accroche aux méthodes Node.prototype de l'élément parent avec un délai d'expiration en une seule fois pour surveiller les modifications successives.

Cela fonctionne la plupart du temps correctement mais j’ai eu quelques cas où le altDone serait appelé deux fois.

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

1
Sjeiti

Je suis très surpris de ne pas trouver la solution la plus simple parmi les réponses à cette question . Ce que vous voulez faire est d’ajouter une directive ngInit à votre élément répété (l’élément avec la directive ngRepeat) en recherchant $last variable définie dans la portée par ngRepeat qui indique que l'élément répété est le dernier de la liste). Si $last est vrai, nous convertissons le dernier élément et nous pouvons appeler la fonction souhaitée.

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

Le code complet de votre balise 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 d'étendue que vous souhaitez appeler (dans ce cas, test) car ngInit est fourni par Angular.js. Assurez-vous simplement d'avoir votre fonction test dans la portée afin de pouvoir y accéder à partir du modèle:

$scope.test = function test() {
    console.log("test executed");
}

Veuillez regarder le violon, http://jsfiddle.net/yNXS2/ . Comme la directive que vous avez créée n’a pas créé de nouvelle portée, j’ai poursuivi dans la voie.

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

0