_ParentComponent > ChildComponent
_とサービスの2つのコンポーネントがあります。 TitleService
。
ParentComponent
は次のようになります:
_export class ParentComponent implements OnInit, OnDestroy {
title: string;
private titleSubscription: Subscription;
constructor (private titleService: TitleService) {
}
ngOnInit (): void {
// Watching for title change.
this.titleSubscription = this.titleService.onTitleChange()
.subscribe(title => this.title = title)
;
}
ngOnDestroy (): void {
if (this.titleSubscription) {
this.titleSubscription.unsubscribe();
}
}
}
_
ChildComponent
は次のようになります:
_export class ChildComponent implements OnInit {
constructor (
private route: ActivatedRoute,
private titleService: TitleService
) {
}
ngOnInit (): void {
// Updating title.
this.titleService.setTitle(this.route.snapshot.data.title);
}
}
_
アイデアは非常に単純です。ParentController
は画面にタイトルを表示します。常に実際のタイトルをレンダリングするために、TitleService
にサブスクライブし、イベントをリッスンします。タイトルが変更されると、イベントが発生し、タイトルが更新されます。
ChildComponent
が読み込まれると、ルーターからデータを取得し(動的に解決されます)、TitleService
にタイトルを新しい値で更新するように指示します。
問題は、この解決策がこのエラーを引き起こすことです:
_Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'Dynamic Title'.
_
変化検出ラウンドで値が更新されたようです。
より良い実装を得るためにコードを再配置する必要がありますか、それともどこかで別の変更検出を開始する必要がありますか?
setTitle()
およびonTitleChange()
呼び出しを尊敬されるコンストラクターに移動できますが、コンストラクターロジックで「重労働」を行うことは悪い習慣と見なされていることを読みました。ローカルプロパティの初期化。
また、タイトルは子コンポーネントによって決定される必要があるため、このロジックを子コンポーネントから抽出することはできませんでした。
問題をよりよく示すために、最小限の例を実装しました。 GitHubリポジトリ にあります。
徹底的な調査の結果、問題はParentComponent
で_*ngIf="title"
_を使用した場合にのみ発生しました。
_<p>Parent Component</p>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>
<hr>
<app-child></app-child>
_
記事 ExpressionChangedAfterItHasBeenCheckedError
エラーについて知っておく必要があるすべて は、この動作を詳細に説明しています。
あなたの問題には2つの可能な解決策があります:
1)ngIf
の前にapp-child
を置きます。
<app-child></app-child>
<p>Title: "<span *ngIf="title">{{ title }}</span>"</p>
2)非同期イベントを使用します。
export class TitleService {
private titleChangeEmitter = new EventEmitter<string>(true);
^^^^^^--------------
徹底的な調査の結果、問題は* ngIf = "title"を使用した場合にのみ発生しました。
説明している問題はngIf
に固有のものではなく、入力後の変更検出中に同期的に更新される親入力に依存するカスタムディレクティブを実装することで簡単に再現できます。ディレクティブに渡されました:
@Directive({
selector: '[aDir]'
})
export class ADirective {
@Input() aDir;
------------
<div [aDir]="title"></div>
<app-child></app-child> <----------- will throw ExpressionChangedAfterItHasBeenCheckedError
それが実際に発生する理由は、変更の検出とコンポーネント/ディレクティブ表現に固有のAngular内部を十分に理解する必要があります。あなたはこれらの記事から始めることができます:
この回答ですべてを詳細に説明することはできませんが、ここに高レベルの説明があります。 ダイジェストサイクル Angularの間、子ディレクティブに対して特定の操作を実行します。そのような操作の1つは、入力を更新し、子ディレクティブ/コンポーネントでngOnInit
ライフサイクルフックを呼び出すことです。重要なのは、これらの操作が厳密な順序で実行されることです。
これで、次の階層ができました。
parent-component
ng-if
child-component
また、上記の操作を実行する場合、Angularはこの階層に従います。したがって、現在Angularがparent-component
をチェックすると仮定します。
ng-if
のtitle
入力バインディングを更新し、初期値undefined
に設定しますng-if
についてはngOnInit
を呼び出します。child-component
の更新は必要ありませんchild-component
のタイトルをDynamic Title
に変更するparent-component
に対してngOnInti
を呼び出します。したがって、title=undefined
のプロパティを更新するときにAngularがng-if
を渡したが、変更の検出が終了すると、title=Dynamic Title
にparent-component
があるという状況になります。ここで、Angularは2番目のダイジェストを実行して、変更がないことを確認します。ただし、前のダイジェストでng-if
に渡されたものを現在の値と比較すると、変更が検出され、エラーがスローされます。
ng-if
テンプレートのchild-component
とparent-component
の順序を変更すると、angularがparent-component
のプロパティを更新する前にng-if
のプロパティが更新される状況になります。
ChangeDetectorRef
を使用してみると、Angularが変更について手動で通知され、エラーはスローされません。title
のParentComponent
を変更した後、detectChanges
のChangeDetectorRef
メソッドを呼び出します。
export class ParentComponent implements OnInit, OnDestroy {
title: string;
private titleSubscription: Subscription;
constructor(private titleService: TitleService, private changeDetector: ChangeDetectorRef) {
}
public ngOnInit() {
this.titleSubscription = this.titleService.onTitleChange()
.subscribe(title => this.title = title);
this.changeDetector.detectChanges();
}
public ngOnDestroy(): void {
if (this.titleSubscription
) {
this.titleSubscription.unsubscribe();
}
}
}
場合によっては、「迅速な」解決策はsetTimeout()
を使用することです。すべての警告を考慮する必要がありますが(他の人が参照している記事を参照)、タイトルを設定するような単純なケースでは、これが最も簡単な方法です。
ngOnInit (): void {
// Watching for title change.
this.titleSubscription = this.titleService.onTitleChange().subscribe(title => {
setTimeout(() => { this.title = title; }, 0);
});
}
変更検出のすべての複雑さをまだ理解していない場合は、これを最後の手段として使用してください。それから戻ってきて、あなたがいるときに別の方法でそれを修正してください:)
私の場合、私はデータの状態を変更する-この回答では、ダイジェストサイクルのAngularInDepth.comの説明を読む必要があります--htmlレベル内、すべて私私がデータを処理する方法を次のように変更する必要がありました。
<div>{{event.subjects.pop()}}</div>
に
<div>{{event.subjects[0]}}</div>
要約:ポップ(配列の最後の要素を返し、データの状態を変更する)の代わりに、通常の方法で状態を変更せずにデータを取得して例外を防止しました。