Form Controls customizados no Angular com ControlValueAccessor
O Angular possui duas abordagens para lidar com formulários: Template-driven Forms e Reactive Forms. Ambas capturam eventos de entrada do usuário, validam esta entrada, e criam um modelo de dados para atualizar e fornecer um rastreamento de qualquer alteração.
Para criar um formulário, escolhemos uma das abordagens. Assim podemos facilmente vincular nossos controles como inputs, radio buttons ou checkboxes. Todos os controles nativos recebem um tratamento do próprio framework. Mas e quando precisamos criar controles customizados?
Neste artigo vamos construir um componente de avaliação. Com ele, podemos avaliar algo, de 1 a 5 estrelas. A ideia é que este componente possa ser reutilizado em qualquer formulário e "plugado" nas abordagens do Angular, assim como os controles nativos.
Conhecendo a interface ControlValueAccessor
Antes de partirmos para o código, precisamos entender a interface ControlValueAccessor
.
Para todos os controles padrão, o Angular disponibiliza uma implementação com esta interface, como por exemplo RadioControlValueAccessor. Isso significa que para criarmos nossos próprios controles customizados precisamos também seguir esta abordagem.
O ControlValueAccessor age como uma ponte entre os elementos do DOM e a API de Forms do Angular. Conectando-o em um componente, podemos manipular valores através do ngModel
(com Template-driven Forms) ou formControl
(com Reactive Forms).
Vamos analisar quais são os métodos que precisaremos implementar:
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
writeValue
- esta função é chamada pela API de Forms do Angular para atualizar o valor do controle. Quando o valor dengModel
ouformControl
é alterado, esta função é chamada e o valor mais recente é passado por parâmetro para a função. Podemos usar o valor mais recente e fazer alterações no componente com uma variável local. Você pode fazer uma relação dowriteValue
com o@Input
do componente.registerOnChange
- registra uma função de callback que devemos chamar sempre que o valor do controle é atualizado na UI. É ela que controla as atualizações no componente customizado. Você pode fazer uma relação doregisterOnChange
com o@Output
do componente.registerOnTouched
- esta função é utilizada para atualizar o estado do formulário paratouched
. Quando 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.setDisabledState
- esta função será chamada para informar a API de Forms quando o estado desabilitado for alterado. (true para desabilitado e false para habilitado). Podemos também obter esse estado, salvando em uma variável local para controlar adequadamente a UI do componente.
Agora que entendemos a anatomia do ControlValueAccessor
, vamos criar nosso componente de avaliação e implementar a interface:
Ao implementar ControlValueAccessor
, obrigatoriamente precisamos criar seus métodos.
Em writeValue
e setDisabledState
atribuímos o valor recebido para as variáveis locais do componente: value
e disabled
respectivamente. Perceba que essas variáveis foram definidas como protected
, pois não queremos que outros componentes possam alterar seus valores, garantindo seu encapsulamento. E como precisamos realizar o bind no template HTML, com private
isso não seria possível. A partir da versão 14 conseguimos realizar o bind de propriedades protected
.
Em registerOnChange
e registerOnTouched
apenas salvamos as funções de callback para podermos utilizá-las posteriormente.
Porém, somente implementar os métodos de ControlValueAccessor
não é o suficiente. É necessário registrar no array de providers do componente o token NG_VALUE_ACCESSOR
. Ele é o responsável pela integração do componente com a API de Forms do Angular.
Note também a presença do forwardRef
. Ele é necessário porque estamos nos referindo à classe StarRatingComponent
que não é definida antes de sua referência.
Agora que já temos a base pronta, basta criar a lógica para o usuário poder de fato utilizar o componente. O código TypeScript final ficou assim:
Temos o array de avaliações com a quantidade de estrelas e um texto informativo para cada uma. Atribuímos o decorator @Input
para deixar este valor customizável.
Por fim foi criado o método setRating
que será chamado toda vez que o usuário clicar em uma estrela para definir sua avaliação. Um ponto importante é que só atualizamos de fato o valor caso o controle esteja habilitado. É neste momento também que chamamos nossas funções salvas anteriormente.
Agora vamos criar o template HTML do nosso componente:
Aqui percorremos nosso array de avaliações para renderizar nossas estrelas. Para cada estrela atribuímos alguns estilos e também nosso evento de click que irá chamar o método setRating
. Temos duas variáveis locais para controlar o texto atual da avaliação e os eventos de mouseover
e mouseout
para entregar uma melhor experiência para o usuário.
Utilizando o componente customizado
Chegamos agora na melhor parte: a utilização da nossa solução em um formulário! Para este exemplo vamos utilizar os Reactive Forms, mas vale ressaltar que poderíamos utilizar com Template-driven Forms também.
Em um novo componente, criamos um FormGroup com dois campos: rating
e comment
. A grande vantagem é que no campo de avaliação temos a flexibilidade para informar opções adicionais e validações.
Veja no exemplo abaixo como é simples adicionar validadores e informar se o campo está habilitado ou desabilitado. Uma vez informado dentro do FormControl, o nosso componente customizado terá a inteligência para reagir a essas especificações.
O código final da implementação pode ser conferido pelo StackBlitz:
Conclusão
Neste artigo podemos entender como criar componentes de controles customizados que podem ser reutilizados em vários formulários no Angular, independente da solução adotada (Reactive Forms ou Template-driven Forms). Para isso foi introduzida a interface ControlValueAccessor
, que consegue em conjunto do token NG_VALUE_ACCESSOR
, conectar os recursos da API de Forms do Angular a um determinado componente.