Memanggil fungsi ketika ng-repeat telah selesai

249

Apa yang saya coba terapkan pada dasarnya adalah handler "on ng repeat finished rendering". Saya dapat mendeteksi ketika itu dilakukan tetapi saya tidak tahu bagaimana cara memicu suatu fungsi darinya.

Periksa biola: 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>

Jawab : Bekerja biola dari finishingmove: http://jsfiddle.net/paulocoelho/BsMqq/4/

PCoelho
sumber
Karena penasaran, apa tujuan element.ready()potongan itu? Maksud saya .. apakah ini semacam plugin jQuery yang Anda miliki, atau haruskah dipicu ketika elemen sudah siap?
gion_13
1
Orang bisa melakukannya menggunakan arahan
bawaan
Kemungkinan duplikat dari acara ng-repeat finish
Damjan Pavlica
duplikat stackoverflow.com/questions/13471129/… , lihat jawaban saya di sana
bresleveloper

Jawaban:

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

Perhatikan bahwa saya tidak menggunakan .ready()melainkan membungkusnya dalam $timeout. $timeoutmemastikan itu dieksekusi ketika elemen-elemen yang diulang telah BENAR-BENAR selesai rendering (karena $timeoutakan dijalankan pada akhir siklus digest saat ini - dan itu juga akan memanggil secara $applyinternal, tidak sepertisetTimeout ). Jadi setelah ng-repeatselesai, kita gunakan$emit untuk memancarkan suatu peristiwa ke lingkup luar (saudara dan orang tua lingkup).

Dan kemudian di controller Anda, Anda dapat menangkapnya dengan $on:

$scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) {
    //you also get the actual event object
    //do stuff, execute functions -- whatever...
});

Dengan html yang terlihat seperti ini:

<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
    <div>{{item.name}}}<div>
</div>
prinsip holografik
sumber
10
+1, tapi saya akan menggunakan $ eval sebelum menggunakan acara - kurang kopling. Lihat jawaban saya untuk lebih jelasnya.
Mark Rajcok
1
@PigalevPavel Saya pikir Anda bingung $timeout(yang pada dasarnya setTimeout+ Angular $scope.$apply()) dengan setInterval. $timeoutakan mengeksekusi hanya sekali per ifkondisi, dan itu akan menjadi pada awal $digestsiklus berikutnya . Untuk info lebih lanjut tentang batas waktu JavaScript, lihat: ejohn.org/blog/how-javascript-timers-work
holographic-principle
1
@finishingmove Mungkin saya tidak mengerti sesuatu :) Tapi lihat saya punya ng-repeat di ng-repeat lain. Dan saya ingin tahu pasti kapan semuanya selesai. Ketika saya menggunakan skrip Anda dengan $ timeout hanya untuk orang tua ng-ulangi semuanya berfungsi dengan baik. Tetapi jika saya tidak menggunakan $ timeout, saya mendapat jawaban sebelum ng-mengulang anak selesai. Saya ingin tahu kenapa? Dan bisakah saya memastikan bahwa jika saya menggunakan skrip Anda dengan $ timeout untuk orang tua ng-repeat saya akan selalu mendapat respons ketika semua ng-pengulangan selesai?
Pigalev Pavel
1
Mengapa Anda akan menggunakan sesuatu seperti setTimeout()dengan 0 penundaan adalah pertanyaan lain meskipun saya harus mengatakan saya tidak pernah menemukan spesifikasi yang sebenarnya untuk antrian acara browser di mana saja, hanya apa yang tersirat oleh single-threadedness dan keberadaan setTimeout().
rakslice
1
Terima kasih atas jawaban ini. Saya punya pertanyaan, mengapa kita perlu menetapkan on-finish-render="ngRepeatFinished"? Saat saya tetapkan on-finish-render="helloworld", kerjanya sama.
Arnaud
89

Gunakan $ evalAsync jika Anda ingin callback Anda (yaitu, test ()) dieksekusi setelah DOM dibuat, tetapi sebelum browser merender. Ini akan mencegah flicker - ref .

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

Biola .

Jika Anda benar-benar ingin menelepon panggilan balik setelah rendering, gunakan $ timeout:

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

Saya lebih suka $ eval daripada acara. Dengan suatu acara, kita perlu mengetahui nama acara dan menambahkan kode ke controller kita untuk acara itu. Dengan $ eval, ada lebih sedikit coupling antara controller dan directive.

Mark Rajcok
sumber
Untuk apa cek itu $lastdilakukan? Tidak dapat menemukannya di dokumen. Apakah sudah dihapus?
Erik Aigner
2
@ErikAigner, $lastdidefinisikan pada ruang lingkup jika suatu ng-repeatelemen aktif.
Mark Rajcok
Sepertinya Fiddle Anda mengambil yang tidak perlu $timeout.
bbodenmiller
1
Bagus. Ini harus menjadi jawaban yang diterima, melihat bahwa biaya siaran / siaran lebih dari sudut pandang kinerja.
Florin Vistig
29

Jawaban yang telah diberikan sejauh ini hanya akan berfungsi saat pertama kali ng-repeatdiberikan , tetapi jika Anda memiliki dinamika ng-repeat, artinya Anda akan menambahkan / menghapus / memfilter item, dan Anda harus diberi tahu setiap kali bahwang-repeat diberikan, solusi itu tidak akan bekerja untuk Anda.

Jadi, jika Anda perlu diberi tahu SETIAP SAAT bahwa yang di ng-repeat-re-render dan bukan hanya pertama kali, saya telah menemukan cara untuk melakukan itu, ini cukup 'retas', tetapi akan berfungsi dengan baik jika Anda tahu apa yang Anda lakukan perbuatan. Gunakan ini $filterdi Anda ng-repeat sebelum Anda menggunakan yang lain$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;
    };
})

Ini akan $emitacara yang disebut ngRepeatFinishedsetiap kaling-repeat mendapat diberikan.

Bagaimana cara menggunakannya:

<li ng-repeat="item in (items|ngRepeatFinish) | filter:{name:namedFiltered}" >

The ngRepeatFinishkebutuhan Filter untuk diterapkan langsung ke Arrayatau Objectdidefinisikan dalam Anda$scope , Anda dapat menerapkan filter lain setelah.

Bagaimana TIDAK menggunakannya:

<li ng-repeat="item in (items | filter:{name:namedFiltered}) | ngRepeatFinish" >

Jangan menerapkan filter lain terlebih dahulu lalu menerapkan ngRepeatFinishfilter.

Kapan saya harus menggunakan ini?

Jika Anda ingin menerapkan gaya css tertentu ke DOM setelah daftar selesai render, karena Anda harus memperhitungkan dimensi baru elemen DOM yang telah dirender ulang oleh ng-repeat . (BTW: operasi semacam itu harus dilakukan di dalam arahan)

Apa yang TIDAK HARUS DILAKUKAN dalam fungsi yang menangani ngRepeatFinishedacara:

  • Jangan melakukan $scope.$applydalam fungsi itu atau Anda akan menempatkan Angular dalam loop tanpa akhir yang tidak dapat dideteksi oleh Angular.

  • Jangan menggunakannya untuk membuat perubahan di $scopeproperti, karena perubahan itu tidak akan tercermin dalam tampilan Anda sampai $digestloop berikutnya , dan karena Anda tidak dapat melakukan itu $scope.$applymereka tidak akan ada gunanya.

"Tapi filter tidak dimaksudkan untuk digunakan seperti itu !!"

Tidak, ini bukan hack, jika Anda tidak suka, jangan menggunakannya. Jika Anda tahu cara yang lebih baik untuk mencapai hal yang sama, mohon beri tahu saya.

Meringkas

Ini adalah retasan , dan menggunakannya dengan cara yang salah berbahaya, gunakan hanya untuk menerapkan gaya setelah ng-repeatrendering selesai dan Anda seharusnya tidak memiliki masalah.

Josep
sumber
Thx 4 tipnya. Bagaimanapun, seorang plunkr dengan contoh kerja akan sangat dihargai.
holographix
2
Sayangnya ini tidak berhasil untuk saya, setidaknya untuk AngularJS 1.3.7 terbaru. Jadi saya menemukan solusi lain, bukan yang terbaik, tetapi jika ini penting bagi Anda itu akan berhasil. Apa yang saya lakukan adalah setiap kali saya menambahkan / mengubah / menghapus elemen, saya selalu menambahkan elemen dummy lain ke akhir daftar, karena itu karena $lastelemen juga diubah, ngRepeatFinisharahan sederhana dengan memeriksa $lastsekarang akan berfungsi. Segera setelah ngRepeatFinish dipanggil, saya menghapus item dummy. (Saya juga menyembunyikannya dengan CSS agar tidak muncul sebentar)
darklow
Retasan ini bagus tetapi tidak sempurna; setiap kali filter menendang dalam acara tersebut dikirim lima kali: - /
Sjeiti
Yang di bawah ini Mark Rajcokbekerja dengan sempurna setiap pengulangan ulang dari ng-repeat (misalnya karena perubahan model). Jadi solusi hacky ini tidak diperlukan.
Dzhu
1
@Josep Anda benar - ketika item disambungkan / tidak digeser (yaitu array dimutasi), itu tidak berfungsi. Pengujian saya sebelumnya mungkin dengan array yang telah dipindahkan (menggunakan sugarjs) karena itu bekerja setiap saat ... Terima kasih telah menunjukkan ini
Dzhu
10

Jika Anda perlu memanggil fungsi yang berbeda untuk ng-pengulangan yang berbeda pada pengontrol yang sama Anda dapat mencoba sesuatu seperti ini:

Arahan:

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

Di controller Anda, tangkap acara dengan $ pada:

$scope.$on('ngRepeatBroadcast1', function(ngRepeatFinishedEvent) {
// Do something
});

$scope.$on('ngRepeatBroadcast2', function(ngRepeatFinishedEvent) {
// Do something
});

Dalam template Anda dengan beberapa 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>
MCurbelo
sumber
5

Solusi lain akan bekerja dengan baik pada pemuatan halaman awal, tetapi memanggil $ timeout dari controller adalah satu-satunya cara untuk memastikan bahwa fungsi Anda dipanggil ketika model berubah. Berikut adalah biola yang berfungsi menggunakan $ timeout. Sebagai contoh Anda akan menjadi:

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

ngRepeat hanya akan mengevaluasi arahan ketika konten baris baru, jadi jika Anda menghapus item dari daftar Anda, onFinishRender tidak akan diaktifkan. Sebagai contoh, coba masukkan nilai filter dalam memancarkan biola ini .

John Harley
sumber
Demikian pula, test () tidak selalu disebut dalam solusi evalAsynch ketika model berubah biola
John Harley
2
apa yang ada tadi dalam $scope.$watch("ta",...?
Muhammad Adeel Zahid
4

Jika Anda tidak menolak untuk menggunakan alat peraga lingkup dolar ganda dan Anda sedang menulis arahan yang kontennya hanya pengulangan, ada solusi yang cukup sederhana (dengan asumsi Anda hanya peduli tentang render awal). Dalam fungsi tautan:

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

Ini berguna untuk kasus-kasus di mana Anda memiliki arahan yang perlu melakukan manipulasi DOM berdasarkan lebar atau tinggi anggota daftar yang diberikan (yang saya pikir adalah alasan paling mungkin seseorang akan mengajukan pertanyaan ini), tetapi itu tidak generik sebagai solusi lain yang telah diusulkan.

Titik koma
sumber
1

Solusi untuk masalah ini dengan ngRepeat yang difilter dapat dengan Mutasi peristiwa , tetapi mereka sudah usang (tanpa penggantian segera).

Lalu saya memikirkan satu lagi yang mudah:

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

Ini mengaitkan ke metode Node.prototype dari elemen induk dengan batas waktu $ satu-tik untuk melihat modifikasi berturut-turut.

Sebagian besar kerjanya benar tetapi saya mendapatkan beberapa kasus di mana altDone akan dipanggil dua kali.

Lagi ... tambahkan arahan ini ke induk dari ngRepeat.

Sjeiti
sumber
Sejauh ini ini yang terbaik, tetapi itu tidak sempurna. Misalnya saya beralih dari 70 item ke 68, tetapi tidak menyala.
Gaui
1
Apa yang bisa berfungsi adalah menambahkan appendChildjuga (karena saya hanya menggunakan insertBeforedan removeChild) dalam kode di atas.
Sjeiti
1

Sangat mudah, ini adalah bagaimana saya melakukannya.

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

CPhelefu
sumber
Kode ini tentu saja berfungsi jika Anda memiliki layanan $ blockUI Anda sendiri.
CPhelefu
0

Silakan lihat biola, http://jsfiddle.net/yNXS2/ . Karena arahan yang Anda buat tidak menciptakan ruang lingkup baru saya melanjutkan jalan.

$scope.test = function(){... membuat itu terjadi.

Rajkamal Subramanian
sumber
Biola yang salah? Sama seperti yang ada di pertanyaan.
James M
humm, apakah Anda memperbarui biola? Saat ini menunjukkan salinan kode asli saya.
PCoelho
0

Saya sangat terkejut tidak melihat solusi paling sederhana di antara jawaban atas pertanyaan ini. Apa yang ingin Anda lakukan adalah menambahkan ngInitarahan pada elemen berulang Anda (elemen dengan ngRepeatarahan) memeriksa $last(variabel khusus yang diatur dalam lingkup ngRepeatyang menunjukkan bahwa elemen yang diulang adalah yang terakhir dalam daftar). Jika $lastbenar, kami sedang merender elemen terakhir dan kami dapat memanggil fungsi yang kita inginkan.

ng-init="$last && test()"

Kode lengkap untuk markup HTML Anda adalah:

<div ng-app="testApp" ng-controller="myC">
    <p ng-repeat="t in ta" ng-init="$last && test()">{{t}}</p>
</div>

Anda tidak memerlukan kode JS tambahan di aplikasi Anda selain fungsi lingkup yang ingin Anda panggil (dalam hal ini, test) karena ngInitdisediakan oleh Angular.js. Pastikan Anda memiliki testfungsi dalam lingkup sehingga dapat diakses dari templat:

$scope.test = function test() {
    console.log("test executed");
}
Alejandro García Iglesias
sumber