O erro significa que o Angular não sabe o que fazer quando você coloca um formControl
a div
. Para corrigir isso, você tem duas opções.
- Você coloca o
formControlName
elemento em, que é suportado pelo Angular imediatamente. Essas são: input
, textarea
e select
.
- Você implementa a
ControlValueAccessor
interface. Ao fazer isso, você está dizendo ao Angular "como acessar o valor do seu controle" (daí o nome). Ou em termos simples: o que fazer, quando você coloca um formControlName
elemento, que naturalmente não tem um valor associado a ele.
Agora, implementar a ControlValueAccessor
interface pode ser um pouco assustador no começo. Especialmente porque não há muita documentação boa por aí e você precisa adicionar muita clichê no seu código. Então, deixe-me tentar detalhar isso em algumas etapas simples de seguir.
Mova seu controle de formulário para seu próprio componente
Para implementar o ControlValueAccessor
, você precisa criar um novo componente (ou diretiva). Mova o código relacionado ao seu controle de formulário para lá. Assim, também será facilmente reutilizável. Ter um controle já dentro de um componente pode ser o motivo, em primeiro lugar, porque você precisa implementar a ControlValueAccessor
interface, caso contrário, não poderá usar seu componente personalizado junto com os formulários Angular.
Adicione o boilerplate ao seu código
A implementação da ControlValueAccessor
interface é bastante detalhada, aqui está o boilerplate que vem com ela:
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
}
Então, o que as partes individuais estão fazendo?
- a) Permite ao Angular saber durante o tempo de execução que você implementou a
ControlValueAccessor
interface
- b) Garante que você está implementando a
ControlValueAccessor
interface
- c) Esta é provavelmente a parte mais confusa. Basicamente, o que você está fazendo é dar ao Angular os meios para substituir suas propriedades / métodos de classe
onChange
e onTouch
com sua própria implementação durante o tempo de execução, para que você possa chamar essas funções. Portanto, é importante entender este ponto: você não precisa implementar onChange e onTouch por conta própria (exceto a implementação vazia inicial). A única coisa que você faz com (c) é deixar o Angular anexar suas próprias funções à sua classe. Por quê? Portanto, você pode chamar os métodos onChange
e onTouch
fornecidos pela Angular no momento apropriado. Vamos ver como isso funciona abaixo.
- d) Veremos também como o
writeValue
método funciona na próxima seção, quando o implementarmos. Coloquei aqui, para que todas as propriedades necessárias ControlValueAccessor
sejam implementadas e seu código ainda seja compilado.
Implementar writeValue
O que writeValue
faz é fazer algo dentro do seu componente personalizado, quando o controle do formulário é alterado do lado de fora . Por exemplo, se você tiver nomeado seu componente de controle de formulário personalizado app-custom-input
e o estiver usando no componente pai, desta forma:
<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
então writeValue
é acionado sempre que o componente pai, de alguma forma, altera o valor de myFormControl
. Pode ser, por exemplo, durante a inicialização do formulário ( this.form = this.formBuilder.group({myFormControl: ""});
) ou em uma redefinição do formulário this.form.reset();
.
O que você normalmente deseja fazer se o valor do controle de formulário for alterado externamente é gravá-lo em uma variável local que represente o valor do controle de formulário. Por exemplo, se você CustomInputComponent
gira em torno de um controle de formulário baseado em texto, ele pode se parecer com o seguinte:
writeValue(input: string) {
this.input = input;
}
e no html de CustomInputComponent
:
<input type="text"
[ngModel]="input">
Você também pode gravá-lo diretamente no elemento de entrada, conforme descrito nos documentos angulares.
Agora você lidou com o que acontece dentro do seu componente quando algo muda fora. Agora vamos olhar para a outra direção. Como você informa o mundo exterior quando algo muda dentro do seu componente?
Chamando onChange
O próximo passo é informar o componente pai sobre alterações dentro do seu CustomInputComponent
. É aqui que as funções onChange
e onTouch
de (c) de cima entram em cena. Ao chamar essas funções, você pode informar o exterior sobre alterações dentro do seu componente. Para propagar alterações do valor para o exterior, você precisa chamar onChange com o novo valor como argumento . Por exemplo, se o usuário digitar algo no input
campo em seu componente personalizado, você chamará onChange
com o valor atualizado:
<input type="text"
[ngModel]="input"
(ngModelChange)="onChange($event)">
Se você verificar a implementação (c) de cima novamente, verá o que está acontecendo: Angular vincula sua própria implementação à onChange
propriedade da classe. Essa implementação espera um argumento, que é o valor de controle atualizado. O que você está fazendo agora é chamar esse método e avisar a Angular sobre a mudança. Agora, o Angular vai em frente e altera o valor do formulário do lado de fora. Esta é a parte chave disso tudo. Você disse ao Angular quando deveria atualizar o controle de formulário e com qual valor chamandoonChange
. Você forneceu os meios para "acessar o valor do controle".
A propósito: O nome onChange
é escolhido por mim. Você pode escolher qualquer coisa aqui, por exemplo propagateChange
ou similar. No entanto, o nome que você escolher será a mesma função que recebe um argumento, que é fornecido pelo Angular e que é vinculado à sua classe pelo registerOnChange
método durante o tempo de execução.
Chamando onTouch
Como os controles de formulário podem ser "tocados", você também deve fornecer ao Angular os meios para entender quando seu controle de formulário personalizado é tocado. Você pode fazer isso, adivinhou, chamando a onTouch
função Portanto, para o nosso exemplo aqui, se você quiser se manter em conformidade com o modo como o Angular está fazendo isso nos controles prontos para uso, chame onTouch
quando o campo de entrada estiver embaçado:
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
Novamente, onTouch
é um nome escolhido por mim, mas sua função real é fornecida pelo Angular e leva zero argumentos. O que faz sentido, já que você está apenas informando a Angular, que o controle de formulário foi tocado.
Juntando tudo
Então, como é que isso acontece quando tudo se junta? Deve ficar assim:
// 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>
Mais exemplos
Formulários aninhados
Observe que os Controladores de Valor de Controle NÃO são a ferramenta certa para grupos de formulários aninhados. Para grupos de formulários aninhados, você pode simplesmente usar um @Input() subform
. Os Controladores de Valor de Controle são feitos para quebrar controls
, não groups
! Veja este exemplo como usar uma entrada para um formulário aninhado: https://stackblitz.com/edit/angular-nested-forms-input-2
Fontes