Verwalten der Angular2-Ausnahme "Ausdruck hat sich geändert, nachdem er aktiviert wurde", wenn eine Komponenteneigenschaft von der aktuellen Datums- und Uhrzeitangabe abhängt

167

Meine Komponente verfügt über Stile, die von der aktuellen Datums- und Uhrzeitangabe abhängen. In meiner Komponente habe ich die folgende Funktion.

  private fontColor( dto : Dto ) : string {
    // date d'exécution du dto
    let dtoDate : Date = new Date( dto.LastExecution );

    (...)

    let color =  "hsl( " + hue + ", 80%, " + (maxLigness - lightnessAmp) + "%)";

    return color;
  }

lightnessAmpwird aus der aktuellen datetime berechnet. Die Farbe ändert dtoDatesich in den letzten 24 Stunden.

Der genaue Fehler ist der folgende:

Der Ausdruck hat sich geändert, nachdem er überprüft wurde. Vorheriger Wert: 'hsl (123, 80%, 49%)'. Aktueller Wert: 'hsl (123, 80%, 48%)'

Ich weiß, dass die Ausnahme im Entwicklungsmodus nur zum Zeitpunkt der Überprüfung des Werts angezeigt wird. Wenn sich der überprüfte Wert vom aktualisierten Wert unterscheidet, wird die Ausnahme ausgelöst.

Daher habe ich versucht, die aktuelle Datums- und Uhrzeitangabe in jedem Lebenszyklus mithilfe der folgenden Hook-Methode zu aktualisieren, um die Ausnahme zu vermeiden:

  ngAfterViewChecked()
  {
    console.log( "! changement de la date du composant !" );
    this.dateNow = new Date();
  }

...aber ohne Erfolg.

Anthony Brenelière
quelle

Antworten:

356

Führen Sie die Änderungserkennung explizit nach der Änderung aus:

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

constructor(private cdRef:ChangeDetectorRef) {}

ngAfterViewChecked()
{
  console.log( "! changement de la date du composant !" );
  this.dateNow = new Date();
  this.cdRef.detectChanges();
}
Günter Zöchbauer
quelle
Perfekte Lösung, danke. Ich habe festgestellt, dass dies auch mit den folgenden Hook-Methoden funktioniert: ngOnChanges, ngDoCheck, ngAfterContentChecked. Gibt es also die beste Option?
Anthony Brenelière
27
Das hängt von Ihrem Anwendungsfall ab. Wenn Sie bei der Initialisierung einer Komponente etwas tun möchten, ngOnInit()ist dies normalerweise der erste Ort. Wenn der Code vom gerenderten DOM abhängt ngAfterViewInit()oder ngAfterContentInit()die nächsten Optionen sind. ngOnChanges()ist eine gute Anpassung, wenn der Code jedes Mal ausgeführt werden soll, wenn eine Eingabe geändert wurde. ngDoCheck()dient zur benutzerdefinierten Änderungserkennung. Eigentlich weiß ich nicht, wofür ngAfterViewChecked()am besten verwendet wird. Ich denke, es heißt kurz davor oder danach ngAfterViewInit().
Günter Zöchbauer
2
@KushalJayswal Entschuldigung, kann aus Ihrer Beschreibung keinen Sinn ergeben. Ich würde vorschlagen, eine neue Frage mit dem Code zu erstellen, die zeigt, was Sie erreichen möchten. Idealerweise mit einem StackBlitz-Beispiel.
Günter Zöchbauer
4
Dies ist auch eine hervorragende Lösung, wenn Ihr Komponentenstatus auf browserberechneten DOM-Eigenschaften wie clientWidthusw. basiert .
Jonah
1
Gerade auf ngAfterViewInit verwendet, hat wie ein Zauber funktioniert.
Francisco Arleo
41

TL; DR

ngAfterViewInit() {
    setTimeout(() => {
        this.dateNow = new Date();
    });
}

Obwohl dies eine Problemumgehung ist, ist es manchmal sehr schwierig, dieses Problem besser zu lösen. Machen Sie sich also keine Vorwürfe, wenn Sie diesen Ansatz verwenden. Das ist okay.

Beispiele : Das ursprüngliche Problem [ Link ], gelöst mit setTimeout()[ Link ]


Wie vermeide ich?

Im Allgemeinen tritt dieser Fehler normalerweise nach dem Hinzufügen auf (auch in übergeordneten / untergeordneten Komponenten) ngAfterViewInit. Die erste Frage ist also, sich zu fragen: Kann ich ohne leben ngAfterViewInit? Vielleicht verschieben Sie den Code irgendwohin ( ngAfterViewCheckedkönnte eine Alternative sein).

Beispiel : [ Link ]


Ebenfalls

Auch asynchrone Inhalte ngAfterViewInit, die sich auf DOM auswirken, können dies verursachen. Kann auch über setTimeoutoder durch Hinzufügen des delay(0)Operators in der Pipe gelöst werden :

ngAfterViewInit() {
  this.foo$
    .pipe(delay(0)) //"delay" here is an alternative to setTimeout()
    .subscribe();
}

Beispiel : [ Link ]


Schöne Lektüre

Guter Artikel darüber, wie man das debuggt und warum es passiert: Link

Sergei Panfilov
quelle
2
Scheint langsamer zu sein als die ausgewählte Lösung
Pete B
6
Dies ist nicht die beste Lösung, aber verdammt, das funktioniert IMMER. Die ausgewählte Antwort funktioniert nicht immer (man muss den Haken gut verstehen, damit er funktioniert)
Freddy Bonda
Was ist die richtige Erklärung, warum das funktioniert, weiß jemand? Liegt es daran, dass es dann in einem anderen Thread (asynchron) ausgeführt wird?
Knnhcn
3
@knnhcn Es gibt keinen anderen Thread in Javascript. JS ist von Natur aus Single-Threaded. SetTimeout weist die Engine lediglich an, die Funktion einige Zeit nach Ablauf des Timers auszuführen . Hier ist der Timer 0, was in modernen Browsern tatsächlich als 4 behandelt wird. Dies ist genügend Zeit für Angular, um seine magischen Dinge zu tun: developer.mozilla.org/en-US/docs/Web/API/…
Kilves
26

Wie von @leocaseiro zum Thema Github erwähnt .

Ich habe 3 Lösungen für diejenigen gefunden, die nach einfachen Lösungen suchen.

1) Wechseln von ngAfterViewInitnachngAfterContentInit

2) Umzug in ngAfterViewCheckedKombination mit ChangeDetectorRefwie auf # 14748 vorgeschlagen (Kommentar)

3) Bleiben Sie bei ngOnInit (), rufen ChangeDetectorRef.detectChanges()Sie jedoch nach Ihren Änderungen auf.

CandidJ
quelle
1
Kann jemand dokumentieren, was die am meisten empfohlene Lösung ist?
Pipo
13

Sie gehen zwei Lösungen!

1. Ändern Sie ChangeDetectionStrategy in OnPush

Für diese Lösung sagen Sie im Grunde eckig:

Hören Sie auf, nach Änderungen zu suchen. Ich werde es nur tun, wenn ich weiß, dass es notwendig ist

Die schnelle Lösung:

Ändern Sie Ihre Komponente so, dass sie verwendet wird ChangeDetectionStrategy.OnPush

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
    // ...
}

Damit scheinen die Dinge nicht mehr zu funktionieren. Das liegt daran, dass Sie Angular von nun an detectChanges()manuell aufrufen müssen.

this.cdr.detectChanges();

Hier ist ein Link, der mir geholfen hat, ChangeDetectionStrategy richtig zu verstehen : https://alligator.io/angular/change-detection-strategy/

2. Grundlegendes zu ExpressionChangedAfterItHasBeenCheckedError

Hier ist ein kleiner Auszug aus der Antwort von tomonari_t über die Ursachen für diesen Fehler. Ich habe versucht, nur die Teile einzubeziehen , die mir geholfen haben, dies zu verstehen.

Der vollständige Artikel zeigt echte Codebeispiele zu jedem hier gezeigten Punkt.

Die Hauptursache ist der Winkellebenszyklus:

Nach jeder Operation merkt sich Angular, welche Werte zur Ausführung einer Operation verwendet wurden. Sie werden in der Eigenschaft oldValues ​​der Komponentenansicht gespeichert.

Nachdem die Überprüfungen für alle Komponenten durchgeführt wurden, startet Angular den nächsten Digest-Zyklus. Statt jedoch Vorgänge auszuführen, werden die aktuellen Werte mit denen verglichen, an die er sich aus dem vorherigen Digest-Zyklus erinnert.

Die folgenden Vorgänge werden bei Digest-Zyklen überprüft:

Überprüfen Sie, ob die an die untergeordneten Komponenten übergebenen Werte mit den Werten übereinstimmen, mit denen die Eigenschaften dieser Komponenten jetzt aktualisiert werden.

Überprüfen Sie, ob die zum Aktualisieren der DOM-Elemente verwendeten Werte mit den Werten übereinstimmen, die zum Aktualisieren dieser Elemente verwendet werden.

prüft auf alle untergeordneten Komponenten

Und so wird der Fehler ausgelöst wird, wenn die verglichenen Werte unterschiedlich sind. , erklärte Blogger Max Koretskyi :

Der Schuldige ist immer die untergeordnete Komponente oder eine Richtlinie.

Und schließlich sind hier einige Beispiele aus der Praxis, die normalerweise diesen Fehler verursachen:

  • Geteilte Dienstleistungen
  • Synchrone Ereignisübertragung
  • Dynamische Komponenteninstanziierung

Jedes Beispiel finden Sie hier (plunkr). In meinem Fall war das Problem eine dynamische Komponenteninstanziierung.

Aus eigener Erfahrung empfehle ich jedem dringend, die setTimeoutLösung zu vermeiden . In meinem Fall kam es zu einer "fast" Endlosschleife (21 Anrufe, die ich Ihnen nicht zeigen möchte, wie man sie provoziert).

Ich würde empfehlen, immer die Winkellebenszyklen im Auge zu behalten, damit Sie berücksichtigen können, wie sie sich jedes Mal auswirken, wenn Sie den Wert einer anderen Komponente ändern. Mit diesem Fehler sagt Ihnen Angular:

Du machst das vielleicht falsch, bist du sicher, dass du Recht hast?

Der gleiche Blog sagt auch:

Häufig besteht die Lösung darin, den richtigen Änderungserkennungs-Hook zu verwenden, um eine dynamische Komponente zu erstellen

Eine kurze Anleitung für mich ist, beim Codieren mindestens die folgenden zwei Dinge zu berücksichtigen ( ich werde versuchen, sie im Laufe der Zeit zu ergänzen ):

  1. Vermeiden Sie es, übergeordnete Komponentenwerte aus den untergeordneten Komponenten zu ändern. Ändern Sie sie stattdessen über die übergeordneten Komponenten.
  2. Wenn Sie @Inputund @OutputDirektiven verwenden, versuchen Sie zu vermeiden, dass Lyfecycle-Änderungen ausgelöst werden, es sei denn, die Komponente ist vollständig initialisiert.
  3. Vermeiden Sie unnötige Aufrufe, da this.cdr.detectChanges();diese mehr Fehler auslösen können, insbesondere wenn Sie mit vielen dynamischen Daten arbeiten
  4. Wenn die Verwendung von this.cdr.detectChanges();obligatorisch ist, stellen Sie sicher, dass die verwendeten Variablen ( @Input, @Output, etc) am rechten Erkennungshaken ( OnInit, OnChanges, AfterView, etc) gefüllt / initialisiert sind.
  5. Wenn möglich, entfernen statt verstecken , dies bezieht sich auf Punkt 3 und 4.

Ebenfalls

Wenn Sie Angular Life Hook vollständig verstehen möchten, empfehle ich Ihnen, die offizielle Dokumentation hier zu lesen:

Luis Limas
quelle
Ich kann immer noch nicht verstehen, warum ich das geändert habe, ChangeDetectionStrategyum OnPushes für mich zu reparieren. Ich habe eine einfache Komponente, die hat [disabled]="isLastPage()". Diese Methode liest das MatPaginator-ViewChild und kehrt zurück this.paginator !== undefined ? this.paginator.pageIndex === this.paginator.getNumberOfPages() - 1 : true;. Der Paginator ist nicht sofort verfügbar, aber nachdem er mit gebunden wurde @ViewChild. Durch Ändern des ChangeDetectionStrategywurde der Fehler behoben - und die Funktionalität ist nach wie vor vorhanden. Ich bin mir nicht sicher, welche Nachteile ich jetzt habe, aber danke!
Igor
Das ist großartig @Igor! Der einzige Nachteil der Verwendung OnPushbesteht darin, dass Sie sie this.cdr.detectChanges()jedes Mal verwenden müssen, wenn Sie möchten, dass Ihre Komponente aktualisiert wird. Ich denke du hast es schon benutzt
Luis Limas
8

In unserem Fall haben wir das Problem behoben, indem wir changeDetection in die Komponente eingefügt und DetectChanges () in ngAfterContentChecked wie folgt aufgerufen haben

@Component({
  selector: 'app-spinner',
  templateUrl: './spinner.component.html',
  styleUrls: ['./spinner.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpinnerComponent implements OnInit, OnDestroy, AfterContentChecked {

  show = false;

  private subscription: Subscription;

  constructor(private spinnerService: SpinnerService, private changeDedectionRef: ChangeDetectorRef) { }

  ngOnInit() {
    this.subscription = this.spinnerService.spinnerState
      .subscribe((state: SpinnerState) => {
        this.show = state.show;
      });
  }

  ngAfterContentChecked(): void {
      this.changeDedectionRef.detectChanges();
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}
Charitha Goonewardena
quelle
2

Ich denke, die beste und sauberste Lösung, die Sie sich vorstellen können, ist folgende:

@Component( {
  selector: 'app-my-component',
  template: `<p>{{ myData?.anyfield }}</p>`,
  styles: [ '' ]
} )
export class MyComponent implements OnInit {
  private myData;

  constructor( private myService: MyService ) { }

  ngOnInit( ) {
    /* 
      async .. await 
      clears the ExpressionChangedAfterItHasBeenCheckedError exception.
    */
    this.myService.myObservable.subscribe(
      async (data) => { this.myData = await data }
    );
  }
}

Getestet mit Angular 5.2.9

JavierFuentes
quelle
3
Das ist hacky .. und so unnötig.
JoeriShoeby
1
@JoeriShoeby Alle anderen oben genannten Lösungen sind Problemumgehungen, die auf setTimeouts oder erweiterten Angular-Ereignissen basieren. Diese Lösung ist reine ES2017-Lösung, die von allen großen Browsern unterstützt wird. Developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… caniuse.com / # search =
warte
1
Was ist, wenn Sie beispielsweise mit Angular + Cordova eine mobile Anwendung (für Android API 17) erstellen, auf die Sie sich nicht auf ES2017-Funktionen verlassen können? Beachten Sie, dass die akzeptierte Antwort eine Lösung ist, keine
Umgehung
@JoeriShoeby Mit Typoskript wird es in JS kompiliert, sodass keine Probleme mit der Funktionsunterstützung auftreten.
Biggbest
@biggbest Was meinst du damit? Ich weiß, dass Typescript zu JS kompiliert wird, aber das lässt Sie nicht davon ausgehen, dass alle JS funktionieren werden. Beachten Sie, dass Javascript in einem Webbrowser ausgeführt wird und sich jeder Browser anders verhält.
JoeriShoeby
2

Eine kleine Arbeit, die ich oft benutzt habe

Promise.resolve(null).then(() => {
    console.log( "! changement de la date du composant !" );
    this.dateNow = new Date();
    this.cdRef.detectChanges();
});

Ich ersetze hauptsächlich "null" durch eine Variable, die ich im Controller verwende.

Khateeb321
quelle
1

Obwohl es bereits viele Antworten und einen Link zu einem sehr guten Artikel zur Änderungserkennung gibt, wollte ich hier meine zwei Cent geben. Ich denke, die Überprüfung ist aus einem bestimmten Grund vorhanden. Daher habe ich über die Architektur meiner App nachgedacht und festgestellt, dass die Änderungen in der Ansicht mithilfe BehaviourSubjectdes richtigen Lebenszyklus-Hooks behoben werden können. Also hier ist, was ich für eine Lösung getan habe.

  • Ich verwende eine Komponente eines Drittanbieters ( Vollkalender) , aber ich verwende auch Angular Material . Obwohl ich ein neues Plugin für das Styling erstellt habe, war es etwas umständlich, das Erscheinungsbild zu erhalten, da die Anpassung des Kalender-Headers ohne Verzweigen des Repos nicht möglich ist und rollen Sie Ihre eigenen.
  • Daher habe ich die zugrunde liegende JavaScript-Klasse erhalten und muss meinen eigenen Kalenderheader für die Komponente initialisieren. Das erfordert ViewChild, dass das gerendert wird, bevor mein Elternteil gerendert wird, was Angular nicht funktioniert. Aus diesem Grund habe ich den Wert, den ich für meine Vorlage benötige, in Folgendes verpackt BehaviourSubject<View>(null):

    calendarView$ = new BehaviorSubject<View>(null);

Wenn ich sicher sein kann, dass die Ansicht aktiviert ist, aktualisiere ich diesen Betreff mit dem Wert aus @ViewChild:

  ngAfterViewInit(): void {
    // ViewChild is available here, so get the JS API
    this.calendarApi = this.calendar.getApi();
  }

  ngAfterViewChecked(): void {
    // The view has been checked and I know that the View object from
    // fullcalendar is available, so emit it.
    this.calendarView$.next(this.calendarApi.view);
  }

Dann verwende ich in meiner Vorlage einfach die asyncPipe. Kein Hacken mit Änderungserkennung, keine Fehler, funktioniert reibungslos.

Bitte zögern Sie nicht zu fragen, ob Sie weitere Details benötigen.

thomi
quelle
1

Verwenden Sie einen Standardformularwert, um den Fehler zu vermeiden.

Anstatt die akzeptierte Antwort der Anwendung von detectChanges () in ngAfterViewInit () zu verwenden (wodurch auch der Fehler in meinem Fall behoben wurde), habe ich stattdessen beschlossen, einen Standardwert für ein dynamisch erforderliches Formularfeld zu speichern, damit bei einer späteren Aktualisierung des Formulars Die Gültigkeit wird nicht geändert, wenn der Benutzer eine Option im Formular ändert, die die neuen erforderlichen Felder auslöst (und die Schaltfläche zum Senden deaktiviert).

Dadurch wurde ein kleines Stück Code in meiner Komponente gespeichert, und in meinem Fall wurde der Fehler vollständig vermieden.

T. Bulford
quelle
Dies ist eine wirklich gute Übung, mit der Sie einen unnötigen Anruf beithis.cdr.detectChanges()
Luis Limas
1

Ich habe diesen Fehler erhalten, weil ich eine Variable deklariert habe und später
ihren Wert mit ändern wolltengAfterViewInit

export class SomeComponent {

    header: string;

}

um das zu beheben, wechselte ich von

ngAfterViewInit() { 

    // change variable value here...
}

zu

ngAfterContentInit() {

    // change variable value here...
}
user12163165
quelle