Błąd oznacza, że Angular nie wie, co zrobić, gdy postawisz formControl
na div
. Aby to naprawić, masz dwie możliwości.
- Umieszczasz
formControlName
element na elemencie, który jest obsługiwany przez Angular po wyjęciu z pudełka. Są to: input
, textarea
i select
.
- Ty implementujesz
ControlValueAccessor
interfejs. W ten sposób mówisz Angularowi "jak uzyskać dostęp do wartości twojej kontrolki" (stąd nazwa). Lub w prostych słowach: co zrobić, gdy umieścisz element formControlName
na elemencie, który nie ma naturalnie związanej z nim wartości.
Teraz wdrożenie ControlValueAccessor
interfejsu może być na początku nieco zniechęcające. Zwłaszcza, że nie ma tam dobrej dokumentacji na ten temat i musisz dodać dużo schematu do swojego kodu. Pozwólcie, że spróbuję to wyjaśnić w kilku prostych krokach.
Przenieś kontrolkę formularza do jej własnego składnika
Aby zaimplementować ControlValueAccessor
, musisz stworzyć nowy komponent (lub dyrektywę). Przenieś tam kod związany z formantem formularza. W ten sposób będzie również łatwo wielokrotnego użytku. Posiadanie kontrolki już wewnątrz komponentu może być przede wszystkim powodem, dla którego musisz zaimplementowaćControlValueAccessor
interfejs, ponieważ w przeciwnym razie nie będziesz mógł używać swojego komponentu niestandardowego razem z formularzami Angular.
Dodaj szablon do swojego kodu
Implementacja ControlValueAccessor
interfejsu jest dość rozwlekła, oto standardowy szablon, który z nim związany:
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
}
Więc co robią poszczególne części?
- a) Pozwala Angularowi wiedzieć, że
ControlValueAccessor
interfejs został zaimplementowany
- b) Upewnij się, że implementujesz
ControlValueAccessor
interfejs
- c) To jest prawdopodobnie najbardziej zagmatwana część. Zasadniczo to, co robisz, polega na tym, że dajesz Angularowi środki do nadpisywania właściwości / metod klasy
onChange
i onTouch
jego własnej implementacji w czasie wykonywania, dzięki czemu możesz wywoływać te funkcje. Więc ten punkt jest ważny, aby zrozumieć: nie musisz samodzielnie implementować onChange i onTouch (poza początkową pustą implementacją). Jedyne, co robisz z (c), to pozwolić Angularowi dołączyć jego własne funkcje do twojej klasy. Czemu? Możesz więc zadzwonić pod numeronChange
ionTouch
metod przewidzianych przez kątowej w odpowiednim czasie. Zobaczymy, jak to działa poniżej.
- d) W
writeValue
następnej sekcji zobaczymy, jak działa ta metoda, kiedy ją zaimplementujemy. Umieściłem to tutaj, więc wszystkie wymagane właściwości ControlValueAccessor
są zaimplementowane, a kod nadal się kompiluje.
Zaimplementuj writeValue
Co writeValue
robi, to zrobić coś wewnątrz komponentu niestandardowego, gdy kontrolka formularza zostanie zmieniona na zewnątrz . Na przykład, jeśli nazwałeś swój komponent kontroli formularza niestandardowego app-custom-input
i chciałbyś go używać w komponencie nadrzędnym w następujący sposób:
<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
następnie writeValue
jest wyzwalany za każdym razem, gdy komponent nadrzędny w jakiś sposób zmienia wartość myFormControl
. Może to być na przykład podczas inicjowania formularza ( this.form = this.formBuilder.group({myFormControl: ""});
) lub resetowania formularza this.form.reset();
.
Zwykle będziesz chciał zrobić, jeśli wartość kontrolki formularza zmieni się na zewnątrz, to zapisanie jej w zmiennej lokalnej, która reprezentuje wartość kontrolki formularza. Na przykład, jeśli CustomInputComponent
obraca się wokół kontrolki formularza opartego na tekście, może to wyglądać następująco:
writeValue(input: string) {
this.input = input;
}
oraz w html CustomInputComponent
:
<input type="text"
[ngModel]="input">
Możesz również napisać go bezpośrednio do elementu wejściowego, jak opisano w dokumentacji Angular.
Teraz poradziłeś sobie z tym, co dzieje się wewnątrz komponentu, gdy coś zmienia się na zewnątrz. Spójrzmy teraz w innym kierunku. W jaki sposób informujesz świat zewnętrzny, gdy coś zmienia się w twoim elemencie?
Calling onChange
Następnym krokiem jest poinformowanie komponentu nadrzędnego o zmianach w twoim CustomInputComponent
. W tym miejscu do gry wchodzą funkcje onChange
i onTouch
z (c) z góry. Wywołując te funkcje, możesz informować zewnątrz o zmianach w twoim komponencie. Aby propagować zmiany wartości na zewnątrz, musisz wywołać onChange z nową wartością jako argumentem . Na przykład, jeśli użytkownik wpisze coś w input
polu komponentu niestandardowego, wywołasz onChange
zaktualizowaną wartość:
<input type="text"
[ngModel]="input"
(ngModelChange)="onChange($event)">
Jeśli ponownie sprawdzisz implementację (c) z góry, zobaczysz, co się dzieje: Angular powiązał swoją własną implementację z onChange
właściwością klasy. Ta implementacja oczekuje jednego argumentu, który jest zaktualizowaną wartością kontrolną. To, co teraz robisz, to wywołanie tej metody, a tym samym poinformowanie Angulara o zmianie. Angular przejdzie teraz dalej i zmieni wartość formularza na zewnątrz. To jest kluczowa część tego wszystkiego. Poinformowałeś Angular, kiedy powinien zaktualizować kontrolkę formularza i jaką wartość, wywołująconChange
. Dałeś mu sposób „dostępu do wartości kontrolnej”.
Przy okazji: nazwa onChange
jest wybrana przeze mnie. Możesz tu wybrać cokolwiek, na przykład propagateChange
lub coś podobnego. Jednak jakkolwiek ją nazwiesz, będzie to ta sama funkcja, która pobiera jeden argument, który jest dostarczany przez Angular i który jest powiązany z twoją klasą przez registerOnChange
metodę w czasie wykonywania.
Wywołanie onTouch
Ponieważ kontrolki formularza można „dotykać”, należy również dać Angularowi środki do zrozumienia, kiedy dotknięta zostanie niestandardowa kontrolka formularza. Możesz to zrobić, zgadłeś, wywołując onTouch
funkcję. Więc w naszym przykładzie tutaj, jeśli chcesz zachować zgodność z tym, jak Angular robi to dla gotowych kontrolek formularza, powinieneś zadzwonić, onTouch
gdy pole wejściowe jest rozmyte:
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
Ponownie, onTouch
to nazwa wybrana przeze mnie, ale to, jaka jest jego rzeczywista funkcja, jest dostarczana przez Angular i nie przyjmuje żadnych argumentów. Co ma sens, ponieważ po prostu informujesz Angulara, że kontrolka formularza została dotknięta.
Kładąc wszystko razem
Jak to wygląda, gdy wszystko idzie razem? To powinno wyglądać tak:
// 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>
Więcej przykładów
Zagnieżdżone formularze
Należy zauważyć, że Accessors wartości kontrolnych NIE są odpowiednim narzędziem dla zagnieżdżonych grup formularzy. W przypadku zagnieżdżonych grup formularzy możesz po prostu użyć @Input() subform
zamiast tego. Accessors wartości kontrolnych mają na celu zawijanie controls
, a nie groups
! Zobacz ten przykład, jak używać danych wejściowych dla zagnieżdżonego formularza: https://stackblitz.com/edit/angular-nested-forms-input-2
Źródła