Guias Detalhados
Forms

Validando entrada de formulário

Você pode melhorar a qualidade geral dos dados validando a entrada do usuário quanto à precisão e completude. Esta página mostra como validar a entrada do usuário a partir da UI e exibir mensagens de validação úteis, tanto em reactive forms quanto em template-driven forms.

Validando entrada em template-driven forms

Para adicionar validação a um template-driven form, você adiciona os mesmos atributos de validação que faria com validação de formulário HTML nativa. O Angular usa directives para corresponder esses atributos a funções validadoras no framework.

Toda vez que o valor de um form control muda, o Angular executa a validação e gera uma lista de erros de validação que resulta em um status INVALID, ou null, que resulta em um status VALID.

Você pode então inspecionar o estado do control exportando ngModel para uma variável local de template. O exemplo a seguir exporta NgModel para uma variável chamada name:

template/actor-form-template.component.html (name)

<div>  <h2>Template-Driven Form</h2>  <form #actorForm="ngForm" appUnambiguousRole>    <div [hidden]="actorForm.submitted">      <div class="cross-validation"           [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)">        <div class="form-group">          <label for="name">Name</label>          <input type="text" id="name" name="name" class="form-control"                 required minlength="4" appForbiddenName="bob"                 [(ngModel)]="actor.name" #name="ngModel">          @if (name.invalid && (name.dirty || name.touched)) {            <div class="alert">              @if (name.hasError('required')) {                <div>                  Name is required.                </div>              }              @if (name.hasError('minlength')) {                <div>                  Name must be at least 4 characters long.                </div>              }              @if (name.hasError('forbiddenName')) {                <div>                  Name cannot be Bob.                </div>              }            </div>          }        </div>        <div class="form-group">          <label for="role">Role</label>          <input type="text"                 id="role"                 name="role"                 #role="ngModel"                 [(ngModel)]="actor.role"                 [ngModelOptions]="{ updateOn: 'blur' }"                 appUniqueRole>          @if (role.pending) {            <div>Validating...</div>          }          @if (role.invalid) {            <div class="alert role-errors">              @if (role.hasError('uniqueRole')) {                <div>                  Role is already taken.                </div>              }            </div>          }        </div>        @if (actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)) {          <div class="cross-validation-error-message alert">            Name cannot match role.          </div>        }      </div>      <div class="form-group">        <label for="skill">Skill</label>        <select id="skill"                name="skill"                required [(ngModel)]="actor.skill"                #skill="ngModel">          @for (skill of skills; track $index) {            <option [value]="skill">{{ skill }}</option>          }        </select>        @if (skill.errors && skill.touched) {          <div class="alert">            @if (skill.errors['required']) {              <div>Skill is required.</div>            }          </div>        }      </div>      <p>Complete the form to enable the Submit button.</p>      <button type="submit"              [disabled]="actorForm.invalid">Submit      </button>      <button type="button"              (click)="actorForm.resetForm({})">Reset      </button>    </div>    @if (actorForm.submitted) {      <div class="submitted-message">        <p>You've submitted your actor, {{ actorForm.value.name }}!</p>        <button type="button" (click)="actorForm.resetForm({})">Add new actor</button>      </div>    }  </form></div>

Observe os seguintes recursos ilustrados pelo exemplo.

  • O elemento <input> carrega os atributos de validação HTML: required e minlength. Ele também carrega uma directive validadora personalizada, forbiddenName. Para mais informações, consulte a seção Validadores personalizados.

  • #name="ngModel" exporta NgModel para uma variável local chamada name. NgModel espelha muitas das propriedades de sua instância FormControl subjacente, então você pode usar isso no template para verificar estados de control como valid e dirty. Para uma lista completa de propriedades de control, consulte a referência da API AbstractControl.

    • O @if mais externo revela um conjunto de mensagens aninhadas, mas apenas se o name for inválido e o control estiver dirty ou touched.

    • Cada @if aninhado pode apresentar uma mensagem personalizada para um dos possíveis erros de validação. Existem mensagens para required, minlength e forbiddenName.

HELPFUL: Para evitar que o validador exiba erros antes que o usuário tenha a chance de editar o formulário, você deve verificar os estados dirty ou touched em um control.

  • Quando o usuário altera o valor no campo observado, o control é marcado como "dirty"
  • Quando o usuário desfoca o elemento form control, o control é marcado como "touched"

Validando entrada em reactive forms

Em um reactive form, a fonte da verdade é a classe do component. Em vez de adicionar validadores através de atributos no template, você adiciona funções validadoras diretamente ao form control model na classe do component. O Angular então chama essas funções sempre que o valor do control muda.

Funções validadoras

Funções validadoras podem ser síncronas ou assíncronas.

Tipo de validador Detalhes
Validadores síncronos Funções síncronas que recebem uma instância de control e retornam imediatamente um conjunto de erros de validação ou null. Passe-os como o segundo argumento ao instanciar um FormControl.
Validadores assíncronos Funções assíncronas que recebem uma instância de control e retornam uma Promise ou Observable que posteriormente emite um conjunto de erros de validação ou null. Passe-os como o terceiro argumento ao instanciar um FormControl.

Por razões de desempenho, o Angular executa validadores assíncronos apenas se todos os validadores síncronos passarem. Cada um deve ser concluído antes que os erros sejam definidos.

Funções validadoras nativas

Você pode optar por escrever suas próprias funções validadoras, ou pode usar alguns dos validadores nativos do Angular.

Os mesmos validadores nativos que estão disponíveis como atributos em template-driven forms, como required e minlength, estão todos disponíveis para uso como funções da classe Validators. Para uma lista completa de validadores nativos, consulte a referência da API Validators.

Para atualizar o formulário de ator para ser um reactive form, use alguns dos mesmos validadores nativos—desta vez, na forma de função, como no exemplo a seguir.

reactive/actor-form-reactive.component.ts (validator functions)

import {Component} from '@angular/core';import {FormControl, FormGroup, Validators, ReactiveFormsModule} from '@angular/forms';import {forbiddenNameValidator} from '../shared/forbidden-name.directive';@Component({  selector: 'app-actor-form-reactive',  templateUrl: './actor-form-reactive.component.html',  styleUrls: ['./actor-form-reactive.component.css'],  imports: [ReactiveFormsModule],})export class HeroFormReactiveComponent {  skills = ['Method Acting', 'Singing', 'Dancing', 'Swordfighting'];  actor = {name: 'Tom Cruise', role: 'Romeo', skill: this.skills[3]};  actorForm = new FormGroup({    name: new FormControl(this.actor.name, [      Validators.required,      Validators.minLength(4),      forbiddenNameValidator(/bob/i), // <-- Here's how you pass in the custom validator.    ]),    role: new FormControl(this.actor.role),    skill: new FormControl(this.actor.skill, Validators.required),  });  get name() {    return this.actorForm.get('name');  }  get skill() {    return this.actorForm.get('skill');  }}

Neste exemplo, o control name configura dois validadores nativos—Validators.required e Validators.minLength(4)— e um validador personalizado, forbiddenNameValidator.

Todos esses validadores são síncronos, então são passados como o segundo argumento. Observe que você pode suportar vários validadores passando as funções como um array.

Este exemplo também adiciona alguns métodos getter. Em um reactive form, você sempre pode acessar qualquer form control através do método get em seu grupo pai, mas às vezes é útil definir getters como atalho para o template.

Se você olhar para o template da entrada name novamente, é bastante semelhante ao exemplo template-driven.

reactive/actor-form-reactive.component.html (name with error msg)

<div class="container">  <h2>Reactive Form</h2>  <form [formGroup]="actorForm" #formDir="ngForm">    <div [hidden]="formDir.submitted">      <div class="cross-validation"           [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)">        <div class="form-group">          <label for="name">Name</label>          <input type="text" id="name" class="form-control"                 formControlName="name" required>          @if (name.invalid && (name.dirty || name.touched)) {            <div class="alert alert-danger">              @if (name.hasError('required')) {                <div>                  Name is required.                </div>              }              @if (name.hasError('minlength')) {                <div>                  Name must be at least 4 characters long.                </div>              }              @if (name.hasError('forbiddenName')) {                <div>                  Name cannot be Bob.                </div>              }            </div>          }        </div>        <div class="form-group">          <label for="role">Role</label>          <input type="text" id="role" class="form-control"                 formControlName="role">          @if (role.pending) {            <div>Validating...</div>          }          @if (role.invalid) {            <div class="alert alert-danger role-errors">              @if (role.hasError('uniqueRole')) {                <div>                  Role is already taken.                </div>              }            </div>          }        </div>        @if (actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)) {          <div class="cross-validation-error-message alert alert-danger">            Name cannot match role or audiences will be confused.          </div>        }      </div>      <div class="form-group">        <label for="skill">Skill</label>        <select id="skill" class="form-control"                formControlName="skill" required>          @for (skill of skills; track $index) {            <option [value]="skill">{{ skill }}</option>          }        </select>        @if (skill.invalid && skill.touched) {          <div class="alert alert-danger">            @if (skill.hasError('required')) {              <div>Skill is required.</div>            }          </div>        }      </div>      <p>Complete the form to enable the Submit button.</p>      <button type="submit"              class="btn btn-default"              [disabled]="actorForm.invalid">Submit      </button>      <button type="button" class="btn btn-default"              (click)="formDir.resetForm({})">Reset      </button>    </div>  </form>  @if (formDir.submitted) {    <div class="submitted-message">      <p>You've submitted your actor, {{ actorForm.value.name }}!</p>      <button type="button" (click)="formDir.resetForm({})">Add new actor</button>    </div>  }</div>

Este formulário difere da versão template-driven pelo fato de não exportar mais nenhuma directive. Em vez disso, ele usa o getter name definido na classe do component.

Observe que o atributo required ainda está presente no template. Embora não seja necessário para validação, ele deve ser mantido para fins de acessibilidade.

Definindo validadores personalizados

Os validadores nativos nem sempre correspondem ao caso de uso exato da sua aplicação, então às vezes você precisa criar um validador personalizado.

Considere a função forbiddenNameValidator do exemplo anterior. Aqui está a definição dessa função.

shared/forbidden-name.directive.ts (forbiddenNameValidator)

import {Directive, forwardRef, input} from '@angular/core';import {  AbstractControl,  NG_VALIDATORS,  ValidationErrors,  Validator,  ValidatorFn,} from '@angular/forms';/** An actor's name can't match the given regular expression */export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {  return (control: AbstractControl): ValidationErrors | null => {    const forbidden = nameRe.test(control.value);    return forbidden ? {forbiddenName: {value: control.value}} : null;  };}@Directive({  selector: '[appForbiddenName]',  providers: [    {      provide: NG_VALIDATORS,      useExisting: forwardRef(() => ForbiddenValidatorDirective),      multi: true,    },  ],})export class ForbiddenValidatorDirective implements Validator {  readonly forbiddenName = input<string>('', {alias: 'appForbiddenName'});  validate(control: AbstractControl): ValidationErrors | null {    return this.forbiddenName      ? forbiddenNameValidator(new RegExp(this.forbiddenName(), 'i'))(control)      : null;  }}

A função é uma fábrica que recebe uma expressão regular para detectar um nome proibido específico e retorna uma função validadora.

Nesta amostra, o nome proibido é "bob", então o validador rejeita qualquer nome de ator contendo "bob". Em outro lugar, poderia rejeitar "alice" ou qualquer nome que a expressão regular configurada corresponda.

A fábrica forbiddenNameValidator retorna a função validadora configurada. Essa função recebe um objeto de control Angular e retorna ou null se o valor do control for válido ou um objeto de erro de validação. O objeto de erro de validação normalmente tem uma propriedade cujo nome é a chave de validação, 'forbiddenName', e cujo valor é um dicionário arbitrário de valores que você poderia inserir em uma mensagem de erro, {name}.

Validadores assíncronos personalizados são semelhantes aos validadores síncronos, mas devem retornar uma Promise ou observable que posteriormente emite null ou um objeto de erro de validação. No caso de um observable, o observable deve ser completado, momento em que o formulário usa o último valor emitido para validação.

Adicionando validadores personalizados a reactive forms

Em reactive forms, adicione um validador personalizado passando a função diretamente para o FormControl.

reactive/actor-form-reactive.component.ts (validator functions)

import {Component} from '@angular/core';import {FormControl, FormGroup, Validators, ReactiveFormsModule} from '@angular/forms';import {forbiddenNameValidator} from '../shared/forbidden-name.directive';@Component({  selector: 'app-actor-form-reactive',  templateUrl: './actor-form-reactive.component.html',  styleUrls: ['./actor-form-reactive.component.css'],  imports: [ReactiveFormsModule],})export class HeroFormReactiveComponent {  skills = ['Method Acting', 'Singing', 'Dancing', 'Swordfighting'];  actor = {name: 'Tom Cruise', role: 'Romeo', skill: this.skills[3]};  actorForm = new FormGroup({    name: new FormControl(this.actor.name, [      Validators.required,      Validators.minLength(4),      forbiddenNameValidator(/bob/i), // <-- Here's how you pass in the custom validator.    ]),    role: new FormControl(this.actor.role),    skill: new FormControl(this.actor.skill, Validators.required),  });  get name() {    return this.actorForm.get('name');  }  get skill() {    return this.actorForm.get('skill');  }}

Adicionando validadores personalizados a template-driven forms

Em template-driven forms, adicione uma directive ao template, onde a directive encapsula a função validadora. Por exemplo, a ForbiddenValidatorDirective correspondente serve como um wrapper em torno do forbiddenNameValidator.

O Angular reconhece o papel da directive no processo de validação porque a directive se registra com o provider NG_VALIDATORS, conforme mostrado no exemplo a seguir. NG_VALIDATORS é um provider predefinido com uma coleção extensível de validadores.

shared/forbidden-name.directive.ts (providers)

import {Directive, forwardRef, input} from '@angular/core';import {  AbstractControl,  NG_VALIDATORS,  ValidationErrors,  Validator,  ValidatorFn,} from '@angular/forms';/** An actor's name can't match the given regular expression */export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {  return (control: AbstractControl): ValidationErrors | null => {    const forbidden = nameRe.test(control.value);    return forbidden ? {forbiddenName: {value: control.value}} : null;  };}@Directive({  selector: '[appForbiddenName]',  providers: [    {      provide: NG_VALIDATORS,      useExisting: forwardRef(() => ForbiddenValidatorDirective),      multi: true,    },  ],})export class ForbiddenValidatorDirective implements Validator {  readonly forbiddenName = input<string>('', {alias: 'appForbiddenName'});  validate(control: AbstractControl): ValidationErrors | null {    return this.forbiddenName      ? forbiddenNameValidator(new RegExp(this.forbiddenName(), 'i'))(control)      : null;  }}

A classe directive então implementa a interface Validator, para que possa se integrar facilmente com formulários Angular. Aqui está o resto da directive para ajudá-lo a entender como tudo se encaixa.

shared/forbidden-name.directive.ts (directive)

import {Directive, forwardRef, input} from '@angular/core';import {  AbstractControl,  NG_VALIDATORS,  ValidationErrors,  Validator,  ValidatorFn,} from '@angular/forms';/** An actor's name can't match the given regular expression */export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {  return (control: AbstractControl): ValidationErrors | null => {    const forbidden = nameRe.test(control.value);    return forbidden ? {forbiddenName: {value: control.value}} : null;  };}@Directive({  selector: '[appForbiddenName]',  providers: [    {      provide: NG_VALIDATORS,      useExisting: forwardRef(() => ForbiddenValidatorDirective),      multi: true,    },  ],})export class ForbiddenValidatorDirective implements Validator {  readonly forbiddenName = input<string>('', {alias: 'appForbiddenName'});  validate(control: AbstractControl): ValidationErrors | null {    return this.forbiddenName      ? forbiddenNameValidator(new RegExp(this.forbiddenName(), 'i'))(control)      : null;  }}

Uma vez que a ForbiddenValidatorDirective esteja pronta, você pode adicionar seu seletor, appForbiddenName, a qualquer elemento de entrada para ativá-la. Por exemplo:

template/actor-form-template.component.html (forbidden-name-input)

<div>  <h2>Template-Driven Form</h2>  <form #actorForm="ngForm" appUnambiguousRole>    <div [hidden]="actorForm.submitted">      <div class="cross-validation"           [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)">        <div class="form-group">          <label for="name">Name</label>          <input type="text" id="name" name="name" class="form-control"                 required minlength="4" appForbiddenName="bob"                 [(ngModel)]="actor.name" #name="ngModel">          @if (name.invalid && (name.dirty || name.touched)) {            <div class="alert">              @if (name.hasError('required')) {                <div>                  Name is required.                </div>              }              @if (name.hasError('minlength')) {                <div>                  Name must be at least 4 characters long.                </div>              }              @if (name.hasError('forbiddenName')) {                <div>                  Name cannot be Bob.                </div>              }            </div>          }        </div>        <div class="form-group">          <label for="role">Role</label>          <input type="text"                 id="role"                 name="role"                 #role="ngModel"                 [(ngModel)]="actor.role"                 [ngModelOptions]="{ updateOn: 'blur' }"                 appUniqueRole>          @if (role.pending) {            <div>Validating...</div>          }          @if (role.invalid) {            <div class="alert role-errors">              @if (role.hasError('uniqueRole')) {                <div>                  Role is already taken.                </div>              }            </div>          }        </div>        @if (actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)) {          <div class="cross-validation-error-message alert">            Name cannot match role.          </div>        }      </div>      <div class="form-group">        <label for="skill">Skill</label>        <select id="skill"                name="skill"                required [(ngModel)]="actor.skill"                #skill="ngModel">          @for (skill of skills; track $index) {            <option [value]="skill">{{ skill }}</option>          }        </select>        @if (skill.errors && skill.touched) {          <div class="alert">            @if (skill.errors['required']) {              <div>Skill is required.</div>            }          </div>        }      </div>      <p>Complete the form to enable the Submit button.</p>      <button type="submit"              [disabled]="actorForm.invalid">Submit      </button>      <button type="button"              (click)="actorForm.resetForm({})">Reset      </button>    </div>    @if (actorForm.submitted) {      <div class="submitted-message">        <p>You've submitted your actor, {{ actorForm.value.name }}!</p>        <button type="button" (click)="actorForm.resetForm({})">Add new actor</button>      </div>    }  </form></div>

HELPFUL: Observe que a directive de validação personalizada é instanciada com useExisting em vez de useClass. O validador registrado deve ser esta instância da ForbiddenValidatorDirective—a instância no formulário com sua propriedade forbiddenName vinculada a "bob".

Se você substituísse useExisting por useClass, então você estaria registrando uma nova instância de classe, uma que não tem forbiddenName.

Classes CSS de status de control

O Angular espelha automaticamente muitas propriedades de control no elemento form control como classes CSS. Use essas classes para estilizar elementos form control de acordo com o estado do formulário. As seguintes classes são atualmente suportadas.

  • .ng-valid
  • .ng-invalid
  • .ng-pending
  • .ng-pristine
  • .ng-dirty
  • .ng-untouched
  • .ng-touched
  • .ng-submitted (apenas elemento form envolvente)

No exemplo a seguir, o formulário de ator usa as classes .ng-valid e .ng-invalid para definir a cor da borda de cada form control.

forms.css (status classes)

.ng-valid[required], .ng-valid.required  {  border-left: 5px solid #42A948; /* green */}.ng-invalid:not(form)  {  border-left: 5px solid #a94442; /* red */}.alert div {  background-color: #fed3d3;  color: #820000;  padding: 1rem;  margin-bottom: 1rem;}.form-group {  margin-bottom: 1rem;}label {  display: block;  margin-bottom: .5rem;}select {  width: 100%;  padding: .5rem;}

Validação de campos cruzados

Um validador de campos cruzados é um validador personalizado que compara os valores de diferentes campos em um formulário e os aceita ou rejeita em combinação. Por exemplo, você pode ter um formulário que oferece opções mutuamente incompatíveis, de modo que se o usuário puder escolher A ou B, mas não ambos. Alguns valores de campo também podem depender de outros; um usuário pode ser permitido escolher B apenas se A também for escolhido.

Os exemplos de validação cruzada a seguir mostram como fazer o seguinte:

  • Validar entrada de formulário reactive ou baseado em template com base nos valores de dois controls irmãos,
  • Mostrar uma mensagem de erro descritiva depois que o usuário interagiu com o formulário e a validação falhou.

Os exemplos usam validação cruzada para garantir que os atores não reutilizem o mesmo nome em seu papel preenchendo o Formulário de Ator. Os validadores fazem isso verificando que os nomes e papéis dos atores não correspondem.

Adicionando validação cruzada a reactive forms

O formulário tem a seguinte estrutura:

const actorForm = new FormGroup({'name': new FormControl(),'role': new FormControl(),'skill': new FormControl()});

Observe que name e role são controls irmãos. Para avaliar ambos os controls em um único validador personalizado, você deve executar a validação em um control ancestral comum: o FormGroup. Você consulta o FormGroup por seus controls filhos para poder comparar seus valores.

Para adicionar um validador ao FormGroup, passe o novo validador como o segundo argumento na criação.

const actorForm = new FormGroup({'name': new FormControl(),'role': new FormControl(),'skill': new FormControl()}, { validators: unambiguousRoleValidator });

O código do validador é o seguinte.

shared/unambiguous-role.directive.ts

import {Directive, forwardRef} from '@angular/core';import {  AbstractControl,  NG_VALIDATORS,  ValidationErrors,  Validator,  ValidatorFn,} from '@angular/forms';/** An actor's name can't match the actor's role */export const unambiguousRoleValidator: ValidatorFn = (  control: AbstractControl,): ValidationErrors | null => {  const name = control.get('name');  const role = control.get('role');  return name && role && name.value === role.value ? {unambiguousRole: true} : null;};@Directive({  selector: '[appUnambiguousRole]',  providers: [    {      provide: NG_VALIDATORS,      useExisting: forwardRef(() => UnambiguousRoleValidatorDirective),      multi: true,    },  ],})export class UnambiguousRoleValidatorDirective implements Validator {  validate(control: AbstractControl): ValidationErrors | null {    return unambiguousRoleValidator(control);  }}

O validador unambiguousRoleValidator implementa a interface ValidatorFn. Ele recebe um objeto de control Angular como argumento e retorna null se o formulário for válido, ou ValidationErrors caso contrário.

O validador recupera os controls filhos chamando o método get do FormGroup, depois compara os valores dos controls name e role.

Se os valores não corresponderem, o papel é inequívoco, ambos são válidos e o validador retorna null. Se eles corresponderem, o papel do ator é ambíguo e o validador deve marcar o formulário como inválido retornando um objeto de erro.

Para fornecer uma melhor experiência ao usuário, o template mostra uma mensagem de erro apropriada quando o formulário é inválido.

reactive/actor-form-template.component.html

<div class="container">  <h2>Reactive Form</h2>  <form [formGroup]="actorForm" #formDir="ngForm">    <div [hidden]="formDir.submitted">      <div class="cross-validation"           [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)">        <div class="form-group">          <label for="name">Name</label>          <input type="text" id="name" class="form-control"                 formControlName="name" required>          @if (name.invalid && (name.dirty || name.touched)) {            <div class="alert alert-danger">              @if (name.hasError('required')) {                <div>                  Name is required.                </div>              }              @if (name.hasError('minlength')) {                <div>                  Name must be at least 4 characters long.                </div>              }              @if (name.hasError('forbiddenName')) {                <div>                  Name cannot be Bob.                </div>              }            </div>          }        </div>        <div class="form-group">          <label for="role">Role</label>          <input type="text" id="role" class="form-control"                 formControlName="role">          @if (role.pending) {            <div>Validating...</div>          }          @if (role.invalid) {            <div class="alert alert-danger role-errors">              @if (role.hasError('uniqueRole')) {                <div>                  Role is already taken.                </div>              }            </div>          }        </div>        @if (actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)) {          <div class="cross-validation-error-message alert alert-danger">            Name cannot match role or audiences will be confused.          </div>        }      </div>      <div class="form-group">        <label for="skill">Skill</label>        <select id="skill" class="form-control"                formControlName="skill" required>          @for (skill of skills; track $index) {            <option [value]="skill">{{ skill }}</option>          }        </select>        @if (skill.invalid && skill.touched) {          <div class="alert alert-danger">            @if (skill.hasError('required')) {              <div>Skill is required.</div>            }          </div>        }      </div>      <p>Complete the form to enable the Submit button.</p>      <button type="submit"              class="btn btn-default"              [disabled]="actorForm.invalid">Submit      </button>      <button type="button" class="btn btn-default"              (click)="formDir.resetForm({})">Reset      </button>    </div>  </form>  @if (formDir.submitted) {    <div class="submitted-message">      <p>You've submitted your actor, {{ actorForm.value.name }}!</p>      <button type="button" (click)="formDir.resetForm({})">Add new actor</button>    </div>  }</div>

Este @if exibe o erro se o FormGroup tiver o erro de validação cruzada retornado pelo validador unambiguousRoleValidator, mas apenas se o usuário terminou de interagir com o formulário.

Adicionando validação cruzada a template-driven forms

Para um template-driven form, você deve criar uma directive para encapsular a função validadora. Você fornece essa directive como validador usando o token NG_VALIDATORS token, conforme mostrado no exemplo a seguir.

shared/unambiguous-role.directive.ts

import {Directive, forwardRef} from '@angular/core';import {  AbstractControl,  NG_VALIDATORS,  ValidationErrors,  Validator,  ValidatorFn,} from '@angular/forms';/** An actor's name can't match the actor's role */export const unambiguousRoleValidator: ValidatorFn = (  control: AbstractControl,): ValidationErrors | null => {  const name = control.get('name');  const role = control.get('role');  return name && role && name.value === role.value ? {unambiguousRole: true} : null;};@Directive({  selector: '[appUnambiguousRole]',  providers: [    {      provide: NG_VALIDATORS,      useExisting: forwardRef(() => UnambiguousRoleValidatorDirective),      multi: true,    },  ],})export class UnambiguousRoleValidatorDirective implements Validator {  validate(control: AbstractControl): ValidationErrors | null {    return unambiguousRoleValidator(control);  }}

Você deve adicionar a nova directive ao template HTML. Como o validador deve ser registrado no nível mais alto no formulário, o template a seguir coloca a directive na tag form.

template/actor-form-template.component.html

<div>  <h2>Template-Driven Form</h2>  <form #actorForm="ngForm" appUnambiguousRole>    <div [hidden]="actorForm.submitted">      <div class="cross-validation"           [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)">        <div class="form-group">          <label for="name">Name</label>          <input type="text" id="name" name="name" class="form-control"                 required minlength="4" appForbiddenName="bob"                 [(ngModel)]="actor.name" #name="ngModel">          @if (name.invalid && (name.dirty || name.touched)) {            <div class="alert">              @if (name.hasError('required')) {                <div>                  Name is required.                </div>              }              @if (name.hasError('minlength')) {                <div>                  Name must be at least 4 characters long.                </div>              }              @if (name.hasError('forbiddenName')) {                <div>                  Name cannot be Bob.                </div>              }            </div>          }        </div>        <div class="form-group">          <label for="role">Role</label>          <input type="text"                 id="role"                 name="role"                 #role="ngModel"                 [(ngModel)]="actor.role"                 [ngModelOptions]="{ updateOn: 'blur' }"                 appUniqueRole>          @if (role.pending) {            <div>Validating...</div>          }          @if (role.invalid) {            <div class="alert role-errors">              @if (role.hasError('uniqueRole')) {                <div>                  Role is already taken.                </div>              }            </div>          }        </div>        @if (actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)) {          <div class="cross-validation-error-message alert">            Name cannot match role.          </div>        }      </div>      <div class="form-group">        <label for="skill">Skill</label>        <select id="skill"                name="skill"                required [(ngModel)]="actor.skill"                #skill="ngModel">          @for (skill of skills; track $index) {            <option [value]="skill">{{ skill }}</option>          }        </select>        @if (skill.errors && skill.touched) {          <div class="alert">            @if (skill.errors['required']) {              <div>Skill is required.</div>            }          </div>        }      </div>      <p>Complete the form to enable the Submit button.</p>      <button type="submit"              [disabled]="actorForm.invalid">Submit      </button>      <button type="button"              (click)="actorForm.resetForm({})">Reset      </button>    </div>    @if (actorForm.submitted) {      <div class="submitted-message">        <p>You've submitted your actor, {{ actorForm.value.name }}!</p>        <button type="button" (click)="actorForm.resetForm({})">Add new actor</button>      </div>    }  </form></div>

Para fornecer uma melhor experiência ao usuário, uma mensagem de erro apropriada aparece quando o formulário é inválido.

template/actor-form-template.component.html

<div>  <h2>Template-Driven Form</h2>  <form #actorForm="ngForm" appUnambiguousRole>    <div [hidden]="actorForm.submitted">      <div class="cross-validation"           [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)">        <div class="form-group">          <label for="name">Name</label>          <input type="text" id="name" name="name" class="form-control"                 required minlength="4" appForbiddenName="bob"                 [(ngModel)]="actor.name" #name="ngModel">          @if (name.invalid && (name.dirty || name.touched)) {            <div class="alert">              @if (name.hasError('required')) {                <div>                  Name is required.                </div>              }              @if (name.hasError('minlength')) {                <div>                  Name must be at least 4 characters long.                </div>              }              @if (name.hasError('forbiddenName')) {                <div>                  Name cannot be Bob.                </div>              }            </div>          }        </div>        <div class="form-group">          <label for="role">Role</label>          <input type="text"                 id="role"                 name="role"                 #role="ngModel"                 [(ngModel)]="actor.role"                 [ngModelOptions]="{ updateOn: 'blur' }"                 appUniqueRole>          @if (role.pending) {            <div>Validating...</div>          }          @if (role.invalid) {            <div class="alert role-errors">              @if (role.hasError('uniqueRole')) {                <div>                  Role is already taken.                </div>              }            </div>          }        </div>        @if (actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)) {          <div class="cross-validation-error-message alert">            Name cannot match role.          </div>        }      </div>      <div class="form-group">        <label for="skill">Skill</label>        <select id="skill"                name="skill"                required [(ngModel)]="actor.skill"                #skill="ngModel">          @for (skill of skills; track $index) {            <option [value]="skill">{{ skill }}</option>          }        </select>        @if (skill.errors && skill.touched) {          <div class="alert">            @if (skill.errors['required']) {              <div>Skill is required.</div>            }          </div>        }      </div>      <p>Complete the form to enable the Submit button.</p>      <button type="submit"              [disabled]="actorForm.invalid">Submit      </button>      <button type="button"              (click)="actorForm.resetForm({})">Reset      </button>    </div>    @if (actorForm.submitted) {      <div class="submitted-message">        <p>You've submitted your actor, {{ actorForm.value.name }}!</p>        <button type="button" (click)="actorForm.resetForm({})">Add new actor</button>      </div>    }  </form></div>

Isso é o mesmo tanto em template-driven quanto em reactive forms.

Criando validadores assíncronos

Validadores assíncronos implementam as interfaces AsyncValidatorFn e AsyncValidator. Estes são muito semelhantes aos seus equivalentes síncronos, com as seguintes diferenças.

  • As funções validate() devem retornar uma Promise ou um observable,
  • O observable retornado deve ser finito, ou seja, deve ser completado em algum momento. Para converter um observable infinito em um finito, faça o pipe do observable através de um operador de filtragem como first, last, take ou takeUntil.

A validação assíncrona acontece após a validação síncrona e é executada apenas se a validação síncrona for bem-sucedida. Essa verificação permite que os formulários evitem processos de validação assíncrona potencialmente caros (como uma solicitação HTTP) se os métodos de validação mais básicos já encontraram entrada inválida.

Após o início da validação assíncrona, o form control entra em um estado pending. Inspecione a propriedade pending do control e use-a para fornecer feedback visual sobre a operação de validação em andamento.

Um padrão de UI comum é mostrar um spinner enquanto a validação assíncrona está sendo executada. O exemplo a seguir mostra como alcançar isso em um template-driven form.

<input [(ngModel)]="name" #model="ngModel" appSomeAsyncValidator>@if(model.pending) {<app-spinner />}

Implementando um validador assíncrono personalizado

No exemplo a seguir, um validador assíncrono garante que os atores sejam escolhidos para um papel que ainda não está ocupado. Novos atores estão constantemente fazendo audições e atores antigos estão se aposentando, então a lista de papéis disponíveis não pode ser recuperada antecipadamente. Para validar a potencial entrada de papel, o validador deve iniciar uma operação assíncrona para consultar um banco de dados central de todos os atores atualmente escalados.

O código a seguir cria a classe validadora, UniqueRoleValidator, que implementa a interface AsyncValidator.

import {Directive, forwardRef, inject, Injectable} from '@angular/core';import {  AsyncValidator,  AbstractControl,  NG_ASYNC_VALIDATORS,  ValidationErrors,} from '@angular/forms';import {catchError, map} from 'rxjs/operators';import {ActorsService} from './actors.service';import {Observable, of} from 'rxjs';@Injectable({providedIn: 'root'})export class UniqueRoleValidator implements AsyncValidator {  private readonly actorsService = inject(ActorsService);  validate(control: AbstractControl): Observable<ValidationErrors | null> {    return this.actorsService.isRoleTaken(control.value).pipe(      map((isTaken) => (isTaken ? {uniqueRole: true} : null)),      catchError(() => of(null)),    );  }}@Directive({  selector: '[appUniqueRole]',  providers: [    {      provide: NG_ASYNC_VALIDATORS,      useExisting: forwardRef(() => UniqueRoleValidatorDirective),      multi: true,    },  ],})export class UniqueRoleValidatorDirective implements AsyncValidator {  private readonly validator = inject(UniqueRoleValidator);  validate(control: AbstractControl): Observable<ValidationErrors | null> {    return this.validator.validate(control);  }}

A propriedade actorsService é inicializada com uma instância do token ActorsService, que define a seguinte interface.

interface ActorsService {  isRoleTaken: (role: string) => Observable<boolean>;}

Em uma aplicação do mundo real, o ActorsService seria responsável por fazer uma solicitação HTTP ao banco de dados de atores para verificar se o papel está disponível. Do ponto de vista do validador, a implementação real do serviço não é importante, então o exemplo pode apenas codificar contra a interface ActorsService.

À medida que a validação começa, o UnambiguousRoleValidator delega ao método isRoleTaken() do ActorsService com o valor atual do control. Neste ponto, o control é marcado como pending e permanece neste estado até que a cadeia observable retornada do método validate() seja concluída.

O método isRoleTaken() despacha uma solicitação HTTP que verifica se o papel está disponível e retorna Observable<boolean> como resultado. O método validate() faz o pipe da resposta através do operador map e a transforma em um resultado de validação.

O método então, como qualquer validador, retorna null se o formulário for válido e ValidationErrors se não for. Este validador lida com quaisquer erros potenciais com o operador catchError. Neste caso, o validador trata o erro isRoleTaken() como uma validação bem-sucedida, porque a falha ao fazer uma solicitação de validação não significa necessariamente que o papel é inválido. Você poderia lidar com o erro de forma diferente e retornar o objeto ValidationError em vez disso.

Depois de algum tempo, a cadeia observable é concluída e a validação assíncrona é feita. A flag pending é definida como false e a validade do formulário é atualizada.

Adicionando validadores assíncronos a reactive forms

Para usar um validador assíncrono em reactive forms, comece injetando o validador em uma propriedade da classe do component.

import {Component, inject} from '@angular/core';import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';import {forbiddenNameValidator} from '../shared/forbidden-name.directive';import {UniqueRoleValidator} from '../shared/role.directive';@Component({  selector: 'app-actor-form-reactive',  templateUrl: './actor-form-reactive.component.html',  styleUrls: ['./actor-form-reactive.component.css'],  imports: [ReactiveFormsModule],})export class HeroFormReactiveComponent {  roleValidator = inject(UniqueRoleValidator);  skills = ['Method Acting', 'Singing', 'Dancing', 'Swordfighting'];  actor = {name: 'Tom Cruise', role: 'Romeo', skill: this.skills[3]};  actorForm!: FormGroup;  ngOnInit(): void {    const roleControl = new FormControl('', {      asyncValidators: [this.roleValidator.validate.bind(this.roleValidator)],      updateOn: 'blur',    });    roleControl.setValue(this.actor.role);    this.actorForm = new FormGroup({      name: new FormControl(this.actor.name, [        Validators.required,        Validators.minLength(4),        forbiddenNameValidator(/bob/i),      ]),      role: roleControl,      skill: new FormControl(this.actor.skill, Validators.required),    });  }  get name() {    return this.actorForm.get('name');  }  get skill() {    return this.actorForm.get('skill');  }  get role() {    return this.actorForm.get('role');  }}

Em seguida, passe a função validadora diretamente para o FormControl para aplicá-la.

No exemplo a seguir, a função validate de UnambiguousRoleValidator é aplicada a roleControl passando-a para a opção asyncValidators do control e vinculando-a à instância de UnambiguousRoleValidator que foi injetada em ActorFormReactiveComponent. O valor de asyncValidators pode ser uma única função validadora assíncrona ou um array de funções. Para saber mais sobre opções de FormControl, consulte a referência da API AbstractControlOptions.

import {Component, inject} from '@angular/core';import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';import {forbiddenNameValidator} from '../shared/forbidden-name.directive';import {UniqueRoleValidator} from '../shared/role.directive';@Component({  selector: 'app-actor-form-reactive',  templateUrl: './actor-form-reactive.component.html',  styleUrls: ['./actor-form-reactive.component.css'],  imports: [ReactiveFormsModule],})export class HeroFormReactiveComponent {  roleValidator = inject(UniqueRoleValidator);  skills = ['Method Acting', 'Singing', 'Dancing', 'Swordfighting'];  actor = {name: 'Tom Cruise', role: 'Romeo', skill: this.skills[3]};  actorForm!: FormGroup;  ngOnInit(): void {    const roleControl = new FormControl('', {      asyncValidators: [this.roleValidator.validate.bind(this.roleValidator)],      updateOn: 'blur',    });    roleControl.setValue(this.actor.role);    this.actorForm = new FormGroup({      name: new FormControl(this.actor.name, [        Validators.required,        Validators.minLength(4),        forbiddenNameValidator(/bob/i),      ]),      role: roleControl,      skill: new FormControl(this.actor.skill, Validators.required),    });  }  get name() {    return this.actorForm.get('name');  }  get skill() {    return this.actorForm.get('skill');  }  get role() {    return this.actorForm.get('role');  }}

Adicionando validadores assíncronos a template-driven forms

Para usar um validador assíncrono em template-driven forms, crie uma nova directive e registre o provider NG_ASYNC_VALIDATORS nela.

No exemplo abaixo, a directive injeta a classe UniqueRoleValidator que contém a lógica de validação real e a invoca na função validate, acionada pelo Angular quando a validação deve acontecer.

import {Directive, forwardRef, inject, Injectable} from '@angular/core';import {  AsyncValidator,  AbstractControl,  NG_ASYNC_VALIDATORS,  ValidationErrors,} from '@angular/forms';import {catchError, map} from 'rxjs/operators';import {ActorsService} from './actors.service';import {Observable, of} from 'rxjs';@Injectable({providedIn: 'root'})export class UniqueRoleValidator implements AsyncValidator {  private readonly actorsService = inject(ActorsService);  validate(control: AbstractControl): Observable<ValidationErrors | null> {    return this.actorsService.isRoleTaken(control.value).pipe(      map((isTaken) => (isTaken ? {uniqueRole: true} : null)),      catchError(() => of(null)),    );  }}@Directive({  selector: '[appUniqueRole]',  providers: [    {      provide: NG_ASYNC_VALIDATORS,      useExisting: forwardRef(() => UniqueRoleValidatorDirective),      multi: true,    },  ],})export class UniqueRoleValidatorDirective implements AsyncValidator {  private readonly validator = inject(UniqueRoleValidator);  validate(control: AbstractControl): Observable<ValidationErrors | null> {    return this.validator.validate(control);  }}

Em seguida, assim como com validadores síncronos, adicione o seletor da directive a uma entrada para ativá-la.

template/actor-form-template.component.html (unique-unambiguous-role-input)

<div>  <h2>Template-Driven Form</h2>  <form #actorForm="ngForm" appUnambiguousRole>    <div [hidden]="actorForm.submitted">      <div class="cross-validation"           [class.cross-validation-error]="actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)">        <div class="form-group">          <label for="name">Name</label>          <input type="text" id="name" name="name" class="form-control"                 required minlength="4" appForbiddenName="bob"                 [(ngModel)]="actor.name" #name="ngModel">          @if (name.invalid && (name.dirty || name.touched)) {            <div class="alert">              @if (name.hasError('required')) {                <div>                  Name is required.                </div>              }              @if (name.hasError('minlength')) {                <div>                  Name must be at least 4 characters long.                </div>              }              @if (name.hasError('forbiddenName')) {                <div>                  Name cannot be Bob.                </div>              }            </div>          }        </div>        <div class="form-group">          <label for="role">Role</label>          <input type="text"                 id="role"                 name="role"                 #role="ngModel"                 [(ngModel)]="actor.role"                 [ngModelOptions]="{ updateOn: 'blur' }"                 appUniqueRole>          @if (role.pending) {            <div>Validating...</div>          }          @if (role.invalid) {            <div class="alert role-errors">              @if (role.hasError('uniqueRole')) {                <div>                  Role is already taken.                </div>              }            </div>          }        </div>        @if (actorForm.hasError('unambiguousRole') && (actorForm.touched || actorForm.dirty)) {          <div class="cross-validation-error-message alert">            Name cannot match role.          </div>        }      </div>      <div class="form-group">        <label for="skill">Skill</label>        <select id="skill"                name="skill"                required [(ngModel)]="actor.skill"                #skill="ngModel">          @for (skill of skills; track $index) {            <option [value]="skill">{{ skill }}</option>          }        </select>        @if (skill.errors && skill.touched) {          <div class="alert">            @if (skill.errors['required']) {              <div>Skill is required.</div>            }          </div>        }      </div>      <p>Complete the form to enable the Submit button.</p>      <button type="submit"              [disabled]="actorForm.invalid">Submit      </button>      <button type="button"              (click)="actorForm.resetForm({})">Reset      </button>    </div>    @if (actorForm.submitted) {      <div class="submitted-message">        <p>You've submitted your actor, {{ actorForm.value.name }}!</p>        <button type="button" (click)="actorForm.resetForm({})">Add new actor</button>      </div>    }  </form></div>

Otimizando o desempenho de validadores assíncronos

Por padrão, todos os validadores são executados após cada mudança de valor do formulário. Com validadores síncronos, isso normalmente não tem um impacto perceptível no desempenho da aplicação. Validadores assíncronos, no entanto, normalmente executam algum tipo de solicitação HTTP para validar o control. Despachar uma solicitação HTTP após cada pressionamento de tecla pode sobrecarregar a API backend e deve ser evitado, se possível.

Você pode atrasar a atualização da validade do formulário alterando a propriedade updateOn de change (padrão) para submit ou blur.

Com template-driven forms, defina a propriedade no template.

<input [(ngModel)]="name" [ngModelOptions]="{updateOn: 'blur'}">

Com reactive forms, defina a propriedade na instância FormControl.

new FormControl('', {updateOn: 'blur'});

Interação com validação de formulário HTML nativa

Por padrão, o Angular desabilita validação de formulário HTML nativa adicionando o atributo novalidate no <form> envolvente e usa directives para corresponder esses atributos a funções validadoras no framework. Se você quiser usar validação nativa em combinação com validação baseada em Angular, pode reativá-la com a directive ngNativeValidate. Consulte a documentação da API para detalhes.