Entendendo o ControlValueAccessor

Entendendo o ControlValueAccessor

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 para touched (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.

Leave a Reply

Your email address will not be published. Required fields are marked *