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

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

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

ответ: Рабочая скрипка из finishingmove:http://jsfiddle.net/paulocoelho/BsMqq/4/

9 ответов


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, но до отрисовки браузера. Это предотвратит мерцание ... --5-->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 получает повторно вынесено и не только в первый раз, я нашел способ сделать это, это довольно "hacky", но он будет работать нормально, если вы знаете, что делаете. Используйте это $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 в этой функции, или вы поставите угловой в бесконечный цикл, что угловые не сможет обнаружить.

  • не используйте его для внесения изменений в $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 не сработает. Например, попробуйте ввести в них значения фильтра скрипки!--6-->выделяет.


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

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

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


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

это подключается к узлу.прототипные методы родительского элемента с одним тиком $timeout для просмотра последовательных модификаций.

Он работает в основном правильно, но я получил некоторые случаи, когда 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(){... Это случилось.