Publicado em

- 6 minutos de leitura

Control Value Accessor: A Ponte Entre o DOM e o Angular

img of 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)
}

Imagem com o texto: Curso Formulários com Angular e com a logo do Angular em cor roxa. Logo abaixo existe um botão com o texto "Eu quero!"

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

Author's photo
Henrique Custódia Arquiteto Frontend, entusiasta Angular, cat lover, criador de conteúdo e fundador da Code Dimension!