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.
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?
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:
Exemplo prático de uso: <ng-counter-input>
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:
{ Angular Signals } Como deixar seu código mais reativo
[Infográfico] 28 Melhores Extensões do Visual Studio Code para Angular
{ Angular LifeCycle Hooks } Controle sua Aplicação como um Mestre Jedi
[Infográfico] Angular LifeCycle: O Guia Rápido de Referências do Jovem Padawan
Hey,
o que você achou deste conteúdo? Conte nos comentários.
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. 😉