การเรียกฟังก์ชันเมื่อ ng-repeat เสร็จสิ้น


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
semiomant


ซ้ำกับ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() ตรวจสอบให้แน่ใจว่ามีการดำเนินการเมื่อองค์ประกอบที่ทำซ้ำ ng มีการเรนเดอร์ที่เสร็จสมบูรณ์แล้วจริงๆ (เนื่องจากจะดำเนินการเมื่อสิ้นสุดรอบการแยกย่อยปัจจุบัน - และจะเรียกภายในด้วยซึ่งแตกต่างจาก ) ดังนั้นหลังจากที่เสร็จสิ้นเราจะใช้ในการส่งเหตุการณ์ไปยังขอบเขตด้านนอก (พี่น้องและขอบเขตผู้ปกครอง)$timeout$timeout$timeout$applysetTimeoutng-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 ก่อนที่จะใช้กิจกรรม - มีเพศสัมพันธ์น้อยลง ดูคำตอบของฉันสำหรับรายละเอียดเพิ่มเติม
Mark Rajcok

1
@PigalevPavel ฉันคิดว่าคุณกำลังสับสน$timeout(ซึ่งเป็นพื้นsetTimeout+ เชิงมุมของ$scope.$apply()) setIntervalด้วย $timeoutจะดำเนินการเพียงครั้งเดียวตามifเงื่อนไขและจะอยู่ที่จุดเริ่มต้นของ$digestรอบถัดไป สำหรับข้อมูลเพิ่มเติมเกี่ยวกับการหมดเวลา JavaScript โปรดดู: ejohn.org/blog/how-javascript-timers-work
holographic-หลักการ

1
@finishingmove บางทีฉันอาจไม่เข้าใจบางสิ่งบางอย่าง :) แต่ดูฉันมี ng-repeat ในอีก ng-repeat และฉันต้องการที่จะรู้ว่าเมื่อพวกเขาทั้งหมดเสร็จแล้ว เมื่อฉันใช้สคริปต์ของคุณด้วย $ timeout สำหรับผู้ปกครอง ng-repeat ทุกอย่างก็ใช้ได้ดี แต่ถ้าฉันไม่ได้ใช้ $ timeout ฉันจะได้รับคำตอบก่อนที่เด็กจะทำซ้ำเสร็จ ฉันอยากรู้ว่าทำไม และฉันสามารถมั่นใจได้ว่าถ้าฉันใช้สคริปต์ของคุณด้วย $ timeout สำหรับ parent ng-repeat ฉันจะได้รับการตอบกลับเมื่อ ng-repeats ทั้งหมดเสร็จสิ้นแล้วหรือไม่
Pigalev Pavel

1
ทำไมคุณจะใช้สิ่งที่ต้องการsetTimeout()ด้วย 0 ล่าช้าเป็นคำถามอื่นแต่ผมต้องบอกว่าผมไม่เคยเจอสเปคที่แท้จริงสำหรับทุกที่คิวงานเบราว์เซอร์เพียงแค่สิ่งที่โดยนัยเดียว threadedness setTimeout()และการดำรงอยู่ของ
rakslice

1
ขอบคุณสำหรับคำตอบนี้ ฉันมีคำถามทำไมเราต้องกำหนดon-finish-render="ngRepeatFinished"? เมื่อฉันกำหนดon-finish-render="helloworld"มันจะทำงานเหมือนเดิม
Arnaud

89

ใช้$ evalAsyncหากคุณต้องการให้ callback (เช่น test ()) ถูกเรียกใช้หลังจากสร้าง DOM แต่ก่อนที่เบราว์เซอร์จะแสดงผล ซึ่งจะช่วยป้องกันการสั่นไหว - เตะ

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

ซอซอ

หากคุณต้องการโทรกลับหาคุณหลังจากการแสดงผลจริงๆให้ใช้ $ timeout:

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

ฉันชอบ $ eval แทนที่จะเป็นกิจกรรม เมื่อมีเหตุการณ์เราต้องทราบชื่อของเหตุการณ์และเพิ่มรหัสให้กับตัวควบคุมของเราสำหรับเหตุการณ์นั้น ด้วย $ eval จะมีการเชื่อมต่อระหว่างคอนโทรลเลอร์กับคำสั่งน้อยกว่า


ตรวจสอบ$lastทำอะไร? ไม่พบในเอกสาร มันถูกลบ?
Erik Aigner

2
@ErikAigner $lastถูกกำหนดไว้ในขอบเขตหากมีการng-repeatใช้งานในองค์ประกอบ
Mark Rajcok

$timeoutดูเหมือนว่าซอของคุณใช้เวลาในการที่ไม่จำเป็น
bbodenmiller

1
ยิ่งใหญ่ นี่ควรเป็นคำตอบที่ได้รับการยอมรับเนื่องจากเห็นว่าการปล่อย / การออกอากาศนั้นมีค่าใช้จ่ายมากขึ้นจากมุมมองประสิทธิภาพ
Florin Vistig

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การเรนเดอร์เสร็จสิ้นและคุณไม่ควรมีปัญหาใด ๆ


ขอบคุณ 4 เคล็ดลับ อย่างไรก็ตามเสียงพึมพำกับตัวอย่างการทำงานจะได้รับการชื่นชมมาก
holographix

2
น่าเสียดายที่นี่ไม่ได้ผลสำหรับฉันอย่างน้อย AngularJS 1.3.7 ล่าสุด ดังนั้นฉันจึงค้นพบวิธีแก้ปัญหาอื่นไม่ใช่วิธีที่อร่อยที่สุด แต่ถ้านี่เป็นสิ่งจำเป็นสำหรับคุณมันจะใช้ได้ สิ่งที่ฉันทำคือเมื่อใดก็ตามที่ฉันเพิ่ม / เปลี่ยน / ลบองค์ประกอบฉันมักจะเพิ่มองค์ประกอบหุ่นที่ส่วนท้ายของรายการดังนั้นเนื่องจาก$lastองค์ประกอบมีการเปลี่ยนแปลงngRepeatFinishคำสั่งง่ายๆโดยการตรวจสอบ$lastตอนนี้จะทำงาน ทันทีที่ ngRepeatFinish ถูกเรียกว่าฉันลบรายการหุ่น (ฉันยังซ่อนมันด้วย CSS ดังนั้นจึงไม่ปรากฏขึ้นชั่วครู่)
darklow

แฮ็คนี้ดี แต่ไม่สมบูรณ์ ทุกครั้งที่ตัวกรองเตะในเหตุการณ์ถูกส่งห้าครั้ง: - /
Sjeiti

ด้านล่างโดยMark Rajcokทำงานได้อย่างสมบูรณ์แบบทุกครั้งที่แสดงผลของ ng-repeat (เช่นเนื่องจากการเปลี่ยนแปลงรูปแบบ) ดังนั้นจึงไม่จำเป็นต้องใช้โซลูชันแฮ็ค
Dzhu

1
@ โจเซฟคุณพูดถูก - เมื่อรายการถูกแต่งงาน / ไม่เปลี่ยน (นั่นคืออาร์เรย์กำลังกลายพันธุ์) มันไม่ทำงาน การทดสอบก่อนหน้าของฉันอาจเป็นกับอาเรย์ที่ได้รับการกำหนดใหม่ (ใช้ Sugarjs) ดังนั้นมันจึงทำงานได้ทุกครั้ง ... ขอบคุณสำหรับการชี้สิ่งนี้ออกมา
Dzhu

10

หากคุณต้องการเรียกใช้ฟังก์ชันที่แตกต่างกันสำหรับ ng-repeats ที่แตกต่างกันบนคอนโทรลเลอร์เดียวกันคุณสามารถลองดังนี้:

คำสั่ง:

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 เมื่อแบบจำลองมีการเปลี่ยนแปลงซอ
John Harley

2
สิ่งที่อยู่taใน$scope.$watch("ta",...?
มูฮัมหมัด Adeel Zahid

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 ขององค์ประกอบหลักที่มีการหมดเวลา $ หนึ่งขีดเพื่อดูการปรับเปลี่ยนอย่างต่อเนื่อง

มันทำงานได้ถูกต้องส่วนใหญ่ แต่ฉันได้รับบางกรณีที่ altDone จะถูกเรียกสองครั้ง

อีกครั้ง ... เพิ่มคำสั่งนี้ไปยังผู้ปกครองของ ngRepeat


มันใช้งานได้ดีที่สุด แต่ก็ไม่สมบูรณ์แบบ ตัวอย่างเช่นฉันไปจาก 70 รายการถึง 68 แต่มันไม่ได้ยิง
Gaui

1
สิ่งที่สามารถทำงานได้คือการเพิ่มappendChildเช่นกัน (ตั้งแต่ฉันใช้เพียงinsertBeforeและremoveChild) ในรหัสข้างต้น
Sjeiti

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(){... ทำให้เกิดขึ้น


ซอผิดหรือเปล่า? เช่นเดียวกับคำถาม
James M

humm คุณได้อัพเดตซอหรือยัง? มันกำลังแสดงสำเนาของรหัสต้นฉบับของฉัน
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");
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.