Błąd oznacza, że Angular nie wie, co zrobić, gdy postawisz formControlna div. Aby to naprawić, masz dwie możliwości.
- Umieszczasz
formControlNameelement na elemencie, który jest obsługiwany przez Angular po wyjęciu z pudełka. Są to: input, textareai select.
- Ty implementujesz
ControlValueAccessorinterfejs. 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 formControlNamena elemencie, który nie ma naturalnie związanej z nim wartości.
Teraz wdrożenie ControlValueAccessorinterfejsu 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 ControlValueAccessorinterfejsu 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
ControlValueAccessorinterfejs został zaimplementowany
- b) Upewnij się, że implementujesz
ControlValueAccessorinterfejs
- 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
onChangei onTouchjego 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
writeValuenastępnej sekcji zobaczymy, jak działa ta metoda, kiedy ją zaimplementujemy. Umieściłem to tutaj, więc wszystkie wymagane właściwości ControlValueAccessorsą zaimplementowane, a kod nadal się kompiluje.
Zaimplementuj writeValue
Co writeValuerobi, 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-inputi 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 writeValuejest 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 CustomInputComponentobraca 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 onChangei onTouchz (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 inputpolu komponentu niestandardowego, wywołasz onChangezaktualizowaną 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 onChangewł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 onChangejest wybrana przeze mnie. Możesz tu wybrać cokolwiek, na przykład propagateChangelub 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 registerOnChangemetodę 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 onTouchfunkcję. Więc w naszym przykładzie tutaj, jeśli chcesz zachować zgodność z tym, jak Angular robi to dla gotowych kontrolek formularza, powinieneś zadzwonić, onTouchgdy pole wejściowe jest rozmyte:
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
Ponownie, onTouchto 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() subformzamiast 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