Вызов функции после завершения ng-repeat
То, что я пытаюсь реализовать, это в основном обработчик "on ng repeat Законченный рендеринг". Я могу определить, когда это сделано, но я не могу понять, как вызвать функцию из этого.
Проверьте скрипку: 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>
Ответ: Рабочая скрипка с финишной черты: http://jsfiddle.net/paulocoelho/BsMqq/4/
10 ответов
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);
});
}
}
}
});
Обратите внимание, что я не использовал .ready()
а скорее завернул его в $timeout
, $timeout
гарантирует, что он выполняется, когда элементы с повторением ng ДЕЙСТВИТЕЛЬНО закончили рендеринг (потому что $timeout
будет выполняться в конце текущего цикла дайджеста - и это также вызовет $apply
внутренне, в отличие от setTimeout
). Так что после ng-repeat
закончили, мы используем $emit
отправить событие на внешние области (родственные и родительские области).
И тогда в вашем контроллере, вы можете поймать его с $on
:
$scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) {
//you also get the actual event object
//do stuff, execute functions -- whatever...
});
С HTML, который выглядит примерно так:
<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
<div>{{item.name}}}<div>
</div>
Используйте $evalAsync, если вы хотите, чтобы ваш обратный вызов (т. Е. Test()) выполнялся после создания DOM, но до визуализации браузера. Это предотвратит мерцание - ref.
if (scope.$last) {
scope.$evalAsync(attr.onFinishRender);
}
Если вы действительно хотите вызвать обратный вызов после рендеринга, используйте $timeout:
if (scope.$last) {
$timeout(function() {
scope.$eval(attr.onFinishRender);
});
}
Я предпочитаю $ eval вместо события. В случае события нам нужно знать имя события и добавить код в наш контроллер для этого события. С $ eval связь между контроллером и директивой уменьшается.
Ответы, которые были даны до сих пор, будут работать только в первый раз, когда ng-repeat
рендерится, но если у вас есть динамический ng-repeat
Это означает, что вы собираетесь добавлять / удалять / фильтровать элементы, и вы должны получать уведомления каждый раз, когда ng-repeat
эти решения не будут работать для вас.
Итак, если вам нужно получать уведомления КАЖДЫЙ РАЗ ng-repeat
перерисовывается и не только в первый раз, я нашел способ сделать это, он довольно "хакерский", но он будет работать нормально, если вы знаете, что делаете. Использовать этот $filter
в вашем ng-repeat
прежде чем использовать любой другой $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;
};
})
Это будет $emit
событие называется ngRepeatFinished
каждый раз, когда ng-repeat
оказывается
Как это использовать:
<li ng-repeat="item in (items|ngRepeatFinish) | filter:{name:namedFiltered}" >
ngRepeatFinish
фильтр должен быть применен непосредственно к Array
или Object
определены в вашем $scope
Вы можете применить другие фильтры после.
Как НЕ использовать это:
<li ng-repeat="item in (items | filter:{name:namedFiltered}) | ngRepeatFinish" >
Не применяйте сначала другие фильтры, а затем применяйте ngRepeatFinish
фильтр.
Когда я должен использовать это?
Если вы хотите применить определенные стили CSS в DOM после завершения рендеринга списка, потому что вам необходимо учитывать новые измерения элементов DOM, которые были перерисованы ng-repeat
, (Кстати: такие операции должны выполняться внутри директивы)
Чего НЕ ДЕЛАТЬ в функции, которая обрабатывает ngRepeatFinished
событие:
Не выполнять
$scope.$apply
в этой функции или вы поместите Angular в бесконечный цикл, который Angular не сможет обнаружить.Не используйте его для внесения изменений в
$scope
свойства, потому что эти изменения не будут отражены в вашем представлении до следующего$digest
цикл, и так как вы не можете выполнить$scope.$apply
они не будут бесполезны.
"Но фильтры не предназначены для такого использования!!"
Нет, это не хак, если вам не нравится, не используйте его. Если вы знаете лучший способ сделать то же самое, пожалуйста, дайте мне знать.
подведение
Это хак, и использование его неверным способом опасно, используйте его только для применения стилей после
ng-repeat
закончил рендеринг, и у вас не должно быть проблем.
Если вам нужно вызывать разные функции для разных ng-повторов на одном контроллере, вы можете попробовать что-то вроде этого:
Директива:
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');
});
}
}
}
});
В вашем контроллере ловите события с $ on:
$scope.$on('ngRepeatBroadcast1', function(ngRepeatFinishedEvent) {
// Do something
});
$scope.$on('ngRepeatBroadcast2', function(ngRepeatFinishedEvent) {
// Do something
});
В вашем шаблоне с несколькими 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>
Другие решения будут отлично работать при начальной загрузке страницы, но вызов $timeout из контроллера - единственный способ убедиться, что ваша функция вызывается при изменении модели. Вот рабочая скрипка, которая использует $timeout. Для вашего примера это будет:
.controller('myC', function ($scope, $timeout) {
$scope.$watch("ta", function (newValue, oldValue) {
$timeout(function () {
test();
});
});
ngRepeat будет оценивать директиву только тогда, когда содержимое строки новое, поэтому, если вы удалите элементы из своего списка, onFinishRender не сработает. Например, попробуйте ввести значения фильтра в эти скрипты emit.
Если вы не против использования приманок с двойным долларом и пишете директиву, единственное содержимое которой - повтор, есть довольно простое решение (при условии, что вы заботитесь только о начальном рендере). В функции ссылки:
const dereg = scope.$watch('$$childTail.$last', last => {
if (last) {
dereg();
// do yr stuff -- you may still need a $timeout here
}
});
Это полезно в тех случаях, когда у вас есть директива, которая должна выполнять DOM-манипуляции на основе ширины или высоты членов отображаемого списка (что, я думаю, является наиболее вероятной причиной, по которой можно задать этот вопрос), но она не такая общая как и другие решения, которые были предложены.
Я очень удивлен, что не вижу самого простого решения среди ответов на этот вопрос. Что вы хотите сделать, это добавить ngInit
директива на ваш повторяющийся элемент (элемент с ngRepeat
директива) проверка на $last
(специальная переменная, установленная в области видимости ngRepeat
который указывает, что повторяющийся элемент является последним в списке). Если $last
это правда, мы рендерим последний элемент, и мы можем вызвать функцию, которую мы хотим.
ng-init="$last && test()"
Полный код для вашей разметки HTML будет:
<div ng-app="testApp" ng-controller="myC">
<p ng-repeat="t in ta" ng-init="$last && test()">{{t}}</p>
</div>
Вам не нужен дополнительный JS-код в вашем приложении, кроме функции объема, которую вы хотите вызвать (в этом случае test
) поскольку ngInit
предоставляется Angular.js. Просто убедитесь, что ваш test
Функция в области видимости, чтобы к ней можно было получить доступ из шаблона:
$scope.test = function test() {
console.log("test executed");
}
Решение этой проблемы с отфильтрованным ngRepeat могло бы быть с событиями Mutation, но они устарели (без немедленной замены).
Тогда я подумал о другом простом:
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');
}
}
};
});
Это подключается к методам Node.prototype родительского элемента с однократным тайм-аутом $ для отслеживания последовательных изменений.
Это работает в основном правильно, но я получил некоторые случаи, когда altDone будет вызываться дважды.
Снова... добавьте эту директиву к родителю ngRepeat.
Очень просто, вот как я это сделал.
.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());
}
}
};
})
Пожалуйста, взгляните на скрипку, http://jsfiddle.net/yNXS2/. Так как созданная вами директива не создала новую область, я продолжил свой путь.
$scope.test = function(){...
сделал это случиться.