web-dev-qa-db-ja.com

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

このシナリオでは、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;
    }
}

107
Chu Son

問題と可能な解決策を完全に理解するために、パイプとコンポーネントのAngular変更検出を議論する必要があります。

パイプ交換検出

ステートレス/純粋なパイプ

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

この質問では、"#student of students | sortByName:queryElem.value"students、およびqueryElem.valueは入力であり、パイプsortByNameはステートレス/純粋です。 studentsは配列(参照)です。

  • 学生が追加されても、配列referenceは変更されません– 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's answer によると、「このエラーはdevモードでのみ発生します(ベータ0でデフォルトで有効になっています)。アプリをブートストラップするときにenableProdMode()を呼び出すと、エラーはスローされません。 」 ApplicationRef.tick()のドキュメント 状態:

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

このシナリオでは、エラーは偽り/誤解を招くと考えています。ステートフルパイプがあり、出力は呼び出されるたびに変化する可能性があります。副作用がある可能性がありますが、それは問題ありません。 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のイディオムになります。配列がパイプを供給していて、配列が変更される可能性がある場合(配列内のアイテム、配列参照ではない)、ステートフルパイプを使用する必要があります。

137
Mark Rajcok

Eric Martinezがコメントで指摘したように、Pipeデコレーターにpure: falseを追加し、ComponentデコレーターにchangeDetection: ChangeDetectionStrategy.OnPushを追加すると問題が解決します。 これが動作するプランクです。ChangeDetectionStrategy.Alwaysへの変更も機能します。その理由は次のとおりです。

パイプのangle2ガイドによる

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

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

27
0xcaff

デモプランカー

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;
  }
}
19
pixelbits

angular documentation から

純粋なパイプと不純なパイプ

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

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

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

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

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

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

このため、変更検出戦略を使用できる場合は、純粋なパイプが望ましいです。できない場合は、不純なパイプを使用できます。

9
Sumit Jaiswal

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

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

0
Nick Bistrov

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

import { YourPipeComponentName } from 'YourPipeComponentPath';

class YourService {

  constructor(private pipe: YourPipeComponentName) {}

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

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

constructor(
private searchFilter : TableFilterPipe) { }

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

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

0
Richard Luo

Pure:falseを行う代わりに。 this.students = Object.assign([]、NEW_ARRAY);によって、コンポーネントの値をディープコピーおよび置換できます。ここで、NEW_ARRAYは変更された配列です。

angular 6で機能し、他のangularバージョンでも機能するはずです。

0
ideeps