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
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.