Formularios en Ionic: validación, experiencia de usuario y seguridad básica

Capítulo 7

Tiempo estimado de lectura: 13 minutos

+ Ejercicio

Diseño de formularios robustos en Ionic

En Ionic, los formularios suelen construirse con ion-item + ion-input, ion-select, ion-datetime, ion-textarea y controles móviles como ion-toggle o ion-checkbox. Para que un formulario sea “robusto”, no basta con capturar datos: debe guiar al usuario (UX), validar correctamente (sincronía y asincronía), evitar errores comunes (doble envío) y normalizar la información antes de enviarla.

Campos y tipos adecuados (mejor UX y menos errores)

  • Tipo de teclado: usa type y inputmode para mostrar el teclado correcto (email, numérico, etc.).
  • Autocompletado: autocomplete y autocapitalize ayudan a rellenar rápido y evitan errores.
  • Máscaras y formato: cuando el dato requiere un patrón (teléfono, documento), aplica formato en el cliente, pero guarda/enviar una versión normalizada.
  • Selección y fechas: para opciones finitas, ion-select; para fechas/horas, ion-datetime (evita que el usuario escriba formatos ambiguos).
  • Controles móviles: ion-toggle para preferencias, ion-checkbox para aceptación de términos, ion-range para valores continuos.

Ejemplos de controles típicos

<ion-list>  <ion-item>    <ion-label position="stacked">Email</ion-label>    <ion-input type="email" inputmode="email" autocomplete="email" autocapitalize="none"></ion-input>  </ion-item>  <ion-item>    <ion-label position="stacked">Contraseña</ion-label>    <ion-input type="password" autocomplete="current-password"></ion-input>  </ion-item>  <ion-item>    <ion-label>País</ion-label>    <ion-select interface="popover" placeholder="Selecciona">      <ion-select-option value="MX">México</ion-select-option>      <ion-select-option value="ES">España</ion-select-option>    </ion-select>  </ion-item>  <ion-item>    <ion-label position="stacked">Fecha de nacimiento</ion-label>    <ion-datetime presentation="date"></ion-datetime>  </ion-item></ion-list>

Validación en Ionic con Reactive Forms

Para formularios con reglas claras, validaciones complejas y control fino de estados (touched, dirty, pending), Reactive Forms es una opción sólida. La idea es definir un FormGroup con FormControl, validadores síncronos (inmediatos) y asíncronos (requieren tiempo, por ejemplo, verificar disponibilidad).

1) Preparación del módulo

Asegúrate de importar ReactiveFormsModule en el módulo/página donde se usa el formulario.

import { NgModule } from '@angular/core';import { CommonModule } from '@angular/common';import { IonicModule } from '@ionic/angular';import { ReactiveFormsModule } from '@angular/forms';import { RegisterPage } from './register.page';@NgModule({  declarations: [RegisterPage],  imports: [CommonModule, IonicModule, ReactiveFormsModule]})export class RegisterPageModule {}

2) Definir el formulario con validaciones síncronas

Ejemplo: registro con nombre, email, teléfono, fecha de nacimiento, contraseña, confirmación y aceptación de términos. Se incluyen validaciones de requerido, formato, longitud y una validación a nivel de grupo para comparar contraseñas.

import { Component } from '@angular/core';import { AbstractControl, FormBuilder, ValidationErrors, Validators } from '@angular/forms';@Component({  selector: 'app-register',  templateUrl: './register.page.html'})export class RegisterPage {  isSubmitting = false;  form = this.fb.group({    fullName: ['', [Validators.required, Validators.minLength(2)]],    email: ['', [Validators.required, Validators.email]],    phone: ['', [Validators.required, Validators.pattern(/^\+?[0-9]{8,15}$/)]],    birthDate: ['', [Validators.required]],    password: ['', [Validators.required, Validators.minLength(8)]],    confirmPassword: ['', [Validators.required]],    acceptTerms: [false, [Validators.requiredTrue]]  }, { validators: [this.passwordsMatchValidator] });  constructor(private fb: FormBuilder) {}  private passwordsMatchValidator(group: AbstractControl): ValidationErrors | null {    const p = group.get('password')?.value;    const c = group.get('confirmPassword')?.value;    if (!p || !c) return null;    return p === c ? null : { passwordsMismatch: true };  }}

3) Validación asíncrona (ejemplo: email ya registrado)

Una validación asíncrona suele dispararse cuando el usuario deja el campo o tras un pequeño retraso. En Angular puedes configurar updateOn: 'blur' para evitar validar en cada tecla. La validación asíncrona debe devolver null si es válido o un objeto de error si no lo es.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

import { AsyncValidatorFn, AbstractControl, ValidationErrors, FormControl } from '@angular/forms';import { Observable, of, timer } from 'rxjs';import { map, switchMap, catchError } from 'rxjs/operators';// Simulación: reemplaza por tu servicio real que consulte disponibilidadfunction emailAvailableValidator(checkFn: (email: string) => Observable<{ available: boolean }>): AsyncValidatorFn {  return (control: AbstractControl): Observable<ValidationErrors | null> => {    const email = String(control.value || '').trim().toLowerCase();    if (!email) return of(null);    return timer(400).pipe(      switchMap(() => checkFn(email)),      map(res => res.available ? null : { emailTaken: true }),      catchError(() => of(null))    );  };}// Uso en el control (ejemplo con updateOn blur)email = new FormControl('', {  validators: [Validators.required, Validators.email],  asyncValidators: [emailAvailableValidator((e) => this.fakeCheckEmail(e))],  updateOn: 'blur'});

Si tu formulario ya está creado con fb.group, puedes integrar el control email anterior o definirlo directamente en el grupo con la configuración equivalente.

Mensajes de error amigables (sin saturar la pantalla)

Una buena UX muestra errores cuando el usuario ya interactuó con el campo (por ejemplo, touched) y con mensajes claros orientados a la acción. Evita mensajes técnicos como “pattern invalid”.

Patrón recomendado para mostrar errores

  • Mostrar error si: control.invalid y (control.touched o intento de envío).
  • Un mensaje por vez, priorizando el más útil (requerido > formato > longitud).
  • Usar ion-note o ion-text con color danger.
<ion-item>  <ion-label position="stacked">Email</ion-label>  <ion-input formControlName="email" type="email" inputmode="email" autocapitalize="none"></ion-input></ion-item><ion-note color="danger" *ngIf="showError('email')">  {{ emailErrorMessage }}</ion-note>
get emailCtrl() { return this.form.get('email'); }submitted = false;showError(name: string): boolean {  const c = this.form.get(name);  return !!c && c.invalid && (c.touched || this.submitted);}get emailErrorMessage(): string {  const c = this.emailCtrl;  if (!c || !c.errors) return '';  if (c.errors['required']) return 'Escribe tu email.';  if (c.errors['email']) return 'Escribe un email válido (ej: nombre@dominio.com).';  if (c.errors['emailTaken']) return 'Este email ya está registrado. Prueba con otro.';  return 'Revisa este campo.';}

Tabla rápida: error técnico → mensaje humano

ErrorMensaje sugerido
required“Completa este campo.”
email“Escribe un email válido.”
minlength“Debe tener al menos X caracteres.”
pattern“Usa el formato correcto (ej: …).”
passwordsMismatch“Las contraseñas no coinciden.”

Prevención de envíos duplicados

En móviles es común tocar dos veces el botón o perder conectividad y reintentar. Debes bloquear el envío mientras está en curso y dar feedback visual.

Reglas prácticas

  • Deshabilita el botón si el formulario es inválido, está pendiente (pending) o se está enviando.
  • Marca submitted = true al intentar enviar para mostrar errores sin obligar a tocar cada campo.
  • Evita múltiples suscripciones simultáneas: usa una bandera isSubmitting o un operador que ignore reintentos mientras hay uno activo.
<ion-button expand="block" (click)="submit()"  [disabled]="form.invalid || form.pending || isSubmitting">  <ion-spinner *ngIf="isSubmitting" name="lines-small"></ion-spinner>  <span *ngIf="!isSubmitting">Crear cuenta</span></ion-button>
async submit() {  this.submitted = true;  this.form.markAllAsTouched();  if (this.form.invalid || this.form.pending || this.isSubmitting) return;  this.isSubmitting = true;  try {    const payload = this.normalizeRegisterPayload(this.form.value);    // Llama a tu servicio de autenticación/registro    // await this.auth.register(payload);  } finally {    this.isSubmitting = false;  }}

Normalización de datos antes de enviar

Validar no es lo mismo que normalizar. Normalizar significa transformar a un formato consistente para almacenar o enviar: recortar espacios, unificar mayúsculas/minúsculas, eliminar separadores, convertir fechas a ISO, etc. Esto reduce errores en backend y evita duplicados lógicos (por ejemplo, emails con espacios).

Ejemplo de normalización para registro

normalizeRegisterPayload(raw: any) {  const fullName = String(raw.fullName || '').trim().replace(/\s+/g, ' ');  const email = String(raw.email || '').trim().toLowerCase();  const phone = String(raw.phone || '').trim().replace(/[\s()-]/g, '');  const birthDateIso = raw.birthDate ? new Date(raw.birthDate).toISOString() : null;  return {    fullName,    email,    phone,    birthDate: birthDateIso,    password: String(raw.password || '')  };}

Máscaras: mostrar bonito, enviar limpio

Si decides mostrar el teléfono con separadores (por ejemplo, “+34 600 123 123”), mantén el valor interno normalizado. Una estrategia simple es formatear en el evento ionBlur (para no pelear con el cursor) y normalizar al enviar.

<ion-input formControlName="phone" inputmode="tel" (ionBlur)="formatPhone()"></ion-input>
formatPhone() {  const c = this.form.get('phone');  const raw = String(c?.value || '');  const normalized = raw.trim().replace(/[\s()-]/g, '');  c?.setValue(normalized, { emitEvent: false });}

Pautas de seguridad básica en el cliente

El formulario vive en el cliente, por lo que cualquier validación puede ser alterada por un atacante. Aun así, el cliente debe aplicar buenas prácticas para reducir riesgos y evitar filtraciones accidentales.

1) No confiar en la validación del front

  • La validación en el cliente es para UX; el servidor debe validar de nuevo (requeridos, permisos, límites, formatos).
  • No asumas que un campo “disabled” no se enviará: un atacante puede construir la petición manualmente.
  • Evita lógica de autorización en el cliente (por ejemplo, “si rol=admin, mostrar botón” no debe ser la única barrera).

2) Manejo prudente de tokens

  • No muestres tokens en UI ni los registres en consola (console.log).
  • Evita persistir tokens en lugares inseguros. Si necesitas persistencia, usa almacenamiento seguro del dispositivo (por ejemplo, un plugin de secure storage) en lugar de texto plano.
  • Minimiza el tiempo de vida del token y usa refresh tokens si tu arquitectura lo contempla (la política exacta depende del backend).
  • En logout, limpia el token y cualquier dato sensible asociado.

3) Errores y mensajes: no filtrar información sensible

  • En login, evita mensajes que confirmen si un email existe (“usuario no existe”) si tu política de seguridad lo desaconseja. Un mensaje genérico como “Credenciales inválidas” reduce enumeración.
  • En validaciones asíncronas (email disponible), considera si revelar disponibilidad es aceptable para tu caso de uso.

4) Protección básica contra envíos repetidos y automatización

  • El bloqueo de doble envío ayuda a evitar duplicados accidentales, pero no detiene bots. La mitigación real se hace en servidor (rate limiting, captchas, etc.).
  • En el cliente, agrega retrasos y estados de carga para desalentar taps repetidos.

Ejemplo práctico: formulario de registro con Ionic (paso a paso)

Paso 1: Plantilla con componentes móviles

<form [formGroup]="form" (ngSubmit)="submit()">  <ion-list>    <ion-item>      <ion-label position="stacked">Nombre completo</ion-label>      <ion-input formControlName="fullName" autocomplete="name"></ion-input>    </ion-item>    <ion-note color="danger" *ngIf="showError('fullName')">      {{ fullNameErrorMessage }}    </ion-note>    <ion-item>      <ion-label position="stacked">Email</ion-label>      <ion-input formControlName="email" type="email" inputmode="email" autocomplete="email" autocapitalize="none"></ion-input>    </ion-item>    <ion-note color="danger" *ngIf="showError('email')">      {{ emailErrorMessage }}    </ion-note>    <ion-item>      <ion-label position="stacked">Teléfono</ion-label>      <ion-input formControlName="phone" inputmode="tel" autocomplete="tel" (ionBlur)="formatPhone()"></ion-input>    </ion-item>    <ion-note color="danger" *ngIf="showError('phone')">      {{ phoneErrorMessage }}    </ion-note>    <ion-item>      <ion-label position="stacked">Fecha de nacimiento</ion-label>      <ion-datetime formControlName="birthDate" presentation="date" showDefaultButtons="true"></ion-datetime>    </ion-item>    <ion-note color="danger" *ngIf="showError('birthDate')">      Especifica tu fecha de nacimiento.    </ion-note>    <ion-item>      <ion-label position="stacked">Contraseña</ion-label>      <ion-input formControlName="password" type="password" autocomplete="new-password"></ion-input>    </ion-item>    <ion-note color="danger" *ngIf="showError('password')">      {{ passwordErrorMessage }}    </ion-note>    <ion-item>      <ion-label position="stacked">Confirmar contraseña</ion-label>      <ion-input formControlName="confirmPassword" type="password" autocomplete="new-password"></ion-input>    </ion-item>    <ion-note color="danger" *ngIf="submitted && form.errors?.passwordsMismatch">      Las contraseñas no coinciden.    </ion-note>    <ion-item lines="none">      <ion-checkbox slot="start" formControlName="acceptTerms"></ion-checkbox>      <ion-label>Acepto los términos y condiciones</ion-label>    </ion-item>    <ion-note color="danger" *ngIf="showError('acceptTerms')">      Debes aceptar los términos para continuar.    </ion-note>  </ion-list>  <ion-button type="submit" expand="block" [disabled]="form.invalid || form.pending || isSubmitting">    <ion-spinner *ngIf="isSubmitting" name="lines-small"></ion-spinner>    <span *ngIf="!isSubmitting">Crear cuenta</span>  </ion-button></form>

Paso 2: Mensajes de error por campo

get fullNameErrorMessage(): string {  const c = this.form.get('fullName');  if (!c?.errors) return '';  if (c.errors['required']) return 'Escribe tu nombre.';  if (c.errors['minlength']) return 'Tu nombre es demasiado corto.';  return 'Revisa tu nombre.';}get phoneErrorMessage(): string {  const c = this.form.get('phone');  if (!c?.errors) return '';  if (c.errors['required']) return 'Escribe tu teléfono.';  if (c.errors['pattern']) return 'Usa un teléfono válido (solo dígitos, con prefijo opcional).';  return 'Revisa tu teléfono.';}get passwordErrorMessage(): string {  const c = this.form.get('password');  if (!c?.errors) return '';  if (c.errors['required']) return 'Crea una contraseña.';  if (c.errors['minlength']) return 'Usa al menos 8 caracteres.';  return 'Revisa tu contraseña.';}

Paso 3: Envío seguro a nivel de cliente (sin filtrar datos)

En el envío, evita loguear el formulario completo (podría incluir contraseñas). Normaliza el payload y, si necesitas depurar, registra solo campos no sensibles.

async submit() {  this.submitted = true;  this.form.markAllAsTouched();  if (this.form.invalid || this.form.pending || this.isSubmitting) return;  this.isSubmitting = true;  try {    const payload = this.normalizeRegisterPayload(this.form.value);    // No hagas console.log(payload) si incluye password    // await this.auth.register(payload);  } catch (e) {    // Muestra un mensaje genérico y registra el detalle solo en un canal seguro si aplica  } finally {    this.isSubmitting = false;  }}

Paso 4: Ajustes finos de UX

  • Orden de tabulación: en móvil, el botón “siguiente” del teclado debe avanzar de forma natural; revisa el orden de los campos.
  • Validar en blur: para campos con validación asíncrona o reglas costosas, usa updateOn: 'blur'.
  • Accesibilidad: etiquetas claras, mensajes de error cerca del campo, y evita depender solo del color.
  • Estados: muestra ion-spinner durante el envío y deshabilita el formulario si es necesario.

Ahora responde el ejercicio sobre el contenido:

¿Cuál es la forma recomendada de evitar envíos duplicados en un formulario con Ionic y Reactive Forms?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

La práctica recomendada es bloquear el envío mientras está en curso (por ejemplo con isSubmitting) y deshabilitar el botón si el formulario está invalid, pending o enviándose, mostrando un spinner para dar feedback.

Siguiente capítulo

Funciones nativas en apps híbridas Ionic: cámara, archivos y geolocalización

Arrow Right Icon
Portada de libro electrónico gratuitaIonic desde Cero: Crea Aplicaciones Híbridas con HTML, CSS y TypeScript
70%

Ionic desde Cero: Crea Aplicaciones Híbridas con HTML, CSS y TypeScript

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.