Compartir en Twitter
Go to Homepage

VALIDACIONES PERSONALIZADAS EN FORMULARIOS TEMPLATE-DRIVEN DE ANGULAR

October 29, 2025

Introducción a las validaciones en formularios template-driven de Angular

Los formularios son un componente esencial en cualquier aplicación web moderna, especialmente cuando se trata de registrar usuarios, autenticar credenciales o recopilar información estructurada. En el ecosistema de Angular, existen dos enfoques principales para manejar formularios: los formularios reactivos y los formularios basados en plantillas, conocidos como template-driven forms. Este artículo se centra en el segundo enfoque, explorando cómo implementar validaciones tanto integradas como personalizadas de manera eficiente y profesional.

A lo largo de este tutorial, construiremos un formulario de registro de usuario completo, aplicando validaciones en tiempo real que incluyen requisitos de formato, coincidencia de campos y verificación asíncrona de disponibilidad. Utilizaremos las capacidades nativas de Angular junto con directivas personalizadas para mantener el código limpio, reutilizable y fácil de mantener. Este enfoque es ideal para aplicaciones que priorizan la simplicidad en la lógica del componente y delegan la mayor parte del trabajo a la plantilla HTML.

Preparación del entorno de desarrollo

Antes de comenzar con la implementación, es fundamental configurar correctamente el entorno de desarrollo. Angular requiere herramientas específicas que garantizan un flujo de trabajo fluido y productivo. En la actualidad, la versión estable recomendada de Angular CLI supera la 18, y Node.js debe estar en su versión LTS activa, que al momento de escribir este artículo es la 20.x.

Para iniciar el proyecto, abre una terminal en el directorio deseado y ejecuta el siguiente comando:

ng new formulario-registro-angular --routing=false --style=scss

Este comando genera una nueva aplicación Angular con el nombre especificado, desactiva el módulo de enrutamiento por defecto y configura SCSS como preprocesador de estilos. La ausencia de enrutamiento simplifica el ejemplo, aunque en aplicaciones reales se recomienda activarlo desde el inicio.

Una vez creado el proyecto, navega al directorio y ábrelo en tu editor preferido:

cd formulario-registro-angular
code .

Integración de Bootstrap para estilos profesionales

Aunque Angular no depende de frameworks de CSS externos, la incorporación de Bootstrap agiliza el diseño de interfaces responsivas y consistentes. Instala la versión más reciente de Bootstrap mediante npm:

npm install bootstrap --save

Luego, importa los estilos globales en el archivo styles.scss ubicado en la raíz del proyecto:

@import "~bootstrap/dist/css/bootstrap.css";

Esta configuración asegura que todas las clases de Bootstrap estén disponibles en toda la aplicación sin necesidad de importaciones adicionales por componente.

Creación del servicio de validación personalizada

Las validaciones personalizadas requieren lógica centralizada que pueda ser reutilizada en múltiples directivas. Para ello, generamos un servicio dedicado utilizando el CLI de Angular:

ng g s services/validacion-personalizada

Este comando crea una carpeta services con el archivo del servicio y su prueba unitaria correspondiente. Abre el archivo validacion-personalizada.service.ts e implementa las siguientes validaciones:

import { Injectable } from "@angular/core";
import { ValidatorFn, AbstractControl, FormGroup } from "@angular/forms";

@Injectable({
    providedIn: "root",
})
export class ValidacionPersonalizadaService {
    patternValidator(): ValidatorFn {
        return (control: AbstractControl): { [key: string]: any } | null => {
            if (!control.value) {
                return null;
            }
            const regex = new RegExp(
                "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$"
            );
            const valid = regex.test(control.value);
            return valid ? null : { invalidPassword: true };
        };
    }

    MatchPassword(password: string, confirmPassword: string) {
        return (formGroup: FormGroup) => {
            const passwordControl = formGroup.controls[password];
            const confirmPasswordControl = formGroup.controls[confirmPassword];

            if (!passwordControl || !confirmPasswordControl) {
                return null;
            }

            if (
                confirmPasswordControl.errors &&
                !confirmPasswordControl.errors["passwordMismatch"]
            ) {
                return null;
            }

            if (passwordControl.value !== confirmPasswordControl.value) {
                confirmPasswordControl.setErrors({ passwordMismatch: true });
            } else {
                confirmPasswordControl.setErrors(null);
            }
        };
    }

    userNameValidator(userControl: AbstractControl) {
        return new Promise((resolve) => {
            setTimeout(() => {
                if (this.validateUserName(userControl.value)) {
                    resolve({ userNameNotAvailable: true });
                } else {
                    resolve(null);
                }
            }, 1000);
        });
    }

    validateUserName(userName: string) {
        const UserList = ["ankit", "admin", "user", "superuser"];
        return UserList.indexOf(userName.toLowerCase()) > -1;
    }
}

El método patternValidator define un validador síncrono que verifica el cumplimiento de un patrón complejo de contraseña. La expresión regular exige al menos ocho caracteres, incluyendo mayúsculas, minúsculas y números. El validador devuelve null si la validación pasa, o un objeto con la clave invalidPassword en caso contrario.

La función MatchPassword opera a nivel de grupo de formularios, comparando dos controles específicos. Este enfoque es más eficiente que validar cada campo individualmente, ya que permite centralizar la lógica de comparación.

Finalmente, el validador asíncrono userNameValidator simula una verificación contra una base de datos mediante un array estático. La demora artificial de un segundo con setTimeout emula el tiempo de respuesta de una API real, permitiendo demostrar el comportamiento de validaciones asíncronas en la interfaz de usuario.

Definición del modelo de usuario

Aunque los formularios template-driven no requieren modelos tipados estrictamente, definir una clase de modelo mejora la legibilidad y el mantenimiento del código. Crea una carpeta models dentro de src/app y agrega el archivo usuario.model.ts:

export class Usuario {
    public nombre: string = "";
    public email: string = "";
    public username: string = "";
    public password: string = "";
    public confirmPassword: string = "";
}

Este modelo servirá como contenedor para los valores del formulario, facilitando su manipulación en el componente.

Desarrollo de directivas de validación personalizadas

Las validaciones personalizadas en formularios template-driven se implementan mediante directivas que implementan las interfaces Validator o AsyncValidator. Genera tres directivas utilizando el CLI:

ng g d directivas/passwordPattern
ng g d directivas/matchPassword
ng g d directivas/validateUserName

Directiva para patrón de contraseña

La directiva PasswordPatternDirective envuelve el validador síncrono del servicio:

import { Directive } from "@angular/core";
import { NG_VALIDATORS, Validator, AbstractControl } from "@angular/forms";
import { ValidacionPersonalizadaService } from "../services/validacion-personalizada.service";

@Directive({
    selector: "[appPasswordPattern]",
    providers: [
        {
            provide: NG_VALIDATORS,
            useExisting: PasswordPatternDirective,
            multi: true,
        },
    ],
})
export class PasswordPatternDirective implements Validator {
    constructor(private customValidator: ValidacionPersonalizadaService) {}

    validate(control: AbstractControl): { [key: string]: any } | null {
        return this.customValidator.patternValidator()(control);
    }
}

Al implementar la interfaz Validator y proveer el token NG_VALIDATORS, Angular reconoce automáticamente esta directiva como un validador síncrono aplicable a cualquier control de formulario.

Directiva para coincidencia de contraseñas

La directiva MatchPasswordDirective requiere un parámetro de entrada para especificar los nombres de los campos a comparar:

import { Directive, Input } from "@angular/core";
import {
    NG_VALIDATORS,
    Validator,
    ValidationErrors,
    FormGroup,
} from "@angular/forms";
import { ValidacionPersonalizadaService } from "../services/validacion-personalizada.service";

@Directive({
    selector: "[appMatchPassword]",
    providers: [
        {
            provide: NG_VALIDATORS,
            useExisting: MatchPasswordDirective,
            multi: true,
        },
    ],
})
export class MatchPasswordDirective implements Validator {
    @Input("appMatchPassword") matchPassword: string[] = [];

    constructor(private customValidator: ValidacionPersonalizadaService) {}

    validate(formGroup: FormGroup): ValidationErrors | null {
        return this.customValidator.MatchPassword(
            this.matchPassword[0],
            this.matchPassword[1]
        )(formGroup);
    }
}

El decorador @Input permite pasar un array con los nombres de los controles, ofreciendo flexibilidad para reutilizar la directiva en diferentes formularios.

Directiva para validación asíncrona de nombre de usuario

La directiva ValidateUserNameDirective maneja validaciones asíncronas mediante el token NG_ASYNC_VALIDATORS:

import { Directive, forwardRef } from "@angular/core";
import {
    Validator,
    AbstractControl,
    NG_ASYNC_VALIDATORS,
} from "@angular/forms";
import { ValidacionPersonalizadaService } from "../services/validacion-personalizada.service";

@Directive({
    selector: "[appValidateUserName]",
    providers: [
        {
            provide: NG_ASYNC_VALIDATORS,
            useExisting: forwardRef(() => ValidateUserNameDirective),
            multi: true,
        },
    ],
})
export class ValidateUserNameDirective implements Validator {
    constructor(private customValidator: ValidacionPersonalizadaService) {}

    validate(control: AbstractControl): Promise<{ [key: string]: any }> | any {
        return this.customValidator.userNameValidator(control);
    }
}

El uso de forwardRef es necesario debido a la referencia circular que se crea al proveer la propia clase en su definición.

Implementación del componente de formulario

Genera el componente principal del formulario:

ng g c componentes/formulario-registro

Lógica del componente

En el archivo TypeScript del componente, instancia el modelo y define el método de envío:

import { Component } from "@angular/core";
import { Usuario } from "../models/usuario.model";

@Component({
    selector: "app-formulario-registro",
    templateUrl: "./formulario-registro.component.html",
    styleUrls: ["./formulario-registro.component.scss"],
})
export class FormularioRegistroComponent {
    usuario = new Usuario();

    onSubmit() {
        if (confirm("¿Deseas enviar el formulario?")) {
            console.table(this.usuario);
            alert("Formulario enviado exitosamente");
        }
    }
}

Plantilla del formulario

La plantilla HTML combina validaciones integradas de Angular con las directivas personalizadas:

<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card shadow">
                <div class="card-header bg-primary text-white">
                    <h3 class="mb-0">Registro de Usuario con Validaciones</h3>
                </div>
                <div class="card-body">
                    <form
                        #registroForm="ngForm"
                        [appMatchPassword]="['password', 'confirmPassword']"
                        (ngSubmit)="registroForm.form.valid && onSubmit()"
                        novalidate
                    >
                        <div class="form-group mb-3">
                            <label for="nombre">Nombre completo</label>
                            <input
                                type="text"
                                class="form-control"
                                [(ngModel)]="usuario.nombre"
                                name="nombre"
                                #nombre="ngModel"
                                required
                            />
                            <small
                                class="text-danger"
                                *ngIf="(nombre.touched || registroForm.submitted) && nombre.errors?.required"
                            >
                                El nombre es obligatorio
                            </small>
                        </div>

                        <div class="form-group mb-3">
                            <label for="email">Correo electrónico</label>
                            <input
                                type="email"
                                class="form-control"
                                [(ngModel)]="usuario.email"
                                name="email"
                                #email="ngModel"
                                required
                                email
                            />
                            <small
                                class="text-danger"
                                *ngIf="(email.touched || registroForm.submitted) && email.errors?.required"
                            >
                                El email es obligatorio
                            </small>
                            <small
                                class="text-danger"
                                *ngIf="email.touched && email.errors?.email"
                            >
                                Ingresa un email válido
                            </small>
                        </div>

                        <div class="form-group mb-3">
                            <label for="username">Nombre de usuario</label>
                            <input
                                type="text"
                                class="form-control"
                                [(ngModel)]="usuario.username"
                                name="username"
                                #username="ngModel"
                                appValidateUserName
                                required
                            />
                            <small
                                class="text-danger"
                                *ngIf="(username.touched || registroForm.submitted) && username.errors?.required"
                            >
                                El nombre de usuario es obligatorio
                            </small>
                            <small
                                class="text-danger"
                                *ngIf="username.touched && username.errors?.userNameNotAvailable"
                            >
                                **nombre usuario no disponible**
                            </small>
                            <small class="text-info" *ngIf="username.pending">
                                Verificando disponibilidad...
                            </small>
                        </div>

                        <div class="form-group mb-3">
                            <label for="password">Contraseña</label>
                            <input
                                type="password"
                                class="form-control"
                                [(ngModel)]="usuario.password"
                                name="password"
                                #password="ngModel"
                                appPasswordPattern
                                required
                            />
                            <small
                                class="text-danger"
                                *ngIf="(password.touched || registroForm.submitted) && password.errors?.required"
                            >
                                La contraseña es obligatoria
                            </small>
                            <small
                                class="text-danger"
                                *ngIf="password.touched && password.errors?.invalidPassword"
                            >
                                La contraseña debe tener al menos 8 caracteres,
                                una mayúscula, una minúscula y un número
                            </small>
                        </div>

                        <div class="form-group mb-3">
                            <label for="confirmPassword"
                                >Confirmar contraseña</label
                            >
                            <input
                                type="password"
                                class="form-control"
                                [(ngModel)]="usuario.confirmPassword"
                                name="confirmPassword"
                                #confirmPassword="ngModel"
                                required
                            />
                            <small
                                class="text-danger"
                                *ngIf="(confirmPassword.touched || registroForm.submitted) && confirmPassword.errors?.required"
                            >
                                Debes confirmar la contraseña
                            </small>
                            <small
                                class="text-danger"
                                *ngIf="confirmPassword.touched && confirmPassword.errors?.passwordMismatch"
                            >
                                Las contraseñas no coinciden
                            </small>
                        </div>

                        <button type="submit" class="btn btn-success btn-lg">
                            Registrarse
                        </button>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>

La directiva [appMatchPassword] se aplica al formulario completo, mientras que appPasswordPattern y appValidateUserName se aplican a sus respectivos campos. Las validaciones integradas required y email complementan las personalizadas.

Configuración del módulo principal

Actualiza el archivo app.module.ts para incluir los módulos necesarios y configurar el enrutamiento básico:

import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";

import { AppComponent } from "./app.component";
import { FormularioRegistroComponent } from "./componentes/formulario-registro/formulario-registro.component";
import { PasswordPatternDirective } from "./directivas/password-pattern.directive";
import { MatchPasswordDirective } from "./directivas/match-password.directive";
import { ValidateUserNameDirective } from "./directivas/validate-user-name.directive";

@NgModule({
    declarations: [
        AppComponent,
        FormularioRegistroComponent,
        PasswordPatternDirective,
        MatchPasswordDirective,
        ValidateUserNameDirective,
    ],
    imports: [
        BrowserModule,
        FormsModule,
        RouterModule.forRoot([
            { path: "", component: FormularioRegistroComponent },
            { path: "registro", component: FormularioRegistroComponent },
        ]),
    ],
    providers: [],
    bootstrap: [AppComponent],
})
export class AppModule {}

La declaración de todas las directivas en el módulo principal asegura su disponibilidad global.

Ejecución y pruebas de la aplicación

Inicia el servidor de desarrollo con:

ng serve -o

La aplicación se abrirá automáticamente en http://localhost:4200. Prueba los siguientes escenarios:

  1. Dejar campos vacíos: muestra mensajes de requerido
  2. Ingresar email inválido: activa validador de formato
  3. Usar “admin” como username: usuario ya registrado aparece tras un segundo
  4. Contraseña débil: mensaje de patrón no cumplido
  5. Contraseñas diferentes: error de coincidencia

La validación asíncrona muestra un estado “pending” mientras se resuelve la promesa, mejorando la experiencia de usuario.

Mejoras y consideraciones avanzadas

En entornos de producción, reemplaza el array estático de usuarios por una llamada HTTP real:

userNameValidator(control: AbstractControl) {
  return this.http.get<boolean>(`/api/usuarios/existe/${control.value}`).pipe(
    delay(500),
    map(existe => existe ? { userNameNotAvailable: true } : null)
  );
}

Considera también implementar debouncing en los validadores asíncronos para reducir llamadas innecesarias:

this.form.get("username").valueChanges.pipe(
    debounceTime(500),
    distinctUntilChanged(),
    switchMap((value) => this.validarUsuarioService(value))
);

Conclusiones

Las validaciones en formularios template-driven de Angular ofrecen una solución elegante y mantenible para aplicaciones que requieren validación compleja sin sacrificar la simplicidad del enfoque basado en plantillas. La combinación de validadores integrados con directivas personalizadas permite crear experiencias de usuario robustas y profesionales.

La arquitectura presentada demuestra cómo separar la lógica de validación en servicios reutilizables, implementar validaciones síncronas y asíncronas mediante directivas, y mantener una plantilla limpia y declarativa. Este patrón escala bien a formularios más complejos y facilita las pruebas unitarias y de integración.

La integración con Bootstrap proporciona una base visual sólida, mientras que la estructura modular del código garantiza su mantenibilidad a largo plazo. Con las herramientas y patrones aquí presentados, los desarrolladores pueden construir formularios de registro seguros, accesibles y con validación en tiempo real que cumplen con los estándares modernos de desarrollo web.