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

232

То, что я пытаюсь реализовать, в основном является обработчиком "on ng repeat finished render". Я могу обнаружить, когда это будет сделано, но я не могу понять, как вызвать из него функцию.

Проверьте скрипт: 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/

  • 0
    Из любопытства, какова цель фрагмента element.ready() ? Я имею в виду ... это какой-то плагин jQuery, который у вас есть, или он должен быть запущен, когда элемент готов?
  • 1
    Это можно сделать с помощью встроенных директив, таких как ng-init
Показать ещё 2 комментария
Теги:
handler
events
directive
ready

10 ответов

373
Лучший ответ
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 + $scope.$apply() Angular $scope.$apply() ) с setInterval . $timeout будет выполняться только один раз в соответствии с условием if , и это будет в начале следующего цикла $digest . Для получения дополнительной информации о тайм- аутах JavaScript см .: ejohn.org/blog/how-javascript-timers-work
Показать ещё 28 комментариев
87

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

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

Fiddle.

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

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

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

  • 0
    Что делает проверка на $last ? Не могу найти это в документах. Это было удалено?
  • 2
    @ErikAigner, $last определяется в области видимости, если для элемента активен ng-repeat .
Показать ещё 2 комментария
27

Ответы, которые были даны до сих пор, будут работать только в первый раз, когда 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. (BTW: эти операции должны выполняться внутри директивы)

Что НЕ делать в функции, которая обрабатывает событие ngRepeatFinished:

  • Не выполняйте $scope.$apply в этой функции, иначе вы ставите Angular в бесконечный цикл, который Angular не сможет обнаружить.

  • Не используйте его для внесения изменений в свойствах $scope, поскольку эти изменения не будут отображаться в вашем представлении до следующего цикла $digest, и поскольку вы не можете выполнить $scope.$apply они не будут использоваться.

"Но фильтры не предназначены для использования!"

Нет, это не так, это хак, если вам это не нравится, не используйте его. Если вы знаете лучший способ выполнить одно и то же, пожалуйста, сообщите мне об этом.

Подведение итогов

Это хак, и использование его неправильным образом опасно, используйте его только для применения стилей после завершения ng-repeat рендеринга, и у вас не должно быть никаких проблем. p >

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

Если вам нужно вызвать разные функции для разных 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 из контроллера - единственный способ гарантировать, что ваша функция будет вызвана при изменении модели. Вот работающий fiddle, который использует $timeout. Для вашего примера это будет:

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

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

  • 0
    Точно так же test () не всегда вызывается в решении evalAsynch, когда модель меняет скрипку
  • 2
    что такое ta в $scope.$watch("ta",... ?
3

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

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

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

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());
            }
        }
    };
})
  • 0
    Этот код работает, конечно, если у вас есть собственный сервис $ blockUI.
1

Решение этой проблемы с фильтрованным 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.

  • 0
    Это работает лучше всего, но не идеально. Например я иду от 70 пунктов до 68, но это не стреляет.
  • 1
    Что может сработать, так это добавление appendChild (так как я использовал только insertBefore и removeChild ) в приведенном выше коде.
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");
}
0

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

$scope.test = function(){... сделал это.

  • 0
    Неправильная скрипка? То же, что в вопросе.
  • 0
    хм, ты обновил скрипку? В настоящее время показывает копию моего исходного кода.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню