3 formas de criar validações condicionais com Angular Reactive Forms

Andrew Rosário
6 min readSep 13, 2021

--

Atenção: Este artigo foi originalmente escrito por Yury Katkov. Veja o link original aqui.

Imagine que você tem um formulário com um checkbox e um campo de e-mail. O campo de e-mail deve possuir uma validação com um padrão regex e possuir no máximo 250 caracteres e no mínimo 5 caracteres. Quando o checkbox estiver marcado, você precisa tornar esse campo de e-mail obrigatório. Quando o checkbox estiver desmarcado, o campo de e-mail se tornará opcional. Podemos chamar essa situação de validação dinâmica ou condicional.

Essa parece ser uma tarefa trivial no Angular. Vamos criar uma estrutura HTML e um código TypeScript necessário. Utilizarei os Reactive Forms do Angular, então certifique-se de ler a documentação.

Configurando

No código HTML temos um formulário que usa o formGroup armazenado em AppComponent.myForm, e na hora de enviar o formulário chamaremos o método AppComponent.onSubmit().

Em nosso formulário, temos os controles myCheckboxe myEmailField. Também adicionei um console.log para sempre saber seus valores no momento do envio do mesmo.

Vamos dar uma olhada no código:

Como você pode ver, o controle de e-mail possui três validadores anexados: o validador regex, o de tamanho máximo e tamanho mínimo.

Uma abordagem ingênua e cheia de erros

Depois que estiver tudo pronto, vamos tentar implementar a validação condicional. Se o valor do checkbox for true, definiremos o validador required para myEmailField. Para conseguir isso, precisaremos nos inscrever no observable valueChangesde nosso checkbox e usar a funçãosetValidators da classe AbstractControl:

Se você já tentou utilizar o setValidatorse clearValidatorsantes, notará um bug imediatamente. Ao ativar o validador required para myEmailField, todos os validadores existentes serão perdidos, portanto o e-mail preenchido não terá mais validação de tamanho e regex. O mesmo é verdade para clearValidators: removerá todas as validações do campo de e-mail.

Você pode ver esse comportamento por si só usando este exemplo do stackblitz:

Análise

O problema é que setValidatorssubstitui a lista de validadores em vez de adicionar os novos.

Então, se existe o setValidators, o Angular provavelmente deve possuir o getValidatorstambém, certo? Não, não existe getValidators, addValidatorse nemremoveValidators. Além disso, esses métodos provavelmente nunca serão implementados no futuro, e há um bom motivo para isso. Eu explico nessa issue no github do Angular .

Vamos tentar encontrar uma solução alternativa.

Solução 1: armazenar todos os validadores padrão

A primeira solução alternativa será a de salvar o array dos validadores padrão do controle de e-mail em uma variável. Usaremos esta variável para inicializar o formulário.

Quando chegar a hora de tornar o myEmailFieldobrigatório, iremos apenas adicionarValidators.requireda esse array usando a função concat:

Como você pode ver, funciona muito bem. Se desejar, você também pode chamar o método updateValueAndValidity se quiser que os erros apareçam logo após o checkbox ser marcado.

Aqui está a solução e sua versão refatorada.

Solução 2: criar um form validator condicional personalizado

Salvar os validadores como acabamos de fazer pode funcionar para os formulários simples, mas e se você tiver um formulário maior, com condições mais complexas? Posso garantir que você acabará com toneladas de código redundante. Não queremos código redundante. Queremos clareza.

Uma outra maneira de implementar a validação condicional seria escrever seu próprio validador vinculado ao seu form. Este validador irá verificar o valor de todo o formulário, incluindo o checkbox. Se o checkbox estiver marcado, exigirá que o myEmailFieldnão esteja vazio. Caso contrário, ele sempre retornará null, o que significa 'nenhum erro encontrado'. Como será um form validator, não haverá necessidade de chamar o setValidators pra cada controle.

Para adicionar um validador em umFormGroup, você precisa passar um segundo parâmetro para a chamada do FormBuilder.group():

A função emailConditionallyRequired é o nosso form validator. Ele aceita um FormGroup como parâmetro e tem acesso ao valor de todo o form em cada alteração:

Se o valor do controle myCheckbox for falso, nosso form validator dirá que nenhum problema foi encontrado. Se o checkbox estiver marcado, usaremos a função Validators.required do Angular para validar o campo de e-mail e se ele retornar um erro, geraremos nosso próprio objeto de erro.

Como você pode ver, a segunda solução alternativa leva a muito menos código. No entanto, tem suas desvantagens. De agora em diante, você precisa se lembrar que um dos validadores do campo de e-mail não é armazenado em formGroup.get('myEmailField').errors porque está anexado a um validador de todo o formulário. Lembre-se disso ao renderizar as mensagens de erro.

Solução 3: criar um control validator condicional personalizado

Então é possível superar as desvantagens da solução anterior? Vamos tentar criar um validador para o único controle de e-mail, que estaria ciente de seus arredores. Esse validador aceitaria o formControlcomo parâmetro do tipo AbstractControl. Cada AbstractControl tem acesso ao seu pai, então podemos ir para um FormGroup e ver o valor do checkbox!

O validador ficaria desta forma:

Excelente! Funciona! Observe, no entanto, que nosso validador só é acionado quando o valor do controle de e-mail é alterado. Porque? Porque o validador é anexado ao FormControl de e-mail e não observa seus pais. Se quiser acionar a validação condicional ao trocar o valor do checkbox, você precisará se inscrever para ouvir as alterações do checkbox e acionar manualmente a validação do controle de e-mail:

Ainda funciona! Bem, que tal tornar nosso validador condicional um pouco mais genérico? Podemos passar um predicado para nosso validador e com certeza ele parecerá profissional e ágil. Além disso, como ele não está mais vinculado ao campo de e-mail, podemos renomeá-lo de emailConditionalValidatorpara algo como requiredIfValidator:

Eu pararia por aqui se tudo o que precisássemos fosse tornar um controle obrigatório condicionalmente. E se você precisar adicionar mais validadores condicionalmente? Por exemplo, este campo deve possuir um padrão regex se o checkbox estiver marcado, e o valor máximo desse campo deve ser X se uma determinada condição for atendida.

Fácil! Basta passar o predicado como primeiro parâmetro, e o validador como segundo parâmetro. Vou apresentar o meu uber-super-martin-fowler-abstract-validator: conditionalValidator.

Observe como foi utilizado. O predicado que passo como primeiro parâmetro será verificado cada vez que a validação entrar em ação para o myEmailField. Você também pode passar qualquer validador como um segundo parâmetro, de modo que seja possível fazer todos os tipos de validação dinâmica, por exemplo:

Você pode ver a versão final aqui. Na minha opinião, é o melhor que podemos fazer para validar dinamicamente os controles:

Conclusão

É fácil criar seus validadores personalizados no Angular, mas por causa dessa simplicidade, não há como adicionar ou remover os validadores dinamicamente. As soluções alternativas são possíveis. Para os forms mais simples, a solução alternativa #1 pode ser suficiente. Se sua aplicação for grande e complexa, recomendo usar a solução alternativa #3.

Outras abordagens são possíveis, por exemplo, você pode combinar vários valueChangescom RxJS ou usar a função setError diretamente. Não acho que nenhum deles seja tão elegante e flexível quanto a solução alternativa # 3.

Outros links

Aqui estão alguns links que eu recomendo sobre validação dinâmica no Angular:

Bibliotecas relevantes

--

--

Andrew Rosário
Andrew Rosário

Written by Andrew Rosário

Desenvolvedor Front-end, mentor e palestrante. Apaixonado por tecnologia e por compartilhar conhecimento.

Responses (2)