Виклик функції, коли ng-повторення закінчено


249

Те, що я намагаюся реалізувати, це в основному обробник "на повторне завершене надання". Я в змозі виявити, коли це робиться, але я не можу зрозуміти, як запустити функцію від нього.

Перевірте загадку: 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
напівмомант


дублікат 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's $scope.$apply()) з setInterval. $timeoutвиконується лише один раз за ifумовою, і це буде на початку наступного $digestциклу. Більш детальну інформацію про час очікування JavaScript див: ejohn.org/blog/how-javascript-timers-work
голографічний принцип

1
@finishingmove Можливо, я чогось не розумію :) Але дивіться, я маю ng-повторення в іншому ng-повторі. І я хочу точно знати, коли всі вони закінчені. Коли я використовую ваш скрипт з $ timeout лише для батьківського ng-повтору, він працює добре. Але якщо я не використовую $ timeout, я отримую відповідь до завершення дітей ng-повторів. Я хочу знати, чому? І чи можу я бути впевнений, що якщо я буду використовувати ваш скрипт з $ timeout для батьківського ng-повтору, я завжди отримаю відповідь, коли всі ng-повтори закінчені?
Пігальов Павло

1
Чому ви використовуєте щось на зразок setTimeout()із затримкою 0 - це інше питання, хоча мушу сказати, що я ніколи не стикався з фактичною специфікацією для черги подій браузера ніде, лише те, що має на увазі однопоточність та існування setTimeout().
rakslice

1
Дякую за цю відповідь. У мене питання, чому нам потрібно призначати on-finish-render="ngRepeatFinished"? Коли я призначаю on-finish-render="helloworld", це працює так само.
Арно

89

Використовуйте $ evalAsync, якщо ви хочете, щоб ваш зворотний виклик (тобто тест ()) був виконаний після побудови DOM, але до надання браузера. Це запобіжить мерехтіння - посилання .

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. (BTW: такі операції слід робити в рамках директиви)

ЧОГО НЕ ДІЙТИ у функції, яка обробляє ngRepeatFinishedподію:

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

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

"Але фільтри не використовуються таким чином !!"

Ні, вони не є, це хак, якщо вам це не подобається, не використовуйте. Якщо ви знаєте кращий спосіб здійснити те саме, будь ласка, дайте мені це знати.

Узагальнення

Це злом , і використовувати його неправильним чином небезпечно, використовуйте його лише для застосування стилів після ng-repeatзавершення візуалізації, і у вас не повинно виникнути жодних проблем.


Thx 4 наконечник. У будь-якому разі, вкрай вдячний би план з робочим прикладом.
голографічний

2
На жаль, це не спрацювало для мене, принаймні для останнього AngularJS 1.3.7. Тож я виявив ще одне вирішення, не найприємніше, але якщо це важливо для вас, воно спрацює. Що я робив - це коли я додаю / змінюю / видалюю елементи, я завжди додаю ще один фіктивний елемент в кінці списку, тому оскільки $lastелемент також змінений, проста ngRepeatFinishдиректива, перевіривши $lastтепер, буде працювати. Як тільки ngRepeatFinish викликається, я видаляю фіктивний предмет. (Я також приховую це за допомогою CSS, щоб воно не з’являлося ненадовго)
темний

Цей хак приємний, але не ідеальний; кожного разу, коли фільтр стукає у події п'ять разів: - /
Sjeiti

Наведений нижче текст Mark Rajcokідеально працює при кожному повторному відтворенні ng-повтору (наприклад, через зміну моделі). Отже, це гакісне рішення не потрібно.
Джу

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-повторами

<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 не запуститься. Наприклад, спробуйте ввести значення фільтра в цьому Фіделя випустив .


Аналогічно, тест () не завжди викликається у рішенні 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.