Angular 21 de Cero a Experto / Formularios
Estados y errores accesibles: el formulario que se oye
Validar los datos es la mitad del trabajo; la otra mitad es COMUNICAR al usuario qué está mal,
cuándo y cómo. Un error que aparece demasiado pronto molesta; uno que no aparece frustra; uno
que un lector de pantalla no anuncia excluye. Hoy dominas los estados del formulario
(touched, dirty), muestras errores en el momento justo, y haces todo accesible —un
formulario que cualquiera puede completar, también quien no ve la pantalla—.
Cuándo mostrar el error
Los estados de un control
Cada FormControl lleva la cuenta de su historia:
Mostrar el error en el momento justo
@Component({
selector: 'app-campo-nombre',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form">
<label for="nombre">Nombre del gasto</label>
<input id="nombre" formControlName="nombre" />
@if (nombre.invalid && nombre.touched) {
@if (nombre.errors?.['required']) {
<p class="error">El nombre es obligatorio.</p>
}
@if (nombre.errors?.['minlength']) {
<p class="error">Mínimo 2 caracteres.</p>
}
}
</form>
`,
})
export class CampoNombreComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
nombre: ['', [Validators.required, Validators.minLength(2)]],
});
get nombre() { return this.form.controls.nombre; }
}El @if (nombre.invalid && nombre.touched) es la clave: el bloque de errores solo aparece
cuando el campo es inválido Y el usuario ya salió de él. Dentro, cada @if sobre
errors?.['...'] muestra el mensaje específico de cada validador.
Hacerlo accesible: el formulario que se oye
Mostrar el error en pantalla no basta: quien usa un lector de pantalla necesita que el error se ANUNCIE y que el input diga que es inválido. Cuatro atributos ARIA lo logran:
template: `
<label for="valor">Monto</label>
<input
id="valor"
type="number"
formControlName="valor"
[attr.aria-invalid]="valor.invalid && valor.touched"
[attr.aria-describedby]="valor.invalid && valor.touched ? 'valor-error' : null" />
@if (valor.invalid && valor.touched) {
<p id="valor-error" class="error" role="alert">
El monto debe ser mayor que cero.
</p>
}
`,Enfocar el primer campo con error al enviar
Al intentar enviar un formulario inválido, la mejor experiencia —y la más accesible— es
marcar todos los campos como touched (para que se muestren los errores) y llevar el foco al
primer campo con problema:
guardar() {
if (this.form.invalid) {
this.form.markAllAsTouched(); // revela TODOS los errores de una vez
// (opcional) enfocar el primer control inválido para el usuario de teclado
return;
}
// ...guardar...
}markAllAsTouched() es esencial: si el usuario pulsa "Guardar" sin haber tocado los campos,
los errores no se mostrarían (siguen untouched). Marcarlos todos revela el panorama completo.
¿Por qué se muestra el error solo cuando el control es `invalid && touched`, y no solo `invalid`?
Mini-reto
Escribe el template accesible de un campo email: 1) <label for> asociado; 2) el input con
aria-invalid y aria-describedby condicionales; 3) un @if (email.invalid && email.touched)
con mensajes distintos para required y email (formato), el contenedor con id y
role="alert". Añade el markAllAsTouched() en el método de envío.
Qué sigue
Tus formularios validan y comunican con claridad y accesibilidad. Falta un caso avanzado: los
formularios DINÁMICOS, donde el número de campos cambia —agregar varios gastos de una vez, una
lista de ítems que crece—. La próxima lección es FormArray, la herramienta para formularios
que crecen y se encogen.