Validadores síncronos y asíncronos

Avanzado
Angular
Angular
Actualizado: 24/09/2025

Validadores síncronos incorporados

Angular proporciona una clase Validators con un conjunto completo de validadores predefinidos que cubren los casos de uso más comunes en formularios empresariales. Estos validadores síncronos se ejecutan de forma inmediata cuando cambia el valor del control, proporcionando validación en tiempo real sin necesidad de operaciones asíncronas.

La clase Validators incluye métodos estáticos que retornan funciones de validación, las cuales pueden aplicarse tanto a FormControl individuales como a FormGroup completos. Cada validador retorna null cuando el valor es válido, o un objeto ValidationErrors cuando encuentra errores.

Validadores básicos de presencia y longitud

Los validadores más utilizados son aquellos que verifican la presencia y longitud de los datos:

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-user-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="userForm">
      <input 
        type="text" 
        formControlName="name" 
        placeholder="Nombre completo">
      
      <input 
        type="text" 
        formControlName="username" 
        placeholder="Nombre de usuario">
      
      <textarea 
        formControlName="bio" 
        placeholder="Biografía">
      </textarea>
    </form>
  `
})
export class UserFormComponent {
  userForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.userForm = this.fb.group({
      // Campo obligatorio
      name: ['', Validators.required],
      
      // Longitud mínima y máxima
      username: ['', [
        Validators.required,
        Validators.minLength(3),
        Validators.maxLength(20)
      ]],
      
      // Solo longitud máxima
      bio: ['', Validators.maxLength(500)]
    });
  }
}

Validadores de formato y patrones

Para datos que requieren formatos específicos, Angular incluye validadores especializados:

export class ContactFormComponent {
  contactForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.contactForm = this.fb.group({
      // Validación de email integrada
      email: ['', [
        Validators.required,
        Validators.email
      ]],
      
      // Validación con expresión regular personalizada
      phone: ['', [
        Validators.required,
        Validators.pattern(/^[0-9]{9}$/)
      ]],
      
      // Código postal español
      postalCode: ['', [
        Validators.required,
        Validators.pattern(/^[0-9]{5}$/)
      ]],
      
      // URL válida
      website: ['', Validators.pattern(/^https?:\/\/.+\..+/)]
    });
  }
}

Acceso a estados de validación

Los FormControl proporcionan múltiples propiedades para verificar el estado de validación y la interacción del usuario:

export class ValidationStatusComponent {
  profileForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.profileForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(8)]]
    });
  }

  checkValidationStates() {
    const emailControl = this.profileForm.get('email');
    
    // Estados de validación
    console.log('Valid:', emailControl?.valid);       // true/false
    console.log('Invalid:', emailControl?.invalid);   // true/false
    console.log('Errors:', emailControl?.errors);     // objeto con errores o null
    
    // Estados de interacción
    console.log('Touched:', emailControl?.touched);   // usuario tocó el campo
    console.log('Dirty:', emailControl?.dirty);       // usuario modificó el valor
    console.log('Pristine:', emailControl?.pristine); // valor no modificado
    
    // Estado del formulario completo
    console.log('Form valid:', this.profileForm.valid);
    console.log('Form errors:', this.profileForm.errors);
  }
}

Mostrar errores en el template

La integración con el template permite mostrar mensajes de error específicos utilizando la nueva sintaxis de control de flujo:

@Component({
  selector: 'app-registration',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="registrationForm">
      <div class="field">
        <input 
          type="email" 
          formControlName="email"
          placeholder="Email"
          [class.error]="emailControl.invalid && emailControl.touched">
        
        @if (emailControl.invalid && emailControl.touched) {
          <div class="error-messages">
            @if (emailControl.errors?.['required']) {
              <span>El email es obligatorio</span>
            }
            @if (emailControl.errors?.['email']) {
              <span>El formato del email no es válido</span>
            }
          </div>
        }
      </div>
      
      <div class="field">
        <input 
          type="password" 
          formControlName="password"
          placeholder="Contraseña">
        
        @if (passwordControl.invalid && passwordControl.touched) {
          <div class="error-messages">
            @if (passwordControl.errors?.['required']) {
              <span>La contraseña es obligatoria</span>
            }
            @if (passwordControl.errors?.['minlength']) {
              <span>Mínimo {{passwordControl.errors?.['minlength'].requiredLength}} caracteres</span>
            }
          </div>
        }
      </div>
      
      <button 
        type="submit" 
        [disabled]="registrationForm.invalid">
        Registrarse
      </button>
    </form>
  `
})
export class RegistrationComponent {
  registrationForm: FormGroup;
  
  // Getters para acceso fácil en template
  get emailControl() { return this.registrationForm.get('email')!; }
  get passwordControl() { return this.registrationForm.get('password')!; }

  constructor(private fb: FormBuilder) {
    this.registrationForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(8)]]
    });
  }
}

Combinando múltiples validadores

Los validadores pueden combinarse libremente para crear reglas de validación complejas:

export class ProductFormComponent {
  productForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.productForm = this.fb.group({
      // Múltiples validadores en array
      name: ['', [
        Validators.required,
        Validators.minLength(2),
        Validators.maxLength(100),
        Validators.pattern(/^[a-zA-Z0-9\s]+$/) // Solo alfanuméricos y espacios
      ]],
      
      // Precio con validaciones numéricas
      price: ['', [
        Validators.required,
        Validators.min(0.01),
        Validators.max(99999.99)
      ]],
      
      // SKU con formato específico
      sku: ['', [
        Validators.required,
        Validators.pattern(/^[A-Z]{2}-[0-9]{4}-[A-Z]{2}$/)
      ]],
      
      // Descripción opcional con límite
      description: ['', Validators.maxLength(1000)]
    });
  }
  
  // Método para obtener errores específicos
  getFieldErrors(fieldName: string): string[] {
    const control = this.productForm.get(fieldName);
    const errors: string[] = [];
    
    if (control?.errors && control.touched) {
      if (control.errors['required']) errors.push('Campo obligatorio');
      if (control.errors['minlength']) {
        errors.push(`Mínimo ${control.errors['minlength'].requiredLength} caracteres`);
      }
      if (control.errors['maxlength']) {
        errors.push(`Máximo ${control.errors['maxlength'].requiredLength} caracteres`);
      }
      if (control.errors['pattern']) errors.push('Formato no válido');
      if (control.errors['email']) errors.push('Email no válido');
      if (control.errors['min']) errors.push(`Valor mínimo: ${control.errors['min'].min}`);
      if (control.errors['max']) errors.push(`Valor máximo: ${control.errors['max'].max}`);
    }
    
    return errors;
  }
}

Los validadores síncronos proporcionan validación inmediata y son la base del sistema de validación de Angular. Su naturaleza síncrona los hace ideales para verificaciones que no requieren consultas externas, ofreciendo una experiencia de usuario fluida con retroalimentación instantánea sobre la validez de los datos ingresados.

Validadores asíncronos y manejo de estados

Los validadores asíncronos resuelven casos donde la validación requiere consultas externas como verificar disponibilidad de nombres de usuario, validar códigos únicos en bases de datos, o confirmar existencia de recursos remotos. A diferencia de los validadores síncronos, estos retornan Promise o Observable y introducen un estado adicional llamado pending.

Interfaz AsyncValidator

Los validadores asíncronos implementan la interfaz AsyncValidator y deben retornar una Promise o Observable que resuelve a ValidationErrors o null:

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, AsyncValidator, AbstractControl, ValidationErrors } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { Observable, of, delay, map, catchError } from 'rxjs';

@Component({
  selector: 'app-user-registration',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="registrationForm">
      <div class="field">
        <input 
          type="text" 
          formControlName="username"
          placeholder="Nombre de usuario">
        
        @if (usernameControl.pending) {
          <div class="checking">
            <span class="spinner"></span>
            Verificando disponibilidad...
          </div>
        }
        
        @if (usernameControl.invalid && usernameControl.touched && !usernameControl.pending) {
          <div class="error-messages">
            @if (usernameControl.errors?.['required']) {
              <span>El nombre de usuario es obligatorio</span>
            }
            @if (usernameControl.errors?.['usernameTaken']) {
              <span>Este nombre de usuario no está disponible</span>
            }
          </div>
        }
      </div>
    </form>
  `
})
export class UserRegistrationComponent {
  registrationForm: FormGroup;
  
  get usernameControl() { return this.registrationForm.get('username')!; }

  constructor(private fb: FormBuilder, private http: HttpClient) {
    this.registrationForm = this.fb.group({
      username: ['', 
        [Validators.required, Validators.minLength(3)], // Validadores síncronos
        [this.usernameAvailabilityValidator.bind(this)] // Validador asíncrono
      ]
    });
  }

  // Validador asíncrono personalizado
  usernameAvailabilityValidator(control: AbstractControl): Observable<ValidationErrors | null> {
    if (!control.value || control.value.length < 3) {
      return of(null); // No validar si no cumple requisitos síncronos
    }

    return this.http.get<{available: boolean}>(`/api/users/check-username/${control.value}`)
      .pipe(
        delay(500), // Simular latencia de red
        map(response => response.available ? null : { usernameTaken: true }),
        catchError(() => of(null)) // En caso de error, considerar válido
      );
  }
}

Estados de validación asíncrona

Los FormControl con validadores asíncronos introducen el estado pending, que indica que una validación está en progreso:

export class AsyncValidationStatesComponent {
  accountForm: FormGroup;

  constructor(private fb: FormBuilder, private http: HttpClient) {
    this.accountForm = this.fb.group({
      email: ['', 
        [Validators.required, Validators.email],
        [this.emailExistsValidator.bind(this)]
      ],
      companyCode: ['',
        [Validators.required],
        [this.companyCodeValidator.bind(this)]
      ]
    });
  }

  checkAsyncStates() {
    const emailControl = this.accountForm.get('email');
    
    // Estados específicos de validación asíncrona
    console.log('Pending:', emailControl?.pending);    // true durante validación
    console.log('Status:', emailControl?.status);      // 'PENDING', 'VALID', 'INVALID'
    
    // El formulario también puede estar pending
    console.log('Form pending:', this.accountForm.pending);
    console.log('Form status:', this.accountForm.status);
  }

  emailExistsValidator(control: AbstractControl): Observable<ValidationErrors | null> {
    if (!control.value) return of(null);

    return this.http.post<{exists: boolean}>('/api/users/check-email', {
      email: control.value
    }).pipe(
      map(response => response.exists ? { emailTaken: true } : null),
      catchError(() => of({ validationError: true }))
    );
  }

  companyCodeValidator(control: AbstractControl): Observable<ValidationErrors | null> {
    if (!control.value) return of(null);

    return this.http.get<{valid: boolean}>(`/api/companies/validate/${control.value}`)
      .pipe(
        map(response => response.valid ? null : { invalidCompanyCode: true }),
        catchError(() => of({ validationError: true }))
      );
  }
}

Optimización con debounce y manejo de múltiples requests

Para evitar exceso de peticiones durante la escritura, los validadores asíncronos deben implementar estrategias de optimización:

import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';

export class OptimizedAsyncValidatorComponent {
  searchForm: FormGroup;

  constructor(private fb: FormBuilder, private http: HttpClient) {
    this.searchForm = this.fb.group({
      productCode: ['',
        [Validators.required],
        [this.productCodeValidator.bind(this)]
      ],
      taxId: ['',
        [Validators.required],
        [this.taxIdValidator.bind(this)]
      ]
    });
  }

  // Validador con debounce integrado
  productCodeValidator(control: AbstractControl): Observable<ValidationErrors | null> {
    if (!control.value) return of(null);

    return of(control.value).pipe(
      debounceTime(300),                    // Esperar 300ms sin cambios
      distinctUntilChanged(),               // Solo si el valor cambió
      switchMap(value =>                    // Cancelar requests anteriores
        this.http.get<{exists: boolean}>(`/api/products/check/${value}`)
          .pipe(
            map(response => response.exists ? null : { productNotFound: true }),
            catchError(() => of({ validationError: true }))
          )
      )
    );
  }

  // Validador con manejo avanzado de errores
  taxIdValidator(control: AbstractControl): Observable<ValidationErrors | null> {
    if (!control.value) return of(null);

    return this.http.post<{valid: boolean, details: any}>('/api/tax/validate', {
      taxId: control.value
    }).pipe(
      debounceTime(500),
      switchMap(request => request),
      map(response => {
        if (!response.valid) {
          return {
            invalidTaxId: true,
            details: response.details
          };
        }
        return null;
      }),
      catchError(error => {
        // Diferentes tipos de error
        if (error.status === 404) {
          return of({ taxIdNotFound: true });
        } else if (error.status === 429) {
          return of({ rateLimitExceeded: true });
        }
        return of({ validationError: true });
      })
    );
  }
}

Template avanzado con estados asíncronos

El template puede mostrar diferentes estados según el progreso de la validación asíncrona:

@Component({
  selector: 'app-business-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="businessForm" class="business-form">
      <div class="field">
        <label>Código de empresa</label>
        <input 
          type="text" 
          formControlName="businessCode"
          placeholder="Ingrese código"
          [class.validating]="businessCodeControl.pending"
          [class.valid]="businessCodeControl.valid && businessCodeControl.touched"
          [class.invalid]="businessCodeControl.invalid && businessCodeControl.touched">
        
        <div class="validation-status">
          @if (businessCodeControl.pending) {
            <div class="status-pending">
              <div class="loading-indicator"></div>
              <span>Validando código...</span>
            </div>
          } @else if (businessCodeControl.valid && businessCodeControl.touched) {
            <div class="status-valid">
              <span class="check-icon">✓</span>
              <span>Código válido</span>
            </div>
          } @else if (businessCodeControl.invalid && businessCodeControl.touched) {
            <div class="status-invalid">
              <span class="error-icon">✗</span>
              @if (businessCodeControl.errors?.['required']) {
                <span>El código es obligatorio</span>
              } @else if (businessCodeControl.errors?.['businessCodeInvalid']) {
                <span>Código de empresa no válido</span>
              } @else if (businessCodeControl.errors?.['rateLimitExceeded']) {
                <span>Demasiados intentos, intente más tarde</span>
              } @else if (businessCodeControl.errors?.['validationError']) {
                <span>Error de validación, intente nuevamente</span>
              }
            </div>
          }
        </div>
      </div>

      <div class="field">
        <label>Email corporativo</label>
        <input 
          type="email" 
          formControlName="corporateEmail"
          placeholder="email@empresa.com">
        
        @if (corporateEmailControl.pending) {
          <div class="inline-status">
            Verificando dominio corporativo...
          </div>
        }
        
        @if (corporateEmailControl.errors?.['domainNotAllowed'] && corporateEmailControl.touched) {
          <div class="error-message">
            El dominio {{corporateEmailControl.errors?.['domainNotAllowed'].domain}} no está permitido
          </div>
        }
      </div>

      <button 
        type="submit" 
        [disabled]="businessForm.invalid || businessForm.pending"
        class="submit-button">
        @if (businessForm.pending) {
          <span>Validando...</span>
        } @else {
          <span>Registrar empresa</span>
        }
      </button>
    </form>
  `
})
export class BusinessFormComponent {
  businessForm: FormGroup;
  
  get businessCodeControl() { return this.businessForm.get('businessCode')!; }
  get corporateEmailControl() { return this.businessForm.get('corporateEmail')!; }

  constructor(private fb: FormBuilder, private http: HttpClient) {
    this.businessForm = this.fb.group({
      businessCode: ['',
        [Validators.required, Validators.minLength(4)],
        [this.businessCodeValidator.bind(this)]
      ],
      corporateEmail: ['',
        [Validators.required, Validators.email],
        [this.corporateEmailValidator.bind(this)]
      ]
    });
  }

  businessCodeValidator(control: AbstractControl): Observable<ValidationErrors | null> {
    if (!control.value || control.value.length < 4) return of(null);

    return this.http.get<{valid: boolean, exists: boolean}>(`/api/business/validate-code/${control.value}`)
      .pipe(
        debounceTime(400),
        switchMap(request => request),
        map(response => response.valid ? null : { businessCodeInvalid: true }),
        catchError(error => {
          if (error.status === 429) {
            return of({ rateLimitExceeded: true });
          }
          return of({ validationError: true });
        })
      );
  }

  corporateEmailValidator(control: AbstractControl): Observable<ValidationErrors | null> {
    if (!control.value || !control.value.includes('@')) return of(null);

    const domain = control.value.split('@')[1];
    
    return this.http.post<{allowed: boolean}>('/api/domains/validate', { domain })
      .pipe(
        debounceTime(600),
        switchMap(request => request),
        map(response => response.allowed ? null : { domainNotAllowed: { domain } }),
        catchError(() => of({ validationError: true }))
      );
  }
}

Combinando validadores síncronos y asíncronos

Los validadores asíncronos se ejecutan solo después de que todos los validadores síncronos hayan pasado exitosamente:

export class ComplexValidationComponent {
  registrationForm: FormGroup;

  constructor(private fb: FormBuilder, private http: HttpClient) {
    this.registrationForm = this.fb.group({
      username: ['',
        // Primero validadores síncronos
        [
          Validators.required,
          Validators.minLength(3),
          Validators.maxLength(20),
          Validators.pattern(/^[a-zA-Z0-9_]+$/)
        ],
        // Después validadores asíncronos (solo si síncronos pasan)
        [this.usernameAvailabilityValidator.bind(this)]
      ],
      
      email: ['',
        [Validators.required, Validators.email],
        [this.emailUniqueValidator.bind(this)]
      ]
    });
  }

  usernameAvailabilityValidator(control: AbstractControl): Observable<ValidationErrors | null> {
    // Este método solo se ejecuta si las validaciones síncronas pasan
    return this.http.get<{available: boolean}>(`/api/check-username/${control.value}`)
      .pipe(
        debounceTime(300),
        map(response => response.available ? null : { usernameTaken: true }),
        catchError(() => of(null))
      );
  }

  emailUniqueValidator(control: AbstractControl): Observable<ValidationErrors | null> {
    return this.http.post<{unique: boolean}>('/api/check-email', { email: control.value })
      .pipe(
        debounceTime(500),
        map(response => response.unique ? null : { emailAlreadyExists: true }),
        catchError(() => of(null))
      );
  }
}

Los validadores asíncronos proporcionan validación en tiempo real para casos que requieren verificación externa, manteniendo una experiencia de usuario fluida mediante el manejo adecuado de estados pending y la optimización de peticiones de red.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender el funcionamiento y aplicación de validadores síncronos incorporados en Angular.
  • Aprender a implementar validadores asíncronos para validaciones que requieren consultas externas.
  • Manejar estados de validación como valid, invalid y pending en controles y formularios.
  • Optimizar validadores asíncronos para evitar exceso de peticiones mediante debounce y cancelación de solicitudes.
  • Integrar validadores en templates para mostrar mensajes de error y estados de validación en tiempo real.