it-swarm.com.de

Angular2 - Ausdruck hat sich nach der Überprüfung geändert - Bindung an Div-Breite mit Größenänderungsereignissen

Ich habe etwas über diesen Fehler gelesen und nachgeforscht, aber ich bin mir nicht sicher, was die richtige Antwort für meine Situation ist. Ich verstehe, dass die Änderungserkennung im Dev-Modus zweimal ausgeführt wird. Ich möchte jedoch nur enableProdMode() verwenden, um das Problem zu maskieren.

Hier ist ein einfaches Beispiel, bei dem die Anzahl der Zellen in der Tabelle mit zunehmender Breite des div ansteigen sollte. (Beachten Sie, dass die Breite des div nicht nur von der Bildschirmbreite abhängt, sodass @Media nicht einfach angewendet werden kann.)

Mein HTML sieht folgendermaßen aus (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>

Das allein tut nichts. Ich vermute, das liegt daran, dass nichts die Änderungserkennung verursacht. Wenn ich jedoch die erste Zeile in die folgende Zeile ändere und eine leere Funktion zum Empfangen des Anrufs erstelle, beginnt sie zu arbeiten, aber gelegentlich bekomme ich den Ausdruck "Ausdruck hat sich geändert, nachdem der Fehler geprüft wurde".

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

Meine beste Vermutung ist, dass mit dieser Änderung die Änderungserkennung ausgelöst wird und alles beginnt zu reagieren. Wenn sich jedoch die Breite schnell ändert, wird die Ausnahme ausgelöst, da die vorherige Iteration der Änderungserkennung länger als die Änderung der Breite des div dauerte.

  1. Gibt es einen besseren Ansatz, um die Änderungserkennung auszulösen?
  2. Soll ich das Größenänderungsereignis über eine Funktion erfassen, um sicherzustellen, dass die Änderungserkennung erfolgt.
  3. Ist die Verwendung von #widthParentDiv für den Zugriff auf die Breite des Div. Akzeptabel? 
  4. Gibt es eine bessere Gesamtlösung?

Weitere Einzelheiten zu meinem Projekt finden Sie unter this ähnliche Frage.

Vielen Dank

36
Anthony Day

Um Ihr Problem zu beheben, müssen Sie lediglich die Größe von div in einer Komponenteneigenschaft nach jedem Größenänderungsereignis abrufen und speichern und diese Eigenschaft in der Vorlage verwenden. Auf diese Weise bleibt der Wert konstant, wenn die zweite Runde der Änderungserkennung im Dev-Modus ausgeführt wird.

Ich empfehle auch, @HostListener Zu verwenden, anstatt (window:resize) Zu Ihrer Vorlage hinzuzufügen. Wir verwenden @ViewChild, Um einen Verweis auf das div zu erhalten. Und wir werden den Lebenszyklus-Hook ngAfterViewInit() verwenden, um den Anfangswert festzulegen.

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

Schade, dass das nicht funktioniert. Wir bekommen

Der Ausdruck hat sich geändert, nachdem er überprüft wurde. Vorheriger Wert: 'false'. Aktueller Wert: 'true'.

Der Fehler beklagt sich über unsere NgIf Ausdrücke - beim ersten Ausführen ist divWidth gleich 0, dann wird ngAfterViewInit() ausgeführt und der Wert wird in einen anderen Wert als 0 geändert Die 2. Runde der Änderungserkennung wird ausgeführt (im Dev-Modus). Zum Glück gibt es eine einfache/bekannte Lösung, und dies ist ein einmaliges Problem, kein fortbestehendes Problem wie im OP:

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

Beachten Sie, dass diese Technik des Wartens auf einen Tick hier dokumentiert ist: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child =

In ngAfterViewInit() und ngAfterViewChecked() müssen wir häufig den Trick setTimeout() anwenden, da diese Methoden aufgerufen werden, nachdem die Ansicht der Komponente erstellt wurde.

Hier ist eine Arbeit plunker.


Wir können das verbessern. Ich denke, wir sollten die Größenänderungsereignisse so drosseln, dass die Angular Änderungserkennung nur etwa alle 100-250 ms ausgeführt wird und nicht jedes Mal, wenn ein Größenänderungsereignis auftritt. Dies sollte verhindern, dass die App in diesem Fall träge wird Der Benutzer ändert die Größe des Fensters, da derzeit bei jedem Größenänderungsereignis die Änderungserkennung ausgeführt wird (zweimal im Dev-Modus). Sie können dies überprüfen, indem Sie dem vorherigen Plunker die folgende Methode hinzufügen:

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

Observables können Ereignisse leicht drosseln. Anstatt @HostListener Zum Binden an das Größenänderungsereignis zu verwenden, erstellen wir eine Observable:

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

Dies funktioniert, aber ... während ich damit experimentierte, entdeckte ich etwas sehr Interessantes ... obwohl wir das Größenänderungsereignis drosseln, wird die Angular Änderungserkennung immer dann ausgeführt, wenn es ein Größenänderungsereignis gibt. Das heißt, die Drosselung hat keinen Einfluss darauf, wie oft die Änderungserkennung ausgeführt wird. (Tobias Bosch hat dies bestätigt: https://github.com/angular/angular/issues/1773#issuecomment-10207825 .)

Ich möchte, dass die Änderungserkennung nur ausgeführt wird, wenn das Ereignis die Drosselungszeit überschreitet. Und ich brauche nur Änderungserkennung, um auf dieser Komponente ausgeführt zu werden. Die Lösung besteht darin, das Observable außerhalb der Angular Zone zu erstellen und dann die Änderungserkennung innerhalb des Abonnement-Rückrufs manuell aufzurufen:

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();
       })
  );
}

Hier ist eine Arbeit plunker.

Im Plunker habe ich ein counter hinzugefügt, das ich mit dem Lifecycle-Hook ngDoCheck() inkrementiere. Sie können sehen, dass diese Methode nicht aufgerufen wird - der Zählerwert ändert sich bei Ereignissen zur Größenänderung nicht.

detectChanges() führt die Änderungserkennung für diese Komponente und ihre untergeordneten Komponenten aus. Wenn Sie die Änderungserkennung lieber über die Root-Komponente ausführen möchten (d. H. Eine vollständige Änderungserkennung durchführen), verwenden Sie stattdessen ApplicationRef.tick() (dies ist im Plunker auskommentiert). Beachten Sie, dass tick() bewirkt, dass ngDoCheck() aufgerufen wird.


Das ist eine gute Frage. Ich habe viel Zeit damit verbracht, verschiedene Lösungen auszuprobieren und viel gelernt. Vielen Dank, dass Sie diese Frage gestellt haben.

64
Mark Rajcok

Auf andere Weise habe ich das gelöst:

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


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

export class YourComponent{

  constructor(public cdRef:ChangeDetectorRef) { }

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

Die beste Lösung ist, setTimeout oder delay für die Dienste zu verwenden.

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

Verwenden Sie einfach

setTimeout(() => {
  //Your expression to change if state
});
0
Raj Gopal