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