Der Fehler bedeutet, dass Angular nicht weiß, was zu tun ist, wenn Sie eine formControlauf eine setzen div. Um dies zu beheben, haben Sie zwei Möglichkeiten.
- Sie setzen das
formControlNameauf ein Element, das von Angular unterstützt wird, sofort ein. Das sind: input, textareaund select.
- Sie implementieren die
ControlValueAccessorSchnittstelle. 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 formControlNameein Element mit einem Wert versehen, dem natürlich kein Wert zugeordnet ist ?
Jetzt kann die Implementierung der ControlValueAccessorSchnittstelle 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 ControlValueAccessorSchnittstelle 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 ControlValueAccessorSchnittstelle 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
ControlValueAccessorSchnittstelle implementiert haben
- b) Stellt sicher, dass Sie die
ControlValueAccessorSchnittstelle implementieren
- c) Dies ist wahrscheinlich der verwirrendste Teil. Grundsätzlich geben Sie Angular die Möglichkeit, Ihre Klasseneigenschaften / -methoden
onChangeund onTouchihre 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.onChangeonTouch
- d) Wir werden auch
writeValueim nächsten Abschnitt sehen, wie die Methode funktioniert, wenn wir sie implementieren. Ich habe es hier eingefügt, damit alle erforderlichen Eigenschaften ControlValueAccessorimplementiert 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-inputund diese in der übergeordneten Komponente wie folgt verwenden würden:
<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
wird dann writeValueausgelö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 CustomInputComponentsich 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 onChangeund onTouchFunktionen 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 inputin Ihrer benutzerdefinierten Komponente etwas in das Feld eingibt, rufen Sie onChangemit 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 onChangeclass -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 propagateChangeoder ä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 registerOnChangeLaufzeit 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 onTouchFunktion 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, onTouchwenn 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() subformstattdessen 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