Gọi một chức năng khi ng-repeat đã kết thúc


249

Những gì tôi đang cố gắng thực hiện về cơ bản là một trình xử lý "hoàn thành lặp lại kết xuất". Tôi có thể phát hiện khi hoàn thành nhưng tôi không thể tìm ra cách kích hoạt chức năng từ nó.

Kiểm tra câu đố: http://jsfiddle.net/paulocoelho/BsMqq/3/

Mã não

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>

Trả lời : Fiddle làm việc từ việc hoàn thiện: http://jsfiddle.net/paulocoelho/BsMqq/4/


Vì tò mò, mục đích của element.ready()đoạn trích là gì? Ý tôi là .. đó là một số loại plugin jQuery mà bạn có, hay nó nên được kích hoạt khi phần tử đã sẵn sàng?
gion_13

1
Người ta có thể làm điều đó bằng cách sử dụng các chỉ thị tích hợp như ng-init
semiomant 22/1/2015


bản sao của stackoverflow.com/questions/13471129/ , xem câu trả lời của tôi ở đó
bresleveloper

Câu trả lời:


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

Lưu ý rằng tôi đã không sử dụng .ready()mà chỉ gói nó trong một $timeout. $timeoutđảm bảo rằng nó được thực thi khi các phần tử lặp lại ng thực sự hoàn thành kết xuất (bởi vì phần tử $timeoutsẽ thực thi ở cuối chu kỳ phân loại hiện tại - và nó cũng sẽ gọi $applynội bộ, không giống nhưsetTimeout ). Vì vậy, sau khi ng-repeatkết thúc, chúng tôi sử dụng $emitđể phát ra một sự kiện đến phạm vi bên ngoài (phạm vi anh chị em và phụ huynh).

Và sau đó trong bộ điều khiển của bạn, bạn có thể bắt nó với $on:

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

Với html trông giống như thế này:

<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
    <div>{{item.name}}}<div>
</div>

10
+1, nhưng tôi sẽ sử dụng $ eval trước khi sử dụng một sự kiện - ít khớp nối hơn. Xem câu trả lời của tôi để biết thêm chi tiết.
Mark Rajcok

1
@PigalevPavel Tôi nghĩ bạn đang nhầm lẫn $timeout(về cơ bản là setTimeout+ Angular $scope.$apply()) với setInterval. $timeoutsẽ chỉ thực hiện một lần cho mỗi ifđiều kiện và đó sẽ là vào đầu $digestchu kỳ tiếp theo . Để biết thêm thông tin về thời gian chờ JavaScript, hãy xem: ejohn.org/blog/how-javascript-timers-work
nguyên tắc ba chiều

1
@finishingmove Có lẽ tôi không hiểu điều gì đó :) Nhưng hãy xem tôi có lặp lại trong một lần lặp lại khác. Và tôi muốn biết chắc chắn khi tất cả chúng kết thúc. Khi tôi sử dụng tập lệnh của bạn với $ timeout chỉ để cha mẹ lặp lại, tất cả đều hoạt động tốt. Nhưng nếu tôi không sử dụng $ timeout, tôi sẽ nhận được phản hồi trước khi việc lặp lại của trẻ em kết thúc. Tôi muốn biết tại sao? Và tôi có thể chắc chắn rằng nếu tôi sử dụng tập lệnh của bạn với $ timeout cho cha-ng lặp lại thì tôi sẽ luôn nhận được phản hồi khi tất cả các lần lặp lại kết thúc?
Pigalev Pavel

1
Tại sao bạn lại sử dụng một cái gì đó như setTimeout()với độ trễ 0 là một câu hỏi khác mặc dù tôi phải nói rằng tôi chưa bao giờ bắt gặp một đặc điểm kỹ thuật thực tế cho hàng đợi sự kiện của trình duyệt ở bất cứ đâu, chỉ là những gì ngụ ý bởi tính đơn luồng và sự tồn tại của setTimeout().
rakslice

1
Cảm ơn câu trả lời này. Tôi có một câu hỏi, tại sao chúng ta cần chỉ định on-finish-render="ngRepeatFinished"? Khi tôi gán on-finish-render="helloworld", nó hoạt động như nhau.
Arnaud

89

Sử dụng $ evalAsync nếu bạn muốn cuộc gọi lại của mình (nghĩa là test ()) được thực thi sau khi DOM được xây dựng, nhưng trước khi trình duyệt kết xuất lại. Điều này sẽ ngăn ngừa nhấp nháy - ref .

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

Fiddle .

Nếu bạn thực sự muốn gọi lại cuộc gọi của mình sau khi kết xuất, hãy sử dụng $ timeout:

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

Tôi thích $ eval thay vì một sự kiện. Với một sự kiện, chúng ta cần biết tên của sự kiện và thêm mã vào bộ điều khiển của chúng ta cho sự kiện đó. Với $ eval, có ít khớp nối giữa bộ điều khiển và lệnh.


Kiểm tra để $lastlàm gì? Không thể tìm thấy nó trong các tài liệu. Nó đã được gỡ bỏ?
Erik Aigner

2
@ErikAigner, $lastđược xác định trên phạm vi nếu một ng-repeathoạt động trên phần tử.
Mark Rajcok

Có vẻ như Fiddle của bạn đang tham gia một cách không cần thiết $timeout.
bbodenmiller

1
Tuyệt quá. Đây phải là câu trả lời được chấp nhận, khi thấy rằng phát / phát chi phí nhiều hơn từ quan điểm hiệu suất.
Florin Vistig

29

Các câu trả lời đã được đưa ra cho đến nay sẽ chỉ hoạt động ngay lần đầu tiên ng-repeatđược đưa ra , nhưng nếu bạn có động ng-repeat, nghĩa là bạn sẽ thêm / xóa / lọc các mục và bạn cần được thông báo mỗi khi ng-repeatđược kết xuất, những giải pháp đó sẽ không làm việc cho bạn.

Vì vậy, nếu bạn cần được thông báo MERYI LẦN rằngng-repeat lần đầu tiên được kết xuất lại và không chỉ lần đầu tiên, tôi đã tìm ra cách để làm điều đó, nó khá 'hacky', nhưng nó sẽ hoạt động tốt nếu bạn biết bạn là ai đang làm. Sử dụng điều này $filtertrong của bạn ng-repeat trước khi sử dụng bất kỳ khác$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;
    };
})

Đây sẽ $emitlà một sự kiện được gọi ngRepeatFinishedmỗi khi nó ng-repeatđược kết xuất.

Làm thế nào để sử dụng nó:

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

Bộ ngRepeatFinishlọc cần được áp dụng trực tiếp cho một Arrayhoặc một Objectđịnh nghĩa trong của bạn $scope, bạn có thể áp dụng các bộ lọc khác sau.

Làm thế nào KHÔNG sử dụng nó:

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

Không áp dụng các bộ lọc khác trước và sau đó áp dụng ngRepeatFinishbộ lọc.

Khi nào tôi nên sử dụng cái này?

Nếu bạn muốn áp dụng các kiểu css nhất định vào DOM sau khi danh sách kết thúc hiển thị, bởi vì bạn cần phải tính đến kích thước mới của các thành phần DOM đã được kết xuất lại bởi ng-repeat. (BTW: những loại hoạt động đó nên được thực hiện trong một chỉ thị)

Những gì KHÔNG nên làm trong chức năng xử lý ngRepeatFinishedsự kiện:

  • Không thực hiện $scope.$applychức năng đó hoặc bạn sẽ đặt Angular vào một vòng lặp vô tận mà Angular sẽ không thể phát hiện ra.

  • Không sử dụng nó để thực hiện các thay đổi trong các $scopethuộc tính, vì những thay đổi đó sẽ không được phản ánh trong chế độ xem của bạn cho đến $digestvòng lặp tiếp theo và vì bạn không thể thực hiện nên $scope.$applychúng sẽ không được sử dụng.

"Nhưng các bộ lọc không có nghĩa là được sử dụng như vậy !!"

Không, họ không, đây là một hack, nếu bạn không thích nó không sử dụng nó. Nếu bạn biết một cách tốt hơn để hoàn thành điều tương tự, xin vui lòng cho tôi biết điều đó.

Tóm tắt

Đây là một hack và sử dụng sai cách rất nguy hiểm, chỉ sử dụng nó để áp dụng các kiểu sau khi ng-repeatkết xuất xong và bạn không nên có bất kỳ vấn đề nào.


Thx 4 tiền boa. Dù sao, một plunkr với một ví dụ làm việc sẽ được đánh giá cao.
holographix

2
Thật không may, điều này không làm việc cho tôi, ít nhất là cho AngularJS 1.3.7 mới nhất. Vì vậy, tôi đã phát hiện ra một cách giải quyết khác, không phải là cách tốt nhất, nhưng nếu điều này là cần thiết cho bạn thì nó sẽ hoạt động. Những gì tôi đã làm là bất cứ khi nào tôi thêm / thay đổi / xóa các phần tử, tôi luôn thêm một phần tử giả khác vào cuối danh sách, do đó $lastphần tử cũng được thay đổi, ngRepeatFinishchỉ thị đơn giản bằng cách kiểm tra $lastngay bây giờ sẽ hoạt động. Ngay khi ngRepeatFinish được gọi là tôi xóa mục giả. (Tôi cũng ẩn nó bằng CSS để nó không xuất hiện nhanh)
darklow 16/12/14

Hack này là tốt đẹp nhưng không hoàn hảo; mỗi lần bộ lọc khởi động trong sự kiện được gửi đi năm lần: - /
Sjeiti

Cái dưới đây Mark Rajcokhoạt động hoàn hảo mỗi lần kết xuất lại của ng-repeat (ví dụ vì thay đổi mô hình). Vì vậy, giải pháp hacky này là không cần thiết.
Dzhu

1
@Josep bạn đúng - khi các mục được ghép / không dịch chuyển (tức là mảng bị đột biến), nó không hoạt động. Thử nghiệm trước đây của tôi có lẽ là với mảng đã được gán lại (sử dụng sugarjs) do đó nó hoạt động mọi lúc ... Cảm ơn bạn đã chỉ ra điều này
Dzhu

10

Nếu bạn cần gọi các hàm khác nhau cho các lần lặp ng khác nhau trên cùng một bộ điều khiển, bạn có thể thử một cái gì đó như thế này:

Chỉ thị:

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

Trong bộ điều khiển của bạn, bắt các sự kiện với $ on:

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

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

Trong mẫu của bạn có nhiều 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

Các giải pháp khác sẽ hoạt động tốt khi tải trang ban đầu, nhưng gọi $ timeout từ bộ điều khiển là cách duy nhất để đảm bảo rằng chức năng của bạn được gọi khi mô hình thay đổi. Đây là một fiddle làm việc sử dụng $ timeout. Ví dụ của bạn sẽ là:

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

ngRepeat sẽ chỉ đánh giá một lệnh khi nội dung hàng mới, vì vậy nếu bạn xóa các mục khỏi danh sách của mình, onFinishRender sẽ không kích hoạt. Ví dụ: thử nhập các giá trị bộ lọc trong các câu đố phát ra .


Tương tự, test () không phải lúc nào cũng được gọi trong giải pháp evalAsynch khi mô hình thay đổi bí ẩn
John Harley

2
những gì là tatrong $scope.$watch("ta",...?
Muhammad Adeel Zahid

4

Nếu bạn không phản đối việc sử dụng đạo cụ phạm vi hai đô la và bạn đang viết một lệnh mà nội dung duy nhất là lặp lại, thì có một giải pháp khá đơn giản (giả sử bạn chỉ quan tâm đến kết xuất ban đầu). Trong chức năng liên kết:

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

Điều này hữu ích cho các trường hợp bạn có một lệnh cần thực hiện thao tác DOM dựa trên chiều rộng hoặc chiều cao của các thành viên trong danh sách được kết xuất (mà tôi nghĩ là lý do rất có thể người ta sẽ hỏi câu hỏi này), nhưng nó không phải là chung chung như các giải pháp khác đã được đề xuất.


1

Một giải pháp cho vấn đề này với ngRepeat được lọc có thể là với các sự kiện Đột biến , nhưng chúng không được chấp nhận (không có sự thay thế ngay lập tức).

Sau đó, tôi nghĩ đến một điều dễ dàng khác:

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

Điều này nối vào các phương thức Node.prototype của phần tử cha mẹ với thời gian chờ $ một lần để theo dõi các sửa đổi liên tiếp.

Nó hoạt động chủ yếu là đúng nhưng tôi đã nhận được một số trường hợp trong đó altDone sẽ được gọi hai lần.

Một lần nữa ... thêm chỉ thị này vào cha mẹ của ngRepeat.


Điều này hoạt động tốt nhất cho đến nay, nhưng nó không hoàn hảo. Ví dụ: tôi đi từ 70 mục đến 68, nhưng nó không cháy.
Gaui

1
Những gì có thể làm việc là thêm vào appendChild(vì tôi chỉ sử dụng insertBeforeremoveChild) trong đoạn mã trên.
Sjeiti

1

Rất dễ dàng, đây là cách tôi đã làm nó.

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


Mã này hoạt động tất nhiên nếu bạn có dịch vụ $ blockUI của riêng bạn.
CPhelefu

0

Xin hãy xem fiddle, http://jsfiddle.net/yNXS2/ . Vì lệnh bạn tạo không tạo ra một phạm vi mới, tôi tiếp tục theo cách này.

$scope.test = function(){... làm điều đó xảy ra


Sai lầm? Giống như một trong câu hỏi.
James M

humm, bạn đã cập nhật các fiddle? Nó hiện đang hiển thị một bản sao của mã gốc của tôi.
PCoelho

0

Tôi rất ngạc nhiên khi không thấy giải pháp đơn giản nhất trong số các câu trả lời cho câu hỏi này. Những gì bạn muốn làm là thêm một lệnh ngInitvào phần tử lặp lại của bạn (phần tử có lệnh ngRepeat) kiểm tra $last(một biến đặc biệt được đặt trong phạm vi ngRepeatcho biết phần tử lặp lại là phần tử cuối cùng trong danh sách). Nếu $lastđúng, chúng ta sẽ hiển thị phần tử cuối cùng và chúng ta có thể gọi hàm chúng ta muốn.

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

Mã hoàn chỉnh cho đánh dấu HTML của bạn sẽ là:

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

Bạn không cần thêm bất kỳ mã JS nào trong ứng dụng của mình ngoài chức năng phạm vi bạn muốn gọi (trong trường hợp này test) , ngInitdo Angular.js cung cấp. Chỉ cần đảm bảo có testchức năng của bạn trong phạm vi để có thể truy cập nó từ mẫu:

$scope.test = function test() {
    console.log("test executed");
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.