Top / AngularJS / TIPS集 / ng-repeatについて

AngularJSでよく出てくる、ng-repeat について。このdirective は

<ul>
  <li ng-repeat="instance in ctrl.friends ">{{instance.name}} </li>
</ul>

など、Controller がもつ配列を画面に表示するのによく使います。

やってみる

ソース全体

https://github.com/masatomix/ng-repeat-sample-web/releases/tag/0.0.2 からダウンロード可能です。

画面HTML

<table>
    <tr>
        <td>index</td>
        <td>id</td>
        <td>name</td>
        <td>age</td>
    </tr>
    <tr ng-repeat="instance in aboutCtrl.friends" >
        <td>{{$index}}</td>
        <td><display-value value='instance.id'/></td>
        <td><display-value value='instance.name'/></td>
        <td><display-value value='instance.age'/></td>
    </tr>
</table>

こんな感じです。

<tr ng-repeat="instance in aboutCtrl.friends" >

上記のように ng-repeatが使われています。aboutCtrl が保持する friends 配列をぐるぐると table に表示していきます。

aboutCtrl Controller

適当に割愛してますが、だいたい下記のような感じです。

angular.module('ngRepeatSampleWeb')
    .controller('AboutCtrl', function () {
        var me = this;
        me.init = function () {
            me.friends = [
                {name: 'John 25', age: 25, gender: 'boy', id: 5},
                {name: 'Jessie 30', age: 30, gender: 'girl', id: 4},
                {name: 'Johanna 28', age: 28, gender: 'girl', id: 3},
                {name: 'Joy 15', age: 15, gender: 'girl', id: 2},
                {name: 'Mary 28', age: 28, gender: 'girl', id: 6},
                {name: 'Peter 95', age: 95, gender: 'boy', id: 7},
                {name: 'Sebastian 50', age: 50, gender: 'boy', id: 1},
                {name: 'Erika 27', age: 27, gender: 'girl', id: 8},
                {name: 'Patrick 40', age: 40, gender: 'boy', id: 10},
                {name: 'Samantha 60', age: 60, gender: 'girl', id: 9}
            ];
        };
        me.init();
    });

配列オブジェクトを作って、friends フィールドに配列をセットしています。

Directive

画面HTML上に、個々のデータを表示するDirective:

<display-value value='instance.age'/>

が使われていますが、中身は下記のような感じです。

angular.module('ngRepeatSampleWeb')
   .directive('displayValue', function () {
       return {
           restrict: 'E',
           template: '[{{$ctrl.value}}]',
           scope:{},
           bindToController: {
               value: "="
           },
           controller: function () {},
           controllerAs: "$ctrl"
       };
   });

もらったデータにカッコをつけて、表示しているだけですね。

実行結果

実行してみましょう。コマンドは

npm install && bower install
grunt serve

などと叩けばOKだと思いますが、実行結果として

init.png

と、Controllerがもつデータが表示されました。

表示データを入れ替えてみる。

さてソース全体を見ると分かりますが、テーブル上部にはボタンがついていて、

<div>
  <button ng-click="aboutCtrl.search()">search</button>
  <button ng-click="aboutCtrl.init()">init</button>
</div>

とControllerのメソッドを呼び出すようになっています。Controllerに定義されたそれぞれのメソッド、search/init は以下のように定義されています。

// 初期化
me.init = function () {
     me.friends = [
         {name: 'John 25', age: 25, gender: 'boy', id: 5},
         {name: 'Jessie 30', age: 30, gender: 'girl', id: 4},
         {name: 'Johanna 28', age: 28, gender: 'girl', id: 3},
         {name: 'Joy 15', age: 15, gender: 'girl', id: 2},
         {name: 'Mary 28', age: 28, gender: 'girl', id: 6},
         {name: 'Peter 95', age: 95, gender: 'boy', id: 7},
         {name: 'Sebastian 50', age: 50, gender: 'boy', id: 1},
         {name: 'Erika 27', age: 27, gender: 'girl', id: 8},
         {name: 'Patrick 40', age: 40, gender: 'boy', id: 10},
         {name: 'Samantha 60', age: 60, gender: 'girl', id: 9}
     ];
 };

こちらは画面表示時にもよばれていました。friendsフィールドを配列で初期化しています。

// 中身入れ替え。
me.search = function () {
    me.friends = [
        {name: 'Peter 95', age: 95, gender: 'boy', id: 7},
        {name: 'Erika 27', age: 27, gender: 'girl', id: 8}
    ];
};

こちらも、friends フィールドの配列オブジェクトを入れ替えています。データ的には初期化に使ったオブジェクトとおなじプロパティ値を持つデータですが、オブジェクトなのであくまで別インスタンスであることにご注意です。

画面上のボタンを実行してみると、searchやinitをボタンを押すたびに、table上のデータが入れ替わるのが分かると思います。またこのソースは ngAnimate というangularJSのモジュールを適用しているため、フェードアウトしていくアニメーションがかかっていることが分かると思います。

ani01.gif

アニメーションがヘン

さてココからが本題です。このフェードアウトするアニメーションですが、searchで一部のデータに表示を絞ろうとすると、一瞬データが2行増えて、そこから表示が絞られていきます。 また、2行の状態でinitを押すと、2行プラス初期状態(結果一瞬2行多い) になってから元に戻ったり、なんかヘンです。

想定としては、初期状態からsearchを押すと、該当行だけが残されたまま他がフェードアウトして消えてくれたらいいな、、と思いますよね。

ng-repeatのオブジェクトの管理方法について。

実は AngularJS は ng-repeat で画面上に 配列を展開する際、各行にIDを振って管理をしているようです。 そのIDは未指定時では、オブジェクトのハッシュ値*1 が使われるようです。

そのIDですが、今回のように表示に使っている配列やコレクションに追加・変更・削除があった場合、

  • 追加されたオブジェクトのIDと、元配列上のオブジェクトたちのIDを比較し、同じIDのオブジェクトがあればそのオブジェクトの画面上データを更新しにいく
  • 追加されたオブジェクトのIDが、元配列上になかった場合は、画面上も新規で挿入される
  • 新しい配列上に存在しないオブジェクトは、画面上から削除される

といった動きをするようですね。要するに行をIDで管理してて、おなじIDの行に更新をかけにいくわけです。

さっきのアニメーションがヘンという話は init/searchメソッドで入れ替えられるオブジェクトは、 プロパティ値はおなじだけどあくまで別オブジェクトであるため、おなじIDのオブジェクトが元配列に存在しない結果、挿入 & 削除 ということがおこり、増えたり減ったりヘンな動きをしたわけですね。

実際、おなじオブジェクトを使うように、下記のように変えてみたところ、

var v7 ={name: 'Peter 95', age: 95, gender: 'boy', id: 7};
var v8 = {name: 'Erika 27', age: 27, gender: 'girl', id: 8};

me.init = function () {
    me.friends = [
        {name: 'John 25', age: 25, gender: 'boy', id: 5},
        {name: 'Jessie 30', age: 30, gender: 'girl', id: 4},
        {name: 'Johanna 28', age: 28, gender: 'girl', id: 3},
        {name: 'Joy 15', age: 15, gender: 'girl', id: 2},
        {name: 'Mary 28', age: 28, gender: 'girl', id: 6},
        v7,
        {name: 'Sebastian 50', age: 50, gender: 'boy', id: 1},
        v8,
        {name: 'Patrick 40', age: 40, gender: 'boy', id: 10},
        {name: 'Samantha 60', age: 60, gender: 'girl', id: 9}
    ];
};

// 中身入れ替え。
me.search = function () {
    me.friends = [
        v7,  // おなじオブジェクト
        v8  // おなじオブジェクト
    ];
};

該当行だけが残されたまま、残りがフェードアウトしていく想定通りのアニメーションが確認出来ました。適切に、おなじ行の更新とオブジェクトの削除だけが発生するようになったからですね。AngularJS、かしこいですねー。

ani02.gif

track by xx のはなし

さてこのオブジェクトのハッシュ値を用いてDOMをトラッキング(追跡)する仕組みですが、上記のようにおなじ参照を用いることで、想定通りの動きが実現できました。 しかし、たとえばRESTの戻り値で画面を更新かける場合など「論理的には同じオブジェクトだけど、参照は別になっちゃう」なんてケースもちょくちょくあります。AngularJSではそのために「行のトラッキングはこの値を使ってやるよ」という指定方法があります。ときどき見かける track by xx ってヤツです。具体的には下記の通り。

修正前:<tr ng-repeat="instance in aboutCtrl.friends" >
修正後:<tr ng-repeat="instance in aboutCtrl.friends track by instance.id" >

これで「(更新をかけにいく行を指定するために) 元データを追跡する際には instance変数のidというプロパティを使うよ」という意味になります。 上記の例では、ハッシュ値が異なっていてもidプロパティが等しいなら同じとみなしてその行を更新したいので、まさに上記の指定で想定通りの動きをさせることができます。

たとえばRESTの結果を画面に表示しているケースで、戻り電文にレコードIDみたいなモノがあった場合、それでトラッキングしておけばいいわけですね。

トラッキング指定に関数を指定する

track by xx のxx部には関数を指定することもできます。

Controllerに定義している下記の関数があったとして、

// trackするモノを決めるメソッド
me.myTrack = function (hash,value,index) {
    console.log(hash);
    console.log(value);
    console.log(index);
    return value.id;
};
<tr class="row animate-repeat"  
 ng-repeat="instance in aboutCtrl.friends track by aboutCtrl.myTrack($id(instance),instance,$index)">

なんて指定することが可能です。オブジェクトのプロパティにはオブジェクトをUniqueにするキーがなかったとしても、関数を使ってキーを作成するなんて事ができるわけですね。

ちなみに$id(instance)は引数のオブジェクトのハッシュ値を求める関数です。

蛇足。track by $indexで重複回避の件

ng-repeat がオブジェクトにIDを振ってDOMをトラッキングする都合上、生成されるIDが重複する事は許されないわけです。そこでよく出てくる話が、

<div ng-repeat="value in [4, 4]"></div>

これだとオブジェクトが重複してエラーが出るので、

<div ng-repeat="value in [4, 4] track by $index"></div>

ってやりましょう、って言う話。「ng-repeat は重複をゆるさないよ」ってのは、ハッシュ値を生成したときおなじIDになると更新行を特定できないので、重複は許さないってことだったわけですね。そこで担ぎだされてくる$indexですが、これは配列やコレクションのindex番号で、$indexをトラッキングのIDにしておけば重複することはないので「なんかエラーが出る場合は track by $index つけとけ」っていう結論になっちゃうわけです。。

正しいっちゃそうなんですが、$index でトラッキングすると、つねに1行目から更新かけに行くわけで、アニメーションさせると「なんか味気ないね」って感じになりますので、できる限りちゃんと行を特定する値でトラッキングするのが正しいように思います。

ani03.gif

↑なんか味気ない感じ。

関連リンク


この記事は

選択肢 投票
おもしろかった 3  
そうでもない 2  

Top / AngularJS / TIPS集 / ng-repeatについて

現在のアクセス:4141


*1 $id(instance)で取れるらしい

添付ファイル: fileani03.gif 689件 [詳細] fileani02.gif 680件 [詳細] fileani01.gif 733件 [詳細] fileinit.png 579件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2022-02-22 (火) 17:25:08 (1019d)