Crie inputs personalizados no Angular com ControlValueAccessor

Angular

Crie inputs personalizados no Angular com ControlValueAccessor

Gabriel Nascimento
Escrito por Gabriel Nascimento em 29 de março de 2023

Se você estiver trabalhando em um projeto complexo, provavelmente vai precisar criar um campo de formulário personalizado.

Existem alguns artigos que explicam como fazer, mas também é importante ter uma visão sobre o papel desse componente na arquitetura Angular.

Afinal, quando você lida com mecanismos mais complexos, é normal precisar entender o seu funcionamento…

E não apenas seguir um passo a passo de implementação.

Por isso, se quiser saber não apenas como implementar, mas também o “por quê”… esse artigo é para você.

Comece pelo FormControl

Se você já trabalhou com forms no Angular, então deve conhecer com o FormControl

Caso não conheça, é um objeto que rastreia o valor e o status de validação de um campo de formulário (ou seja, elementos como input, textarea, select…)

É importante entender que, quando você trabalha com formulários, objetos de FormControl sempre são criados. Independente de você usar template driven ou reactive forms.

Com a abordagem reativa, você mesmo cria e usa o FormControl, e a diretiva formControlName, para vinculá-lo a um campo de formulário:

@Component({
    selector: 'app-reactive-form',
    template: `
        <form [formGroup]="formulario">
             <input type="text" formControlName="nome">
             <input type="email" formControlName="email">
        </form>
    `
})
export class ReactiveFormComponent implements OnInit {

    formulario: FormGroup;

    ngOnInit() {
        this.formulario = new FormGroup({
            nome: new FormControl(null),    <---------------- Aqui
            email: new FormControl(null)    <---------------- E aqui
        }
    }
}

Mas, se você usar a abordagem baseada em template, o FormControl é criado implicitamente pela diretiva ngModel:

@Directive({
    selector: '[ngModel]',
    ...
})
export class NgModel ... {
    _control = new FormControl();    <---------------- Aqui

Perceba que o FormControl interage com um campo de formulário nativo. Como por exemplo: input ou textarea.

Porém você também pode usar em campos de formulário personalizados

O que é campo de formulário personalizado?

Em vez de um campo de formulário nativo, você também pode criar um campo personalizado…

Sendo que os campos personalizados também interagem com um objeto FormControl.

Como por exemplo, um componente Angular atuando como input:

<form [formGroup]="formulario">
    ...
    <ng-counter-input formControlName="contador"></ng-counter-input>
</form>

O que é muito útil, pois o número de elementos nativos é limitado…

Mas para isso, o Angular precisa de um mecanismo genérico que fica entre o FormControl e o campo do formulário (seja ele nativo ou personalizado).

É aqui que o objeto ControlValueAccessor entra em jogo.

Entenda o objeto ControlValueAccessor

O ControlValueAccessor é o objeto que fica entre o Angular FormControl e o campo do formulário… A fim de sincronizar os 2.

Portanto ele atua como uma ponte entre o Angular forms API e o elemento no DOM.

Esquema de ControlValueAccessor

Qualquer componente ou diretiva pode ser transformado em ControlValueAccessor, basta:

  • Se registrar como um provider NG_VALUE_ACCESSOR;
  • E implementar a interface ControlValueAccessor.

O Angular usa o NG_VALUE_ACCESSOR para configurar a sincronização com o FormControl.

Geralmente é a classe do componente ou diretiva que registra o provider…

@Component({
  selector: 'ng-counter-input',
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: NgCounterInputComponent,
    multi: true
  }]
  ...
})
export class NgCounterInputComponent {...}

Note que definimos o provider direto no descritor do componente. Além disso, não podemos ter mais de um accessor definido por elemento.

Depois disso você precisa implementar a interface:

@Component({
  selector: 'ng-counter-input',
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: NgCounterInputComponent,
    multi: true
  }]
  ...
})
export class NgCounterInputComponent implements ControlValueAccessor {...}    <---------------- Aqui

Pronto, agora temos um campo de formulário personalizado?

Não! Ainda precisamos codificar seu comportamento…

Como usar o ControlValueAccessor?

ilustração de carro no portão

A interface ControlValueAccessor define 2 métodos importantes:

  • writeValue;
  • E registerOnChange.
interface ControlValueAccessor {
    writeValue(obj: any): void
    registerOnChange(fn: any): void
    ...
    // Métodos menos importantes aqui
}

O método writeValue é usado para definir o valor do campo.

Já o registerOnChange serve para registrar uma função que deve ser acionada toda vez que o campo for atualizado… Assim ele repassa esse valor ao objeto FormControl.

Então, na essência, teríamos um código parecido com esse:

...
export class NgCounterInputComponent implements ControlValueAccessor {

    value: number;
    onChange: any;

    writeValue(value: number): void {
        this.value = value;
    }
    
    registerOnChange(fn: any): void {
        this.onChange = fn;
    }
    ...
}

Veja também esse diagrama que ilustra a interação:

Diagrama de uso do ControlValueAccessor

Exemplo prático de uso: <ng-counter-input>

ilustração de um timer

Por último, vamos ver um exemplo real de input personalizado.

A ideia é criar um contador numérico com 2 botões: + e -.

@Component({
    selector: 'ng-counter-input',
    template: `
        <div>
             <label>{{ value }}</labe>
             <button> + </button>
             <button> - </button>
        </div>
    `
})
export class NgCounterInputComponent {...}

Seu comportamento é simples:

  • Quando apertar no botão +, deve somar 1 ao valor do input;
  • E quando apertar no botão -, deve subtrair 1.

Em primeiro lugar, você deve configurar o ControlValueAccessor como vimos nos tópicos anteriores:

@Component({
    selector: 'ng-counter-input',
    template: `
        <div>
             <label>{{ value }}</labe>
             <button> + </button>
             <button> - </button>
        </div>
    `,
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: NgCounterInputComponent,
        multi: true
    }]
})
export class NgCounterInputComponent implements ControlValueAccessor {...}

Depois escrever as funções writeValue e registerOnChange:

...
export class NgCounterInputComponent implements ControlValueAccessor {

    value: number;
    onChange: any;

    writeValue(value: number): void {
        this.value = value;
    }
    
    registerOnChange(fn: any): void {
        this.onChange = fn;
    }
}

Porém tem uma pegadinha…

Nesse projeto o valor mínimo do input é zero, então devemos garantir isso ao receber o valor na função writeValue:

...
export class NgCounterInputComponent implements ControlValueAccessor {

    value: number;
    onChange: any;

    writeValue(value: number): void {
        if(value < 0) {
            this.value = 0;
        } else {
            this.value = value;
        }
    }
    
    registerOnChange(fn: any): void {
        this.onChange = fn;
    }
}

Terminamos? Ainda não… precisamos escrever o comportamento dos botões…

E para isso vamos criar 2 funções:

  • increment: para somar mais 1;
  • decrement: para diminuir 1;
@Component({
    selector: 'ng-counter-input',
    template: `
        <div>
             <label>{{ value }}</labe>
             <button (click)="increment()"> + </button>
             <button (click)="decrement()"> - </button>
        </div>
    `,
    ...
})
export class NgCounterInputComponent implements ControlValueAccessor {

    value: number;
    onChange: any;

    increment(): void {
        this.value++;
        this.onChange(this.value);
    }

    decrement(): void {
        if(this.value > 0) {
            this.value--;
            this.onChange(this.value);
        }
    }
    ...
}

Perceba que as 2 funções fazem uso do onChange para avisar ao FormControl que houve uma mudança.

Além disso, a função decrement implementa nossa regra de negócio onde o valor não pode ser menor que zero.

Agora sim nosso input personalizado está completo! Veja como ficou o código abaixo:

@Component({
    selector: 'ng-counter-input',
    template: `
        <div>
             <label>{{ value }}</labe>
             <button (click)="increment()"> + </button>
             <button (click)="decrement()"> - </button>
        </div>
    `,
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: NgCounterInputComponent,
        multi: true
    }]
})
export class NgCounterInputComponent implements ControlValueAccessor {

    value: number;
    onChange: any;

    increment(): void {
        this.value++;
        this.onChange(this.value);
    }

    decrement(): void {
        if(this.value > 0) {
            this.value--;
            this.onChange(this.value);
        }
    }
    
    writeValue(value: number): void {
        if(value < 0) {
            this.value = 0;
        } else {
            this.value = value;
        }
    }
    
    registerOnChange(fn: any): void {
        this.onChange = fn;
    }
}

Conclusão

Claro que não abordamos tudo nesse único artigo, há mais funções disponíveis para você usar…

Como por exemplo, o método registerOnTouched, que indicar quando um usuário interage com o input.

Caso queira se aprofundar nesse mecanismo, recomendo que dê uma olhada na documentação aqui.

E se você gostou desse artigo, ou ficou com alguma dúvida, deixe seu comentário abaixo:

Hey,

o que você achou deste conteúdo? Conte nos comentários.

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

One Reply to “Crie inputs personalizados no Angular com ControlValueAccessor”

Gabriel Nascimento

Quando precisei aprender a fazer um input personalizado esse artigo do indepth me ajudou bastante, então decidi fazer um resumo em português baseado nele. Ele lida de maneira mais aprofundada, recomendo muito a leitura. 😉