Вызов функции после завершения ng-repeat


249

То, что я пытаюсь реализовать, это в основном обработчик «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/


Из любопытства, какова цель element.ready()фрагмента? Я имею в виду ... это какой-то плагин jQuery, который у вас есть, или он должен быть запущен, когда элемент готов?
gion_13

1
Это можно сделать с помощью встроенных директив, таких как ng-init
semiomant


дубликат stackoverflow.com/questions/13471129/… , см. мой ответ там
bresleveloper

Ответы:


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

Обратите внимание, что я не использовал, .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>

10
+1, но я бы использовал $ eval перед использованием событий - меньше связывания. Смотрите мой ответ для более подробной информации.
Марк Райкок

1
@PigalevPavel Я думаю, что вы путаете $timeout(что в основном setTimeout+ с Angular $scope.$apply()) setInterval. $timeoutбудет выполняться только один раз в соответствии с ifусловием, и это будет в начале следующего $digestцикла. Дополнительную информацию о тайм- аутах
голографический принцип,

1
@finishingmove Может быть, я чего-то не понимаю :) Но видите, у меня есть ng-repeat в другом ng-repeat. И я хочу знать наверняка, когда все они закончены. Когда я использую ваш скрипт с $ timeout только для родительского ng-repeat, все работает нормально. Но если я не использую $ timeout, я получаю ответ до того, как дети ng-повторы будут закончены. Я хочу знать почему? И могу ли я быть уверен, что если я использую ваш скрипт с $ timeout для родительского ng-repeat, я всегда получу ответ, когда все ng-повторы будут завершены?
Пигалев Павел

1
Почему вы должны использовать что-то вроде setTimeout()задержки 0, это другой вопрос, хотя я должен сказать, что я никогда не сталкивался с реальной спецификацией для очереди событий браузера где-либо, только то, что подразумевает однопоточность и существование setTimeout().
Ракслице

1
Спасибо за этот ответ. У меня вопрос, зачем нам назначать on-finish-render="ngRepeatFinished"? Когда я назначаю on-finish-render="helloworld", он работает так же.
Арно

89

Используйте $ evalAsync, если вы хотите, чтобы ваш обратный вызов (т. Е. Test ()) выполнялся после создания DOM, но до того, как браузер отобразит. Это предотвратит мерцание - ref .

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

скрипка .

Если вы действительно хотите вызвать обратный вызов после рендеринга, используйте $ timeout:

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

Я предпочитаю $ eval вместо события. Для события нам нужно знать имя события и добавить код в наш контроллер для этого события. С $ eval связь между контроллером и директивой уменьшается.


Для чего нужен чек $last? Не могу найти это в документах. Это было удалено?
Эрик Эйгнер

2
@ErikAigner, $lastопределяется в области действия, если ng-repeatэлемент активен.
Марк Райкок

Похоже, твоя скрипка принимает ненужное $timeout.
bbodenmiller

1
Отлично. Это должен быть принятый ответ, учитывая, что передача / передача стоит дороже с точки зрения производительности.
Флорин Вистиг,

29

Ответы, которые были даны до сих пор, будут работать только при первом 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как закончите рендеринг, и у вас не должно быть никаких проблем.


Спасибо 4 чаевые Во всяком случае, plunkr с рабочим примером будет высоко ценится.
Голографическое изображение

2
К сожалению, это не сработало для меня, по крайней мере, для последней версии AngularJS 1.3.7. Итак, я нашел другой обходной путь, не самый хороший, но если это важно для вас, он сработает. То, что я сделал, всякий раз, когда я добавляю / изменяю / удаляю элементы, я всегда добавляю другой фиктивный элемент в конец списка, поэтому, поскольку $lastэлемент также изменяется, будет работать простая ngRepeatFinishдиректива, проверяющая $lastтеперь. Как только вызывается ngRepeatFinish, я удаляю фиктивный элемент. (Я также скрываю это с помощью CSS, чтобы он не появлялся кратко)
darklow

Этот хак хорош, но не идеален; каждый раз, когда фильтр срабатывает, событие отправляется пять раз: - /
Sjeiti

Нижеследующее Mark Rajcokработает отлично при каждом повторном рендеринге ng-repeat (например, из-за изменения модели). Так что это хакерское решение не требуется.
Джу

1
@ Josep вы правы - когда элементы сращены / не смещены (то есть массив изменен), это не работает. Мое предыдущее тестирование, вероятно, было с массивом, который был переназначен (используя sugarjs), поэтому он работал каждый раз ... Спасибо за указание на это
Джу

10

Если вам нужно вызывать разные функции для разных 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>

5

Другие решения будут нормально работать при начальной загрузке страницы, но вызов $ timeout из контроллера - единственный способ убедиться, что ваша функция вызывается при изменении модели. Вот рабочая скрипка, которая использует $ timeout. Для вашего примера это будет:

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

ngRepeat будет оценивать директиву только тогда, когда содержимое строки новое, поэтому, если вы удалите элементы из своего списка, onFinishRender не сработает. Например, попробуйте ввести значения фильтра в эти эмиссии скриптов .


Аналогично, test () не всегда вызывается в решении evalAsynch, когда модель меняет скрипку
Джон Харли

2
что taвнутри $scope.$watch("ta",...?
Мухаммад Адель Захид

4

Если вы не против использования приманок с двойным долларовым охватом и пишете директиву, единственным содержанием которой является повторение, есть довольно простое решение (при условии, что вы заботитесь только о начальном рендере). В функции ссылки:

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

Это полезно в тех случаях, когда у вас есть директива, которая должна выполнять DOM-манипуляции на основе ширины или высоты членов отображаемого списка (что, я думаю, является наиболее вероятной причиной, по которой можно задать этот вопрос), но она не такая общая как и другие решения, которые были предложены.


1

Решение этой задачи с фильтрованной ngRepeat мог быть с мутацией событиями , но они устарели (без немедленной замены).

Тогда я подумал о другом простом:

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 родительского элемента с однократным $ timeout для отслеживания последовательных изменений.

Это работает в основном правильно, но я получил несколько случаев, когда altDone вызывался бы дважды.

Снова ... добавьте эту директиву к родителю ngRepeat.


Пока это работает лучше, но не идеально. Например я иду от 70 пунктов до 68, но это не стреляет.
Гауи

1
Что может работать это добавление , appendChildа также (так как я использовал только insertBeforeи removeChild) в приведенном выше коде.
Сеити

1

Очень просто, вот как я это сделал.

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


Этот код работает, конечно, если у вас есть собственный сервис $ blockUI.
CPhelefu

0

Пожалуйста, посмотрите на скрипку, http://jsfiddle.net/yNXS2/ . Так как созданная вами директива не создала новую область, я продолжил свой путь.

$scope.test = function(){... сделал это случиться.


Неправильная скрипка? То же, что в вопросе.
Джеймс М

хм, ты обновил скрипку? Это в настоящее время показывает копию моего оригинального кода.
PCoelho

0

Я очень удивлен, что не вижу самого простого решения среди ответов на этот вопрос. То, что вы хотите сделать, это добавить 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");
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.