Вызов функции, когда ng-repeat завершен

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

Ответ 1

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>

Ответ 2

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

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

Fiddle.

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

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

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

Ответ 3

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

Ответ 4

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

Ответ 6

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

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

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

Ответ 7

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

Ответ 8

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

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

Ответ 9

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

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

Ответ 10

Я очень удивлен тем, что не вижу самого простого решения среди ответов на этот вопрос. То, что вы хотите сделать, это добавить директиву 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");
}