Introdução ao ControlValueAccessor
código do projeto
Formulários dentro do Angular podem ser tratados utilizando duas abordagens: Reactive forms ou Template Driven forms. Essas abordagens nos permitem capturar entrada de dados, validar campos que possam estar inválidos, e são baseados em um objeto que será enviado ao servidor.
Pensando, tanto em usar reactive forms ou template driven: quando componentizamos muitos elementos, podemos acabar sentindo uma dificuldade em: "como podemos criar um componente apartado de um formulário, e continuar tendo a referência do dado digitado pelo usuário, via bind, em nosso [(ngModel)]
ou no formControlName
"?
aka: como crio um control personalizado para o meu formulário
É aí que entra o PODEROSO ControlValueAccessor
!
O que é o ControlValueAccessor
MAS O QUE É ESSE ControlValueAccessor???
O ControlValueAccessor é uma interface que atua como uma ponte entre elementos do DOM e a API de formulário do Angular. Implementando essa interface em um componente, podemos manipular valores através das propriedades dispostas pelo próprio FormsModule
: ngModel
; Ou no caso de um formulário reativo, o formControlName
.
Os métodos obrigatórios a serem implementados dentro da classe são os seguintes:
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
- O
writeValue
escreve um novo valor ao elemento - O
registerOnChange
registra uma função callback que é chamada quando o valor do nosso controle muda na tela. - O
registerOnTouched
serve para atualizar o estado do formulário paratouched
(ou blurred). Então, assim que o usuário interage com nosso elemento no controle personalizado, podemos chamar a função salva no callback, para informar ao Angular que o controle recebeu uma alteração. - O
setDisabledState
vai ser chamado para avisar a API de formulários, que o controle sofreu alteração no estado de desabilitado (true para false, ou false para true).
Agora que entendemos como funciona a interface ControlValueAccessor
, e quais métodos ela implementa, vamos ao nosso código.
Formulário de "Contact us"
Vamos criar um formulário básico, sobre "Contact us". É um formulário simples, onde teremos dois inputs e um textarea:
- Digite seu nome;
- Digite seu e-mail;
- Digite uma mensagem;
O formulário será feito com template-driven e com Reactive forms.
Template Driven
// template-driven-form HTML
<form (ngSubmit)="submit()">
<fieldset class="">
<legend>Fale conosco</legend>
<div class="data">
<label for="yourName">Digite seu nome</label>
<input
type="text"
id="yourName"
name="yourName"
[(ngModel)]="contactUs.name"
placeholder="DevTo da Silva"
/>
</div>
<div class="data">
<label for="yourEmail">Digite seu email</label>
<input
type="email"
id="yourEmail"
name="yourEmail"
[(ngModel)]="contactUs.email"
placeholder="example@mail.com"
/>
</div>
<div class="data">
<label for="message">Deixe sua mensagem</label>
<textarea
name="message"
id="message"
cols="30"
rows="10"
[(ngModel)]="contactUs.message"
></textarea>
</div>
</fieldset>
<button
type="submit"
aria-label="Enviar o motivo do contato"
aria-describedby="sendContactUs"
>
Enviar
</button>
<span [hidden]="true" id="sendContactUs"
>Ao clicar no botão, você envia os dados que foram preenchidos. Em breve retornaremos o contato.</span>
</form>
<pre>{{contactUs | json}}</pre>
e o nosso component .ts
// template-driven-form HTML
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { JsonPipe } from '@angular/common';
type ContactUsTemplateDriven = {
name: string;
email: string;
message: string;
};
@Component({
selector: 'app-template-driven-form-contactus',
standalone: true,
imports: [FormsModule, JsonPipe],
templateUrl: './template-driven-form-contactus.component.html',
styleUrl: './template-driven-form-contactus.component.scss',
})
export class TemplateDrivenFormContactusComponent {
protected contactUs: ContactUsTemplateDriven = {
name: '',
email: '',
message: '',
};
protected submit(): void {
console.log('enviou', this.contactUs);
}
}
Formulário Reativo
// reactive-forms-contactus.component
<form (ngSubmit)="submit()" [formGroup]="contactUsForm">
<fieldset class="">
<legend>Fale conosco</legend>
<div class="data">
<label for="name">Digite seu nome</label>
<input
type="text"
id="name"
name="name"
formControlName="name"
placeholder="DevTo da Silva"
/>
</div>
<div class="data">
<label for="email">Digite seu email</label>
<input
type="email"
id="email"
name="email"
formControlName="email"
placeholder="example@mail.com"
/>
</div>
<div class="data">
<label for="message">Deixe sua mensagem</label>
<textarea name="message" id="message" cols="30" rows="10" formControlName="message"></textarea>
</div>
</fieldset>
<button
type="submit"
aria-label="Enviar o motivo do contato"
aria-describedby="sendContactUs"
>
Enviar
</button>
<span [hidden]="true" id="sendContactUs">
Ao clicar no botão, você envia os dados que foram preenchidos. E assim que
pudermos, retornaremos o contato
</span>
</form>
<pre>{{contactUsForm.getRawValue() | json}}</pre>
// reactive-forms-contactus.component
type ContactUsReactiveForm = {
name: FormControl<string>;
email: FormControl<string>;
message: FormControl<string>;
};
@Component({
selector: 'app-reactive-form-contactus',
standalone: true,
imports: [FormsModule, ReactiveFormsModule, JsonPipe],
templateUrl: './reactive-form-contactus.component.html',
styleUrl: './reactive-form-contactus.component.scss',
})
export class ReactiveFormContactusComponent {
private readonly formBuilder = inject(NonNullableFormBuilder);
protected contactUsForm!: FormGroup<ContactUsReactiveForm>;
constructor() {
this.contactUsForm = this.formBuilder.group<ContactUsReactiveForm>({
name: this.formBuilder.control({ value: '', disabled: false }),
email: this.formBuilder.control(
{ value: '', disabled: false },
{ validators: [Validators.email] }
),
message: this.formBuilder.control({ value: '', disabled: false }),
});
}
protected submit() {
console.log('enviou', this.contactUsForm.getRawValue());
}
}
Agora que finalizamos nossos formulários, vamos criar nossos controles personalizados.
Iremos criar três componentes, que serão os dois inputs e o textarea.
Criando controles personalizados com ControlValueAccessor
Vamos criar um novo componente, utilizando o comando do angular-cli:
$ ng g c components/contact-us-name-input
Dentro do nosso componente, iremos implementar a interface ControlValueAccessor e seus respectivos métodos. Porém, só implementar a interface ControlValueAccessor não é o suficiente.
Precisamos também, registrar dentro dos providers do nosso component, o token NG_VALUE_ACCESSOR
. Esse token é responsável pela integração do component, com a API de formulários do Angular.
Junto do NG_VALUE_ACCESSOR, também colocaremos o forwardRef
. O uso do forwardRef se faz necessário porque estamos fazendo referência ao componente que estamos criando (nesse caso, para o "ContactUsNameInputComponent". Porém, faremos para todos os nossos controles personalizados.)
@Component({
selector: 'app-contact-us-name-input',
standalone: true,
imports: [],
templateUrl: './contact-us-name-input.component.html',
styleUrl: './contact-us-name-input.component.scss',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ContactUsNameInputComponent),
multi: true,
},
],
})
export class ContactUsNameInputComponent implements ControlValueAccessor {
@Input() public contactName = '';
protected value = '';
protected disabled = false;
protected onChanged!: (value: string) => void;
protected onTouched!: () => void;
writeValue(value: string): void {
this.value = value;
}
registerOnChange(fn: (value: string) => void): void {
this.onChanged = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
<label for="name">Digite seu nome</label>
<input
type="text"
id="name"
placeholder="DevTo da Silva"
[value]="value"
(input)="onChanged($any($event.target)?.value)"
/>
Vamos analisar o nosso código:
- Registramos dentro dos providers do nosso component, o NG_VALUE_ACCESSOR e fizemos referência ao nosso componente.
Nota: Sempre precisamos fazer referência, no forwardRef, à classe que estamos trabalhando.
Implementamos os métodos obrigatórios da interface ControlValueAccessor, tipamos os parâmetros dos métodos da interface, e atribuímos os valores aos atributos do componente.
No nosso template html, colocamos um evento onInput, chamado o callback onChanged para escutar as mudanças que ocorrem no nosso input, e informar ao nosso formulário que as mudanças ocorreram
Tendo criado nosso primeiro componente, vamos implementá-lo em nossos componentes de formulário da seguinte forma:
Substituiremos a parte do label e input de nome, pelo nosso componente:
<form (ngSubmit)="submit()">
<fieldset class="">
<legend>Fale conosco</legend>
<div class="data">
// essa parte aqui
// pelo nosso novo componente
<app-contact-us-name-input
name="yourName"
[contactName]="contactUs.name"
[(ngModel)]="contactUs.name"
[ngModelOptions]="{ standalone: true }"
/>
</div>
... rest of html
Como estamos trabalhando com standalone components, precisamos informar à instância do NgModel que o nosso control também é standalone.
Faremos o mesmo trabalho dentro do input de email, e o textarea, registrando o NG_VALUE_ACCESSOR
, e fazendo uso do forwardRef para referência dos nossos componentes. (para não ficar muito extenso e repetitivo, o código está no meu github, e você poder ver tudo que foi feito nesse link ao lado: ContactUs - Github.
Nosso formulário, agora com nossos controles personalizados integrados, ficará dessa forma:
<form (ngSubmit)="submit()">
<fieldset class="">
<legend>Fale conosco</legend>
<div class="data">
<app-contact-us-name-input
name="yourName"
[contactName]="contactUs.name"
[(ngModel)]="contactUs.name"
[ngModelOptions]="{ standalone: true }"
/>
</div>
<div class="data">
<app-contact-us-email-input
name="email"
[contactEmail]="contactUs.email"
[(ngModel)]="contactUs.email"
[ngModelOptions]="{ standalone: true }"
/>
</div>
<div class="data">
<app-contact-us-message-text
[(ngModel)]="contactUs.message"
[ngModelOptions]="{ standalone: true }"
[contactText]="contactUs.message"
/>
</div>
</fieldset>
<button
type="submit"
aria-label="Enviar o motivo do contato"
aria-describedby="sendContactUs"
>
Enviar
</button>
<span [hidden]="true" id="sendContactUs"
>Ao clicar no botão, você envia os dados que foram preenchidos. E assim que
pudermos, retornaremos o contato</span
>
</form>
<pre>{{ contactUs | json }}</pre>
e nosso formulário reativo:
<form (ngSubmit)="submit()" [formGroup]="contactUsForm">
<fieldset class="">
<legend>Fale conosco</legend>
<div class="data">
<app-contact-us-name-input formControlName="name" />
</div>
<div class="data">
<app-contact-us-email-input formControlName="email" />
</div>
<div class="data">
<app-contact-us-message-text formControlName="message" />
</div>
</fieldset>
<button
type="submit"
aria-label="Enviar o motivo do contato"
aria-describedby="sendContactUs"
>
Enviar
</button>
<span [hidden]="true" id="sendContactUs">
Ao clicar no botão, você envia os dados que foram preenchidos. E assim que
pudermos, retornaremos o contato
</span>
</form>
<pre>{{ contactUsForm.getRawValue() | json }}</pre>
Conclusão
Conseguimos entender como criar controles customizados, para serem reutilizados dentro de nossos formulários, independente da abordagem que foi utilizada. Também foi mostrado como usamos a interface ControlValueAccessor
, entendemos também o uso do token NG_VALUE_ACCESSOR
, e como fazer a referência do nosso componente enquanto ele não está definido, utilizando forwardRef.
Top comments (3)
gato jóia 🐱👍
Excelente texto, excelente didática, excelentes memes rs. o/
Você é foda! 👊🏽👏👏