Publicado em
- 6 minutos de leitura
Control Value Accessor: A Ponte Entre o DOM e o Angular
Quando começamos a criar formulários no Angular, geralmente usamos componentes nativos padrão, como campos de texto clássicos e caixas de seleção.
Mas, à medida que a aplicação cresce e os requisitos de design ficam mais sofisticados, surge a necessidade de criar componentes totalmente personalizados.
Alguns exemplos muito comuns dessas necessidades incluem:
- Um seletor de data complexo
- Um sistema de avaliação por estrelas
- Um campo de busca com autocompletar
É exatamente nesse cenário que entra uma das ferramentas mais poderosas do Angular: o Control Value Accessor.
Ele é a peça fundamental que permite que os seus componentes customizados conversem perfeitamente com a API do framework.
Isso garante que os seus componentes visuais funcionem exatamente como se fossem inputs nativos do HTML.
A Ponte Entre o DOM e o Angular: Entendendo o Propósito
O Control Value Accessor age como um tradutor fluente entre o modelo de dados interno do Angular (o seu código) e o elemento visual que está sendo exibido na tela para o usuário final.
Ele serve para garantir que o framework saiba exatamente como ler o valor atual do seu componente customizado e como escrever um novo valor nele.
Sem essa ponte de comunicação, o Angular não conseguiria realizar tarefas essenciais, como por exemplo:
- Aplicar regras de validação
- Rastrear se o campo foi tocado pelo usuário
- Atualizar o estado de um Reactive Form de forma automática
Exemplo de código prático
É muito mais fácil entender o poder e a responsabilidade desse recurso quando analisamos um código pronto funcionando na prática.
Abaixo temos um exemplo de um componente de contador customizado.
import { Component, ChangeDetectionStrategy, forwardRef, signal } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
@Component({
selector: 'app-custom-counter',
template: `
<button type="button" (click)="decrement()" [disabled]="disabled()">-</button>
<span>{{ count() }}</span>
<button type="button" (click)="increment()" [disabled]="disabled()">+</button>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomCounterComponent),
multi: true
}
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomCounterComponent implements ControlValueAccessor {
count = signal<number>(0)
disabled = signal<boolean>(false)
// Funções vazias que serão substituídas pelo Angular
onChange = (value: number) => {}
onTouched = () => {}
increment() {
if (this.disabled()) return
this.count.update((current) => current + 1)
this.onChange(this.count())
this.onTouched()
}
decrement() {
if (this.disabled()) return
this.count.update((current) => current - 1)
this.onChange(this.count())
this.onTouched()
}
// Recebe o valor inicial ou atualizado do formulário
writeValue(value: number): void {
if (value !== undefined && value !== null) {
this.count.set(value)
}
}
// Registra a função de callback para quando o valor mudar
registerOnChange(fn: any): void {
this.onChange = fn
}
// Registra a função de callback para quando o componente for tocado
registerOnTouched(fn: any): void {
this.onTouched = fn
}
// Atualiza o estado visual do componente caso o formulário seja desabilitado
setDisabledState(isDisabled: boolean): void {
this.disabled.set(isDisabled)
}
}
Agora que o nosso componente está pronto, precisamos consumi-lo em algum lugar da nossa aplicação.
Veja abaixo como é simples utilizar o CustomCounterComponent dentro de um Reactive Form, tratando-o como se fosse um input HTML nativo:
import { Component, ChangeDetectionStrategy } from '@angular/core'
import { FormControl, ReactiveFormsModule } from '@angular/forms'
import { CustomCounterComponent } from './custom-counter.component'
@Component({
selector: 'app-checkout',
imports: [ReactiveFormsModule, CustomCounterComponent],
template: `
<form>
<label>Quantidade de ingressos:</label>
<app-custom-counter [formControl]="quantityControl"></app-custom-counter>
</form>
<p>Você selecionou {{ quantityControl.value }} ingresso(s).</p>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CheckoutComponent {
// O controle gerencia o valor e a comunicação com o componente
quantityControl = new FormControl(1)
}
O que é a interface ControlValueAccessor
No ecossistema do Angular, o ControlValueAccessor é uma interface do TypeScript que dita um contrato estrito que o seu componente precisa respeitar.
Ao implementar essa interface na sua classe, você é obrigado a estruturar os seguintes métodos para garantir a comunicação correta:
writeValue(obj: any): É chamado pelo Angular para enviar um valor do modelo de dados para dentro do componente visual. É aqui que você atualiza o que o usuário vê na tela.registerOnChange(fn: any): Serve para registrar uma função de callback. O seu componente deve chamar essa função interna sempre que o usuário alterar o valor do campo, avisando o Angular sobre a nova informação.registerOnTouched(fn: any): Registra outra função de callback. Você deve chamá-la quando o usuário interagir com o componente (como clicar e sair do campo), marcando-o como “tocado” para que o Angular possa exibir mensagens de erro de validação, se necessário.setDisabledState(isDisabled: boolean): Embora opcional na interface, é altamente recomendado. O Angular chama este método quando o status muda para habilitado ou desabilitado, permitindo que você bloqueie visualmente as interações no seu componente customizado.
O que é o NG_VALUE_ACCESSOR
O NG_VALUE_ACCESSOR é um “token” especial de injeção de dependência fornecido pelo Angular.
Imagine-o como uma etiqueta de identificação oficial do framework.
Quando você adiciona essa etiqueta aos provedores (providers) do seu componente customizado, está essencialmente enviando uma mensagem para o Angular.
A mensagem diz: “Este componente sabe como lidar com valores e regras de validação, pode usá-lo com segurança através da diretiva formControlName!”.
Por que usar a função “forwardRef”
A função forwardRef é uma solução elegante do Angular para lidar com um problema clássico da linguagem JavaScript.
Esse problema é conhecido como elevação (hoisting) de classes.
Quando estamos configurando o NG_VALUE_ACCESSOR nos provedores do componente, precisamos fazer uma referência à própria classe desse componente.
Porém, como esse código é executado antes da classe terminar de ser construída na memória, a referência daria um erro de “não definido”.
O forwardRef resolve isso dizendo ao Angular para esperar e buscar a referência da classe apenas no momento em que ela realmente for necessária.
Flexibilidade Total: Compatibilidade com Template-Driven Forms e Reactive Forms
O Angular oferece duas abordagens principais para lidar com formulários:
- Os Template-Driven Forms (baseados em diretivas diretamente no HTML, como o
ngModel) - Os Reactive Forms (baseados em objetos e reatividade no código TypeScript, como o
formControlName)
A grande vantagem de investir tempo criando um componente com Control Value Accessor é que ele se torna universal dentro do ecossistema do Angular.
Isso significa que, uma vez que você construiu e exportou o seu componente customizado, o desenvolvedor que for consumi-lo tem total liberdade para escolher a abordagem preferida.
Se a equipe prefere a simplicidade do [(ngModel)] através de Template-Driven Forms, o componente vai funcionar perfeitamente.
Da mesma forma, se a aplicação exige a robustez de um Reactive Form com [formControl], a comunicação de dados e as validações acontecerão com a exata mesma fluidez.
Conclusão
Em resumo, dominar a interface Control Value Accessor é um passo transformador para qualquer desenvolvedor que deseja arquitetar aplicações robustas e criar bibliotecas de componentes reutilizáveis no Angular.
Ele padroniza a forma como os seus elementos visuais se integram aos Reactive Forms e Template-Driven Forms, entregando uma experiência de uso que é:
- Consistente
- Versátil
- Livre de falhas
Com a chegada dos Signals e das novas diretrizes de código do Angular, construir esses componentes customizados ficou ainda mais performático e fácil de manter.
Ao entender plenamente como esse fluxo de dados bidirecional acontece por debaixo dos panos, você adquire controle absoluto sobre a entrada de dados da sua aplicação e eleva o nível das suas interfaces.
Assine nossa Newsletter
Receba novos posts como esse na sua caixa de e-mail!
Sobre o autor