NgForはAngular2のPipeでデータを更新しません


113

このシナリオでは、学生のリスト(配列)をビューに表示していますngFor

<li *ngFor="#student of students">{{student.name}}</li>

他の生徒をリストに追加するたびに更新されるのは素晴らしいことです。

しかし、私はそれ与えるときpipefilter生徒の名前で、

<li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>

フィルタリングする生徒の名前フィールドに何か入力するまで、リストは更新されません。

ここにplnkrへのリンクがあります。

Hello_world.html

<h1>Students:</h1>
<label for="newStudentName"></label>
<input type="text" name="newStudentName" placeholder="newStudentName" #newStudentElem>
<button (click)="addNewStudent(newStudentElem.value)">Add New Student</button>
<br>
<input type="text" placeholder="Search" #queryElem (keyup)="0">
<ul>
    <li *ngFor="#student of students | sortByName:queryElem.value ">{{student.name}}</li>
</ul>

sort_by_name_pipe.ts

import {Pipe} from 'angular2/core';

@Pipe({
    name: 'sortByName'
})
export class SortByNamePipe {

    transform(value, [queryString]) {
        // console.log(value, queryString);
        return value.filter((student) => new RegExp(queryString).test(student.name))
        // return value;
    }
}


14
pure:falseパイプとchangeDetection: ChangeDetectionStrategy.OnPushコンポーネントに追加します。
Eric Martinez

2
ありがとう@EricMartinez。できます。しかし、少し説明できますか?
Chu Son

2
また、.test()フィルター関数では使用しないことをお勧めします。ユーザーのような特別な意味の文字を含む文字列を入力した場合、その理由は、:*+などを、あなたのコードが解除されます。.includes()カスタム関数でクエリ文字列を使用またはエスケープする必要があると思います。
Eggy

6
pure:falseパイプを追加してステートフルにすることで問題が解決します。ChangeDetectionStrategyを変更する必要はありません。
pixelbits 2015

2
これを読んでいる人にとっては、Angular Pipesのドキュメントが大幅に改善され、ここで説明されている同じことの多くが説明されています。見てみな。
0xcaff

回答:


153

問題と可能な解決策を完全に理解するには、パイプとコンポーネントの角度変化検出について説明する必要があります。

パイプ交換検出

ステートレス/ピュアパイプ

デフォルトでは、パイプはステートレス/純粋です。ステートレス/ピュアパイプは、単に入力データを出力データに変換します。彼らは何も覚えていないので、プロパティはなく、transform()メソッドだけです。したがって、Angularはステートレス/純粋なパイプの処理を最適化できます。入力が変更されない場合、変更検出サイクル中にパイプを実行する必要はありません。ようなパイプのため{{power | exponentialStrength: factor}}powerおよびfactor入力です。

この質問の場合"#student of students | sortByName:queryElem.value"studentsqueryElem.valueは入力であり、パイプsortByNameはステートレス/ピュアです。 students配列(参照)です。

  • 生徒を追加しても、配列参照は変わりません–studentsません–変更されません–したがって、ステートレス/ピュアパイプは実行されません。
  • フィルター入力に何かが入力されるとqueryElem.value変化します。したがって、ステートレス/純粋なパイプが実行されます。

配列の問題を修正する1つの方法は、生徒が追加されるたびに配列参照を変更することです。つまり、生徒が追加されるたびに新しい配列を作成します。これはconcat()次のようにして行うことができます:

this.students = this.students.concat([{name: studentName}]);

これは機能しaddNewStudent()ますが、パイプを使用しているという理由だけで、メソッドを特定の方法で実装する必要はありません。を使っpush()て配列に追加したいと思います。

ステートフルパイプ

ステートフルパイプには状態があります。それらには通常、transform()メソッドだけでなく、プロパティがあります。入力が変更されていなくても、評価が必要になる場合があります。パイプがステートフル/非ピュアであることを指定すると、– pure: false– Angularの変更検出システムがコンポーネントの変更をチェックし、そのコンポーネントがステートフルパイプを使用するたびに、パイプの出力がチェックされます。

students参照が変更されていない場合でもパイプを実行する必要があるため、効率は良くありませんが、これは望ましいことのように聞こえます。パイプを単にステートフルにすると、エラーが発生します。

EXCEPTION: Expression 'students | sortByName:queryElem.value  in HelloWorld@7:6' 
has changed after it was checked. Previous value: '[object Object],[object Object]'. 
Current value: '[object Object],[object Object]' in [students | sortByName:queryElem.value

@drewmooreの回答によれば、「このエラーは開発モードでのみ発生します(ベータ0以降ではデフォルトで有効になっています。enableProdMode()アプリをブートストラップするときに呼び出した場合、エラーはスローされません。」状態のドキュメントApplicationRef.tick()

開発モードでは、tick()は2回目の変更検出サイクルも実行して、それ以上の変更が検出されないようにします。この2番目のサイクル中に追加の変更が検出された場合、アプリのバインディングには、1回の変更検出パスでは解決できない副作用があります。この場合、Angularアプリケーションはすべての変更検出を完了する必要がある1つの変更検出パスしか持てないため、Angularはエラーをスローします。

私たちのシナリオでは、エラーは偽り/誤解を招くものだと思います。ステートフルパイプがあり、呼び出されるたびに出力が変化する可能性があります。これには副作用があり、問題ありません。NgForはパイプの後で評価されるため、正常に動作するはずです。

ただし、このエラーがスローされて実際に開発することはできないため、1つの回避策は、パイプの実装に配列プロパティ(つまり、状態)を追加し、常にその配列を返すことです。このソリューションについては、@ pixelbitsの回答をご覧ください。

ただし、より効率的にすることができます。また、後で説明するように、パイプの実装で配列プロパティを使用する必要はなく、二重変更検出の回避策も必要ありません。

コンポーネント変更検出

デフォルトでは、すべてのブラウザーイベントで、Angular変更検出がすべてのコンポーネントを通過して、変更されたかどうかを確認します。入力とテンプレート(およびおそらく他のもの)がチェックされます。

コンポーネントがその入力プロパティ(およびテンプレートイベント)にのみ依存し、入力プロパティが不変であることがわかっている場合は、より効率的なonPush変更検出戦略を使用できます。この戦略では、すべてのブラウザーイベントをチェックする代わりに、コンポーネントは、入力が変更されたとき、およびテンプレートイベントがトリガーされたときにのみチェックされます。そして、どうやら、Expression ... has changed after it was checkedこの設定ではそのエラーは発生しません。これは、onPushコンポーネントが再び「マーク」(ChangeDetectorRef.markForCheck())されるまで、コンポーネントが再度チェックされないためです。したがって、テンプレートバインディングとステートフルパイプ出力は、一度だけ実行/評価されます。ステートレス/ピュアパイプは、入力が変更されない限り実行されません。したがって、ここにはまだステートフルパイプが必要です。

これは、@ EricMartinezが提案するソリューションです:onPush変更検出付きのステートフルパイプ。このソリューションについては、@ caffinatedmonkeyの回答を参照してください。

このソリューションでは、transform()メソッドが毎回同じ配列を返す必要がないことに注意してください。しかし、少し奇妙なことに、ステートのないステートフルパイプがあります。もう少し考えてみてください...ステートフルパイプはおそらく常に同じ配列を返すはずです。それ以外の場合onPushは、開発モードのコンポーネントでのみ使用できます。


したがって、結局のところ、@ Ericと@pixelbitsの答えの組み合わせが好きだと思います。同じ配列参照を返すステートフルパイプでonPush、コンポーネントが許可する場合は変更を検出します。ステートフルパイプは同じ配列参照を返すため、パイプは、で構成されていないコンポーネントでも使用できますonPush

Plunker

これはおそらくAngular 2のイディオムになります。配列がパイプにデータを供給していて、配列が変わる可能性がある場合(配列内の項目であり、配列参照ではない)、ステートフルパイプを使用する必要があります。


問題の詳細な説明をありがとう。配列の変更検出が等値比較ではなくIDとして実装されているのは奇妙です。これをObservableで解決することは可能でしょうか?
Martin Nowak

@MartinNowak、配列がObservableであり、パイプがステートレスであるかどうかを尋ねている場合...わかりませんが、試していません。
Mark Rajcok

別の解決策は、出力配列がイベントリスナーを使用して入力配列の変更を追跡する純粋なパイプです。
Tuupertunut 2017年

私はあなたのPlunkerをforkしたところ、onPush変更検出なしで機能しますplnkr.co/edit/gRl0Pt9oBO6038kCXZPk?p=preview
Dan

27

エリック・マルティネスはコメントで指摘したように、追加pure: falseあなたにPipeデコレータとchangeDetection: ChangeDetectionStrategy.OnPushあなたにComponentデコレータは、あなたの問題を解決します。これが動作するプランカーです。に変更してChangeDetectionStrategy.Alwaysも動作します。これが理由です。

パイプのangular2ガイドによると

デフォルトでは、パイプはステートレスです。デコレータのpureプロパティをに設定して、パイプがステートフルであることを宣言する必要があります。この設定は、入力が変更されたかどうかに関係なく、Angularの変更検出システムに、サイクルごとにこのパイプの出力をチェックするように指示します。@Pipefalse

についてはChangeDetectionStrategy、デフォルトで、すべてのバインディングが1サイクルごとにチェックされます。ときにpure: falseパイプが追加され、私は変化検出方法がからに変わり信じるCheckAlwaysCheckOnceパフォーマンス上の理由から。を使用OnPushすると、コンポーネントのバインディングは、入力プロパティが変更されたとき、またはイベントがトリガーされたときにのみチェックされます。の重要な部分である変更検出器の詳細についてangular2は、次のリンクを確認してください。


pure: falseパイプが追加されると、変更検出メソッドがCheckAlwaysからCheckOnceに変更されると思います」-これは、ドキュメントからの引用と一致しません。私の理解は次のとおりです。ステートレスパイプの入力は、デフォルトでサイクルごとにチェックされます。変更がある場合、パイプのtransform()メソッドが呼び出されて出力が(再)生成されます。ステートフルパイプのtransform()メソッドはサイクルごとに呼び出され、その出力の変更がチェックされます。stackoverflow.com/a/34477111/215945も参照してください。
Mark Rajcok、2015

@MarkRajcok、おっと、そのとおりですが、その場合、変更検出戦略の変更が機能するのはなぜですか?
0xcaff 2015

1
すばらしい質問です。変更検出戦略がOnPush(つまり、コンポーネントが「不変」としてマークされている)に変更された場合、ステートフルパイプ出力は「安定化」する必要がないように見えます。つまり、transform()メソッドは1回だけ実行されるようです(おそらくすべてコンポーネントの変更検出は1回だけ実行されます)。これを@pixelbitsの回答と比較すると、transform()メソッドが複数回呼び出され、(pixelbitsの他の回答に従って)安定させる必要があるため、同じ配列参照を使用する必要があります。
Mark Rajcok、2015

@MarkRajcokあなたが言うことが正しい場合、効率の点では、これはおそらくより良い方法です。この特定のユースケースでtransformは、出力が安定するまで複数回ではなく新しいデータがプッシュされたときにのみ呼び出されるためですよね?
0xcaff

1
たぶん、私の長々とした答えを見てください。Angular 2の変更検出に関するドキュメントがもっとあったらいいのに
Mark Rajcok、2015

22

デモプランカー

ChangeDetectionStrategyを変更する必要はありません。ステートフルなパイプを実装するだけで、すべてが機能します。

これはステートフルパイプです(他の変更は行われていません)。

@Pipe({
  name: 'sortByName',
  pure: false
})
export class SortByNamePipe {
  tmp = [];
  transform (value, [queryString]) {
    this.tmp.length = 0;
    // console.log(value, queryString);
    var arr = value.filter((student)=>new RegExp(queryString).test(student.name));
    for (var i =0; i < arr.length; ++i) {
        this.tmp.push(arr[i]);
     }

    return this.tmp;
  }
}

changeDectectionをonPushに変更せずにこのエラーが発生しました:[ plnkr.co/edit/j1VUdgJx​​Yr6yx8WgzCId?p=preview ]( Plnkr)Expression 'students | sortByName:queryElem.value in HelloWorld@7:6' has changed after it was checked. Previous value: '[object Object],[object Object],[object Object]'. Current value: '[object Object],[object Object],[object Object]' in [students | sortByName:queryElem.value in HelloWorld@7:6]
Chu Son

1
SortByNamePipeのローカル変数に値を格納し、それを変換関数@pixelbitsに返す必要がある理由を例示できますか?また、Angularが変換関数を介してchangeDetetion.OnPushを使用して2回、それを使用せずに4回循環することに気づきました
Chu Son

@ChuSon、あなたはおそらく以前のバージョンのログを見ているでしょう。値の配列を返す代わりに、値の配列への参照を返します。これは、変更が検出される可能性があると思います。Pixelbits、あなたの答えはもっと理にかなっています。
0xcaff

他の読者のために、コメントで前述したエラーChuSon、およびローカル変数を使用する必要性について別の質問が作成され、pixelbitsによって回答されました。
Mark Rajcok、2015

19

角度のドキュメントから

純粋で不純なパイプ

パイプには、純粋と不純の2つのカテゴリがあります。パイプはデフォルトで純粋です。これまでに見たすべてのパイプは純粋です。純粋なフラグをfalseに設定して、パイプを不純にします。あなたはFlyingHeroesPipeをこのように不純にすることができます:

@Pipe({ name: 'flyingHeroesImpure', pure: false })

それを行う前に、純粋なパイプから始めて、純粋と不純の違いを理解してください。

純粋なパイプAngularは、入力値の純粋な変化を検出した場合にのみ純粋なパイプを実行します。純粋な変更は、プリミティブ入力値(String、Number、Boolean、Symbol)への変更、または変更されたオブジェクト参照(Date、Array、Function、Object)のいずれかです。

Angularは(複合)オブジェクト内の変更を無視します。入力月を変更したり、入力配列に追加したり、入力オブジェクトのプロパティを更新したりしても、純粋なパイプは呼び出されません。

これは制限的なように見えるかもしれませんが、高速でもあります。オブジェクト参照チェックは高速で、差異の詳細チェックよりもはるかに高速です。そのため、Angularは、パイプの実行とビューの更新の両方をスキップできるかどうかをすばやく判断できます。

このため、変更検出戦略に耐えられる場合は、純粋なパイプが推奨されます。できない場合は、不純なパイプを使用できます。


答えは正しいですが、投稿するときにもう少し努力するといいでしょう
Stefan

2

pure:falseを実行する代わりに。コンポーネントの値をディープコピーしてthis.students = Object.assign([]、NEW_ARRAY);で置き換えることができます。ここで、NEW_ARRAYは変更された配列です。

角度6で動作し、他の角度バージョンでも動作するはずです。


0

回避策:パイプをコンストラクターに手動でインポートし、このパイプを使用して変換メソッドを呼び出します

constructor(
private searchFilter : TableFilterPipe) { }

onChange() {
   this.data = this.searchFilter.transform(this.sourceData, this.searchText)}

実際にはパイプも必要ありません


0

パイプに追加のパラメーターを追加し、配列の変更直後に変更すると、純粋なパイプでもリストが更新されます

アイテムのアイテムをしましょう| pipe:param


0

この使用例では、データフィルタリングにtsファイルのパイプを使用しました。純粋なパイプを使用するよりも、パフォーマンスがはるかに優れています。次のようなtsで使用します。

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

  YourFunction(value) {
    this.pipe.transform(value, 'pipeFilter');
  }
}

0

不純なパイプを作成するには、パフォーマンスにコストがかかります。したがって、不純なパイプを作成せず、データが変更されたときにデータのコピーを作成してデータ変数の参照を変更し、元のデータ変数のコピーの参照を再割り当てします。

            emp=[];
            empid:number;
            name:string;
            city:string;
            salary:number;
            gender:string;
            dob:string;
            experience:number;

            add(){
              const temp=[...this.emps];
              const e={empid:this.empid,name:this.name,gender:this.gender,city:this.city,salary:this.salary,dob:this.dob,experience:this.experience};
              temp.push(e); 
              this.emps =temp;
              //this.reset();
            } 
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.