Der Fehler bedeutet, dass Angular nicht weiß, was zu tun ist, wenn Sie eine formControl
auf eine setzen div
. Um dies zu beheben, haben Sie zwei Möglichkeiten.
- Sie setzen das
formControlName
auf ein Element, das von Angular unterstützt wird, sofort ein. Das sind: input
, textarea
und select
.
- Sie implementieren die
ControlValueAccessor
Schnittstelle. Auf diese Weise teilen Sie Angular mit, "wie Sie auf den Wert Ihres Steuerelements zugreifen können" (daher der Name). Oder in einfachen Worten: Was tun, wenn Sie formControlName
ein Element mit einem Wert versehen, dem natürlich kein Wert zugeordnet ist ?
Jetzt kann die Implementierung der ControlValueAccessor
Schnittstelle zunächst etwas entmutigend sein. Vor allem, weil es nicht viel gute Dokumentation dafür gibt und Sie Ihrem Code viel Boilerplate hinzufügen müssen. Lassen Sie mich versuchen, dies in einfach zu befolgenden Schritten aufzuschlüsseln.
Verschieben Sie Ihr Formularsteuerelement in eine eigene Komponente
Um das zu implementieren ControlValueAccessor
, müssen Sie eine neue Komponente (oder Direktive) erstellen. Verschieben Sie den Code für Ihr Formularsteuerelement dorthin. So wird es auch leicht wiederverwendbar sein. Ein Steuerelement bereits in einer Komponente zu haben, könnte in erster Linie der Grund sein, warum Sie die ControlValueAccessor
Schnittstelle implementieren müssen , da Sie Ihre benutzerdefinierte Komponente sonst nicht zusammen mit Angular-Formularen verwenden können.
Fügen Sie das Boilerplate Ihrem Code hinzu
Die Implementierung der ControlValueAccessor
Schnittstelle ist recht ausführlich. Hier ist das mitgelieferte Boilerplate:
import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
@Component({
selector: 'app-custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.scss'],
// a) copy paste this providers property (adjust the component name in the forward ref)
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
// b) Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {
// c) copy paste this code
onChange: any = () => {}
onTouch: any = () => {}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
// d) copy paste this code
writeValue(input: string) {
// TODO
}
Was machen die einzelnen Teile?
- a) Lässt Angular zur Laufzeit wissen, dass Sie die
ControlValueAccessor
Schnittstelle implementiert haben
- b) Stellt sicher, dass Sie die
ControlValueAccessor
Schnittstelle implementieren
- c) Dies ist wahrscheinlich der verwirrendste Teil. Grundsätzlich geben Sie Angular die Möglichkeit, Ihre Klasseneigenschaften / -methoden
onChange
und onTouch
ihre eigene Implementierung zur Laufzeit zu überschreiben , sodass Sie diese Funktionen dann aufrufen können. Daher ist dieser Punkt wichtig zu verstehen: Sie müssen onChange und onTouch nicht selbst implementieren (außer der anfänglichen leeren Implementierung). Das einzige, was Sie mit (c) tun, ist, Angular seine eigenen Funktionen an Ihre Klasse anhängen zu lassen. Warum? Sie können dann die von Angular bereitgestellten Methoden und zum entsprechenden Zeitpunkt aufrufen . Wir werden unten sehen, wie das funktioniert.onChange
onTouch
- d) Wir werden auch
writeValue
im nächsten Abschnitt sehen, wie die Methode funktioniert, wenn wir sie implementieren. Ich habe es hier eingefügt, damit alle erforderlichen Eigenschaften ControlValueAccessor
implementiert werden und Ihr Code weiterhin kompiliert wird.
Implementieren Sie writeValue
Was Sie tunwriteValue
müssen, ist, etwas in Ihrer benutzerdefinierten Komponente zu tun, wenn das Formularsteuerelement außen geändert wird . Wenn Sie beispielsweise Ihre benutzerdefinierte Formularsteuerungskomponente benannt haben app-custom-input
und diese in der übergeordneten Komponente wie folgt verwenden würden:
<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
wird dann writeValue
ausgelöst, wenn die übergeordnete Komponente den Wert von irgendwie ändert myFormControl
. Dies kann beispielsweise während der Initialisierung des Formulars ( this.form = this.formBuilder.group({myFormControl: ""});
) oder beim Zurücksetzen eines Formulars geschehen this.form.reset();
.
Wenn sich der Wert des Formularsteuerelements äußerlich ändert, möchten Sie ihn normalerweise in eine lokale Variable schreiben, die den Wert des Formularsteuerelements darstellt. Wenn Sie CustomInputComponent
sich beispielsweise um ein textbasiertes Formularsteuerelement drehen, könnte dies folgendermaßen aussehen:
writeValue(input: string) {
this.input = input;
}
und im HTML von CustomInputComponent
:
<input type="text"
[ngModel]="input">
Sie können es auch direkt in das Eingabeelement schreiben, wie in den Angular-Dokumenten beschrieben.
Jetzt haben Sie behandelt, was innerhalb Ihrer Komponente passiert, wenn sich etwas außerhalb ändert. Schauen wir uns jetzt die andere Richtung an. Wie informieren Sie die Außenwelt, wenn sich innerhalb Ihrer Komponente etwas ändert?
OnChange aufrufen
Der nächste Schritt besteht darin, die übergeordnete Komponente über Änderungen in Ihrem Computer zu informieren CustomInputComponent
. Hier kommen die onChange
und onTouch
Funktionen von (c) von oben ins Spiel. Durch Aufrufen dieser Funktionen können Sie die Außenwelt über Änderungen innerhalb Ihrer Komponente informieren. Um Änderungen des Werts nach außen zu übertragen, müssen Sie onChange mit dem neuen Wert als Argument aufrufen . Wenn der Benutzer beispielsweise input
in Ihrer benutzerdefinierten Komponente etwas in das Feld eingibt, rufen Sie onChange
mit dem aktualisierten Wert auf:
<input type="text"
[ngModel]="input"
(ngModelChange)="onChange($event)">
Wenn Sie die Implementierung (c) erneut von oben überprüfen, werden Sie sehen, was passiert: Angular hat seine eigene Implementierung an die onChange
class -Eigenschaft gebunden . Diese Implementierung erwartet ein Argument, nämlich den aktualisierten Steuerwert. Was Sie jetzt tun, ist, dass Sie diese Methode aufrufen und Angular über die Änderung informieren. Angular ändert nun den Formularwert an der Außenseite. Dies ist der Schlüssel zu all dem. Sie haben Angular durch Aufrufen mitgeteilt, wann und mit welchem Wert das Formularsteuerelement aktualisiert werden sollonChange
. Sie haben ihm die Möglichkeit gegeben, "auf den Steuerwert zuzugreifen".
Übrigens: Der Name onChange
wird von mir gewählt. Sie können hier beispielsweise alles propagateChange
oder ähnliches auswählen . Wie auch immer Sie es nennen, es ist dieselbe Funktion, die ein Argument akzeptiert, das von Angular bereitgestellt wird und das zur registerOnChange
Laufzeit von der Methode an Ihre Klasse gebunden wird .
OnTouch anrufen
Da Formularsteuerelemente "berührt" werden können, sollten Sie Angular auch die Möglichkeit geben, zu verstehen, wann Ihr benutzerdefiniertes Formularsteuerelement berührt wird. Sie können es tun, Sie haben es erraten, indem Sie die onTouch
Funktion aufrufen . Wenn Sie in unserem Beispiel hier die Anforderungen von Angular für die sofort einsatzbereiten Formularsteuerelemente erfüllen möchten, sollten Sie Folgendes aufrufen, onTouch
wenn das Eingabefeld unscharf ist:
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
Nochmal, onTouch
ist ein Name von mir gewählt, aber was seine eigentliche Funktion ist, wird von Angular bereitgestellt und es werden keine Argumente verwendet. Was Sinn macht, da Sie Angular nur wissen lassen, dass das Formularsteuerelement berührt wurde.
Alles zusammenfügen
Wie sieht das aus, wenn alles zusammenkommt? Es sollte so aussehen:
// custom-input.component.ts
import {Component, OnInit, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
@Component({
selector: 'app-custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.scss'],
// Step 1: copy paste this providers property
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
// Step 2: Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {
// Step 3: Copy paste this stuff here
onChange: any = () => {}
onTouch: any = () => {}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
// Step 4: Define what should happen in this component, if something changes outside
input: string;
writeValue(input: string) {
this.input = input;
}
// Step 5: Handle what should happen on the outside, if something changes on the inside
// in this simple case, we've handled all of that in the .html
// a) we've bound to the local variable with ngModel
// b) we emit to the ouside by calling onChange on ngModelChange
}
// custom-input.component.html
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
// parent.component.html
<app-custom-input [formControl]="inputTwo"></app-custom-input>
// OR
<form [formGroup]="form" >
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
Mehr Beispiele
Verschachtelte Formulare
Beachten Sie, dass Control Value Accessors NICHT das richtige Werkzeug für verschachtelte Formulargruppen sind. Für verschachtelte Formulargruppen können Sie @Input() subform
stattdessen einfach ein verwenden. Control Value Accessors sollen einwickeln controls
, nicht groups
! In diesem Beispiel wird gezeigt, wie eine Eingabe für ein verschachteltes Formular verwendet wird: https://stackblitz.com/edit/angular-nested-forms-input-2
Quellen