web-dev-qa-db-ja.com

Angular2-チェック後に式が変更された-サイズ変更イベントでdiv幅にバインド

私はこのエラーを読んで調査しましたが、私の状況に対する正しい答えは何なのかわかりません。開発モードでは、変更の検出が2回実行されることは理解していますが、enableProdMode()を使用して問題をマスクすることには消極的です。

Divの幅が拡大するにつれて、テーブル内のセルの数が増える単純な例を次に示します。 (divの幅は画面の幅だけの関数ではないため、@ Mediaは簡単に適用できないことに注意してください)

私のHTMLは次のようになります(widget.template.html):

<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
   <td>Value1</td>
   <td *ngIf="widgetParentDiv.clientWidth>350">Value2</td>
   <td *ngIf="widgetParentDiv.clientWidth>700">Value3</td>
</tr></table>

これ自体は何もしません。これは、変更の検出を引き起こしているものがないためだと推測しています。ただし、最初の行を次のように変更し、呼び出しを受信する空の関数を作成すると、機能し始めますが、時々「エラーがチェックされた後に式が変更されました」というメッセージが表示されます

<div #widgetParentDiv class="Content">
   gets replaced with
      <div #widgetParentDiv (window:resize)=parentResize(10) class="Content">

この修正により、変更検出がトリガーされ、すべてが応答し始めますが、幅が急速に変化すると、divの幅を変更するよりも前の変更検出の繰り返しが完了するまでに時間がかかったため、例外がスローされます。

  1. 変更検出をトリガーするためのより良いアプローチはありますか?
  2. 変更検出を確実に行うために、関数を使用してサイズ変更イベントをキャプチャする必要がありますか?
  3. #widthParentDivを使用してdivの幅にアクセスできますか?
  4. より良い全体的なソリューションはありますか?

私のプロジェクトの詳細については、 this 同様の質問をご覧ください。

ありがとう

39
Anthony Day

問題を解決するには、サイズ変更イベントごとにdivのサイズを取得してコンポーネントプロパティに保存し、テンプレートでそのプロパティを使用するだけです。この方法では、変更検出の2回目のラウンドがdevモードで実行されるとき、値は一定のままです。

テンプレートに@HostListenerを追加するのではなく、(window:resize)を使用することもお勧めします。 @ViewChildを使用して、divへの参照を取得します。そして、ライフサイクルフックngAfterViewInit()を使用して初期値を設定します。

import {Component, ViewChild, HostListener} from '@angular/core';

@Component({
  selector: 'my-app',
  template: `<div #widgetParentDiv class="Content">
    <p>Sample widget</p>
    <table><tr>
       <td>Value1</td>
       <td *ngIf="divWidth > 350">Value2</td>
       <td *ngIf="divWidth > 700">Value3</td>
      </tr>
    </table>`,
})
export class AppComponent {
  divWidth = 0;
  @ViewChild('widgetParentDiv') parentDiv:ElementRef;
  @HostListener('window:resize') onResize() {
    // guard against resize before view is rendered
    if(this.parentDiv) {
       this.divWidth = this.parentDiv.nativeElement.clientWidth;
    }
  }
  ngAfterViewInit() {
    this.divWidth = this.parentDiv.nativeElement.clientWidth;
  }
}

残念ながら機能しません。我々が得る

式はチェック後に変更されました。前の値: 'false'。現在の値: 'true'。

エラーはNgIf式について不平を言っています-最初に実行されたとき、divWidthは0であり、その後ngAfterViewInit()が実行され、値が0以外に変更され、次に2回目の変更検出が実行されます(開発モードで)。ありがたいことに、簡単で既知の解決策があります。これは1回限りの問題であり、OPのような継続的な問題ではありません。

  ngAfterViewInit() {
    // wait a tick to avoid one-time devMode
    // unidirectional-data-flow-violation error
    setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
  }

1ティックを待つこのテクニックはここに文書化されていることに注意してください: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child =

多くの場合、ngAfterViewInit()およびngAfterViewChecked()では、これらのメソッドはコンポーネントのビューが構成された後に呼び出されるため、setTimeout()トリックを使用する必要があります。

ここに働く plunker


これを改善できます。 Angular変更検出が、サイズ変更イベントが発生するたびにではなく、たとえば100〜250ミリ秒ごとに実行されるように、サイズ変更イベントを調整する必要があると思います。これにより、ユーザーがウィンドウのサイズを変更しているときにアプリの動作が遅くなることを防ぐことができます。現在、すべてのサイズ変更イベントにより変更検出が実行されるためです(開発モードで2回)。これを確認するには、次のメソッドを以前のプランカーに追加します。

ngDoCheck() {
   console.log('change detection');
}

オブザーバブルはイベントを簡単に調整できるため、@HostListenerを使用してサイズ変更イベントにバインドする代わりに、オブザーバブルを作成します。

Observable.fromEvent(window, 'resize')
   .throttleTime(200)
   .subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );

これは動作しますが、...それを実験していると、非常に興味深いものが見つかりました...サイズ変更イベントを調整しても、サイズ変更イベントが発生するたびにAngular変更の検出が実行されます。つまり、調整は変更検出の実行頻度に影響しません。 (Tobias Boschはこれを確認しました: https://github.com/angular/angular/issues/1773#issuecomment-10207825 。)

イベントがスロットル時間を過ぎた場合にのみ、変更検出を実行する必要があります。そして、このコンポーネントで実行する変更検出のみが必要です。解決策は、Angularゾーンの外側にオブザーバブルを作成し、サブスクリプションコールバック内で変更検出を手動で呼び出すことです。

constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef) {}
ngAfterViewInit() {
  // set initial value, but wait a tick to avoid one-time devMode
  // unidirectional-data-flow-violation error
  setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
  this.ngzone.runOutsideAngular( () =>
     Observable.fromEvent(window, 'resize')
       .throttleTime(200)
       .subscribe(_ => {
          this.divWidth = this.parentDiv.nativeElement.clientWidth;
          this.cdref.detectChanges();
       })
  );
}

ここに働く plunker

プランカーには、ライフサイクルフックngDoCheck()を使用してすべての変更検出サイクルをインクリメントするcounterを追加しました。このメソッドが呼び出されていないことがわかります。サイズ変更イベントでカウンター値は変更されません。

detectChanges() は、このコンポーネントとその子に対して変更検出を実行します。ルートコンポーネントから変更検出を実行する(つまり、完全な変更検出チェックを実行する)場合は、代わりに ApplicationRef.tick() を使用します(これはプランカーでコメント化されています)。 tick()ngDoCheck()を呼び出すことに注意してください。


これは素晴らしい質問です。さまざまな解決策を試すのに多くの時間を費やし、多くのことを学びました。この質問を投稿していただきありがとうございます。

64
Mark Rajcok

私がこれを解決するために使用した他の方法:

import { Component, ChangeDetectorRef } from '@angular/core';


@Component({
  selector: 'your-seelctor',
  template: 'your-template',
})

export class YourComponent{

  constructor(public cdRef:ChangeDetectorRef) { }

  ngAfterViewInit() {
    this.cdRef.detectChanges();
  }
}
15
Luiz C. Junior

単に使用

setTimeout(() => {
  //Your expression to change if state
});
1

最善の解決策は、サービスでsetTimeoutまたは遅延を使用することです。

https://blog.angular-university.io/angular-debugging/