ng-repeatが終了したときに関数を呼び出す


249

私が実装しようとしているのは、基本的には「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>

回答:finishmoveからのフィドルの作業:http : //jsfiddle.net/paulocoelho/BsMqq/4/


好奇心から、element.ready()スニペットの目的は何ですか?つまり、あなたが持っているある種のjQueryプラグインですか、それとも要素の準備ができたときにトリガーする必要がありますか?
gion_13 14

1
ng-initなどの組み込みディレクティブを使用してそれを行うことができます
セミオマント2015年


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$timeoutngが繰り返された要素のレンダリングが本当に終了したときに実行されることを確認します($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を使用します-カップリングを減らします。詳細については、私の回答を参照してください。
Mark Rajcok 2013年

1
@PigalevPavel私はあなたが$timeout(基本的にsetTimeout+ Angularの$scope.$apply())と混同していると思いますsetInterval。条件$timeoutごとに1回だけ実行ifされ、それは次の$digestサイクルの開始時に行われます。JavaScriptタイムアウトの詳細については、ejohn.org
holographic-principle

1
@finishingmove多分私は何かを理解できません:)しかし、私が別のng-repeatでng-repeatを持っているのを見てください。そして、それらすべてがいつ完成するかを確実に知りたいのです。親のng-repeatのために$ timeoutでスクリプトを使用すると、すべて正常に動作します。しかし、$ timeoutを使用しない場合、子供のng-repeatsが完了する前に応答が返されます。なぜか知りたい?また、親ng-repeatに$ timeoutを使用してスクリプトを使用すると、すべてのng-repeatが終了したときに常に応答が返されることを確認できますか?
Pigalev Pavel 2013

1
setTimeout()遅延0のようなものを使用する理由は別の質問ですが、ブラウザーのイベントキューの実際の仕様はどこにも見かけたことはありませんsetTimeout()
rakslice 2013

1
この回答をありがとう。質問があります。なぜ割り当てる必要があるのon-finish-render="ngRepeatFinished"ですか?を割り当ててもon-finish-render="helloworld"、同じように機能します。
アルノー、

89

DOMが構築された後、ブラウザーがレンダリングされる前にコールバック(つまり、test())を実行する場合は、$ evalAsyncを使用します。これはちらつきを防ぎます- 参照

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

フィドル

レンダリング後に本当にコールバックを呼び出したい場合は、$ timeoutを使用します。

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

私はイベントではなく$ evalを好みます。イベントの場合、イベントの名前を知り、そのイベントのコントローラーにコードを追加する必要があります。$ evalを使用すると、コントローラーとディレクティブの間の結合が少なくなります。


チェックの目的$lastは何ですか?ドキュメントでそれを見つけることができません。削除されましたか?
Erik Aigner 2013年

2
@ErikAigner $lastng-repeat、要素でアクティブな場合、スコープで定義されます。
Mark Rajcok 2013年

フィドルが不必要に取り込んでいるよう$timeoutです。
bbodenmiller 14

1
すごい。パフォーマンスの観点から見ると、発行/ブロードキャストのコストが高くなることを考えると、これは受け入れられる答えになるはずです。
Florin Vistig 16

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フィルターを適用しないでください。

これはいつ使うべきですか?

リストによってレンダリングされたDOM要素の新しい寸法を考慮する必要があるため、リストのレンダリングが完了した後、特定のcssスタイルをDOMに適用する場合 ng-repeat。(ところで:これらの種類の操作はディレクティブ内で行う必要があります)

を処理する関数ですべきでないこと ngRepeatFinishedイベント:

  • 実行しないでください $scope.$applyその関数でを、Angularが検出できない無限ループにAngularが配置されます。

  • $scopeプロパティの変更には使用しないでください。これらの変更は次の$digestループまでビューに反映されず、また、$scope.$applyそれらをても意味がありません。

「しかし、フィルターはそのように使用することを意図していません!!」

いいえ、そうではありません。これはハックです。気に入らない場合は使用しないでください。同じことを達成するためのより良い方法を知っているなら、私に知らせてください。

まとめ

これはハックであり、間違った方法で使用するのは危険ng-repeatです。レンダリングが完了した後でスタイルを適用する場合にのみ使用してください。問題は発生しません。


Thx 4チップ。とにかく、実用的な例のあるプランカーは大歓迎です。
holographix

2
残念ながら、これは少なくとも最新のAngularJS 1.3.7では機能しません。だから私は別の回避策を発見しましたが、これは最も良いものではありませんが、これがあなたにとって不可欠であるならそれはうまくいきます。私がしたことは、要素を追加/変更/削除するたびに、常にリストの最後に別のダミー要素を追加します。したがって、$last要素も変更されるため、今ngRepeatFinishチェックする$lastことによる単純なディレクティブが機能します。ngRepeatFinishが呼び出されたらすぐに、ダミーのアイテムを削除します。(CSSで非表示にして、短時間表示されないようにします)
darklow 2014

このハックは素晴らしいですが、完璧ではありません。イベントでフィルターが
有効になるたびに

以下のものMark Rajcokは、ng-repeatのすべての再レンダリングを完全に機能させます(たとえば、モデルの変更のため)。したがって、このハッキーなソリューションは必要ありません。
Dzhu

1
あなたが正しい@Josep-アイテムがスプライス/シフトされていない場合(つまり、配列が変更されている場合)は機能しません。私の以前のテストは、おそらく(sugarjsを使用して)再割り当てされた配列を使用していたため、毎回動作しました...これを指摘してくれてありがとう
Dzhu

10

同じコントローラーで異なるng-repeatに対して異なる関数を呼び出す必要がある場合は、次のようなことを試すことができます。

ディレクティブ:

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>

5

その他のソリューションは、初期ページの読み込み時に正常に機能しますが、モデルが変更されたときに関数が確実に呼び出されるようにするには、コントローラーから$ timeoutを呼び出すことが唯一の方法です。$ timeoutを使用する作業フィドルは次のとおりです。あなたの例では、それは:

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

ngRepeatは行のコンテンツが新しい場合のみディレクティブを評価するため、リストからアイテムを削除した場合、onFinishRenderは発生しません。たとえば、これらのバイオリンの放出にフィルター値を入力してみてください。


同様に、モデルがフィドルを
ジョンハーレー

2
何がta入っ$scope.$watch("ta",...てるの?
Muhammad Adeel Zahid 2014

4

2ドルスコープの小道具を使用するのが嫌ではなく、コンテンツがリピートのみであるディレクティブを記述している場合は、非常に単純な解決策があります(最初のレンダリングのみに関心があると仮定した場合)。リンク機能で:

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

これは、レンダリングされたリストのメンバーの幅または高さに基づいてDOM操作を行う必要のあるディレクティブがある場合に役立ちます(これが、この質問をする理由として最も可能性が高いと思います)が、一般的ではありません。提案されている他のソリューションとして。


1

フィルタリングされたngRepeatに関するこの問題の解決策は、Mutationである可能性がありますイベントであったが、非推奨(即時の置き換えなし)です。

それから私は別の簡単なものを考えました:

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

これは、1ティックの$ timeoutを使用して親要素のNode.prototypeメソッドにフックし、連続的な変更を監視します。

ほぼ正しく動作しますが、altDoneが2回呼び出される場合があります。

繰り返しますが、このディレクティブをngRepeatのに追加します。


これはこれまでのところ最もよく機能しますが、完全ではありません。たとえば、70アイテムから68アイテムになりましたが、発砲しません。
ガウイ

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 2016年

0

フィドルhttp://jsfiddle.net/yNXS2/をご覧ください。あなたが作成したディレクティブは新しいスコープを作成しなかったので、私は方法を続けました。

$scope.test = function(){... それを実現しました。


フィドルが間違っていますか?問題の質問と同じです。
ジェームズM

うーん、フィドルを更新しましたか?現在、私の元のコードのコピーが表示されています。
PCoelho

0

この質問に対する回答の中で最も簡単な解決策を見つけられなかったことに非常に驚きました。やりたいことはngInit、繰り返し要素(ngRepeatディレクティブのある要素)にディレクティブを追加して$lastngRepeat繰り返し要素がリストの最後であることを示すスコープ内に設定された特殊変数)をチェックすることです。$lasttrueの場合、最後の要素をレンダリングしており、必要な関数を呼び出すことができます。

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>

Angular.jsによって提供されるtestため、呼び出すスコープ関数(この場合は)以外に、アプリに追加のJSコードは必要ありませんngInittestテンプレートからアクセスできるように、スコープに関数があることを確認してください。

$scope.test = function test() {
    console.log("test executed");
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.