php elephant sticker
Photo by RealToughCandy.com on Pexels.com
目次

[Guía de campo completa] Diseño de formularios e experiencia de entrada en Laravel — Validación, FormRequest, visualización de errores, archivos, asistentes (wizards), prevención de reenvíos y construcción de una UI accesible

Lo que aprenderás (puntos clave)

  • Cómo construir una validación de Laravel mantenible (FormRequest / rules / mensajes personalizados)
  • Patrones reales como Entrada → Confirmación → Finalización (wizard), borradores y prevención de reenvíos (PRG / doble envío)
  • Diseño seguro de subida de archivos (MIME / tamaño / escaneo de virus / vista previa)
  • Visualización estandarizada de errores (resumen de errores, asociación con el campo, aria-invalid / aria-describedby)
  • Ayuda a la entrada (autocomplete, inputmode, precauciones con máscaras, fechas/teléfono, autocompletado de direcciones)
  • Formularios accesibles (labels, indicadores de requerido, agrupación, finalización por teclado, UI que no dependa del color)
  • Testing (Feature / Dusk) para evitar regresiones en la experiencia de entrada

Público objetivo (¿a quién beneficia?)

  • Ingenieros Laravel de nivel principiante–intermedio: quieren un “patrón” que no se derrumbe a medida que aumentan los formularios
  • Diseñadores / redactores / QA: quieren unificar textos de error y ayudas de entrada de forma que cualquiera pueda entender
  • PM / CS: quieren flujos que reduzcan el abandono y las consultas a soporte

Nivel de accesibilidad: ★★★★★

Se incluyen ejemplos concretos para enfocar el resumen de errores, role="alert", aria-describedby, indicadores de requerido que no dependen del color, fieldset/legend, atributos de ayuda a la entrada, flujos completables con teclado y redacción de errores que tiene sentido cuando se lee en voz alta.


1. Introducción: los formularios son donde la “calidad” se nota más

Un formulario es la pantalla con la que los usuarios interactúan de forma más activa. Si la entrada sale mal, las acciones más importantes—compra o registro—se detienen. Y los formularios poco amables generan consultas como “no lo entiendo”, “no puedo enviar”, “¿qué está mal?”.

Laravel ya ofrece una base sólida para validación, CSRF y manejo de archivos; así que, una vez que decides un patrón de implementación, tu equipo puede construir formularios reutilizables y robustos. Este artículo resume ese patrón con un enfoque “accessibility-first”.


2. Empieza con una política: define tus “estándares” de formularios

Estos son buenos candidatos para estandarizar en un equipo:

  • Cómo indicar campos obligatorios (usar texto como “Obligatorio”; no depender solo del color)
  • Visualización de errores (resumen + asociación con campos + reglas de redacción)
  • Prevención de reenvío (PRG, medidas contra doble envío)
  • Ayuda a la entrada (autocomplete, inputmode, ejemplos, uso de placeholder)
  • Manejo de archivos (tipos permitidos, tamaño, vista previa, escaneo, eliminación)
  • Estado tras completar (mensaje de éxito, siguiente acción)

Cuando defines estos estándares, añadir nuevos formularios se vuelve más rápido y la experiencia se vuelve consistente.


3. Centraliza la validación con FormRequest

La validación se vuelve difícil de mantener si está dispersa por controladores. En Laravel, el enfoque estándar es usar FormRequest para centralizar reglas y mensajes.

php artisan make:request RegisterRequest
// app/Http/Requests/RegisterRequest.php
class RegisterRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => ['required','string','max:50'],
            'email' => ['required','email','max:255'],
            'password' => ['required','string','min:12','confirmed'],
            'agree' => ['accepted'],
        ];
    }

    public function attributes(): array
    {
        return [
            'name' => 'Name',
            'email' => 'Email address',
            'password' => 'Password',
            'agree' => 'Agreement to the Terms of Service',
        ];
    }

    public function messages(): array
    {
        return [
            'agree.accepted' => 'You must agree to the Terms of Service.',
        ];
    }
}

Notas

  • Usar attributes() hace que los nombres de los campos en los mensajes de error sean más fáciles de leer.
  • Con confirmed, Laravel comprueba automáticamente password_confirmation.

4. Controlador: evita reenvíos con PRG (lo más importante)

Con PRG (Post/Redirect/Get), siempre rediriges después del envío.
Esto evita envíos duplicados causados por refrescar (F5).

public function store(RegisterRequest $request)
{
    $user = User::create([
      'name' => $request->name,
      'email' => $request->email,
      'password' => Hash::make($request->password),
    ]);

    return redirect()->route('register.done')
      ->with('status', 'Registration completed successfully.');
}

5. Estándar de visualización de errores: resumen + asociación con el campo

5.1 Resumen de errores (parte superior de la página)

  • Tras enviar, enfoca el resumen para que el usuario entienda “qué pasó” lo antes posible.
  • role="alert" es útil, pero evita abusar—define una regla como “mostrar solo en errores de validación”.
@if ($errors->any())
  <div id="error-summary" role="alert" tabindex="-1" class="border p-3 mb-4">
    <h2 class="font-semibold">Please review your input.</h2>
    <ul class="list-disc pl-5">
      @foreach ($errors->all() as $error)
        <li>{{ $error }}</li>
      @endforeach
    </ul>
  </div>
@endif

Movimiento de foco (ejemplo: JS pequeño)

<script>
  (function(){
    const el = document.getElementById('error-summary');
    if (el) el.focus();
  })();
</script>

5.2 Lado del campo (asociación)

  • Los inputs con error deben tener aria-invalid="true"
  • Asigna un id al mensaje de error y relaciónalo con aria-describedby.
<label for="email" class="block font-medium">
  Email address <span aria-hidden="true">(Required)</span>
  <span class="sr-only">Required</span>
</label>
<input id="email" name="email" type="email" value="{{ old('email') }}"
  aria-invalid="{{ $errors->has('email') ? 'true' : 'false' }}"
  aria-describedby="{{ $errors->has('email') ? 'email-error' : 'email-help' }}"
  autocomplete="email"
  class="border rounded px-3 py-2 w-full">

<p id="email-help" class="text-sm text-gray-600">Example: hanako@example.com</p>

@if($errors->has('email'))
  <p id="email-error" class="text-sm text-red-700">{{ $errors->first('email') }}</p>
@endif

Notas

  • No dependas solo de “texto rojo”: deja el error explícito en texto.
  • Mantén los mensajes de error cortos y específicos (p. ej., “Introduce un formato de correo válido.”).

6. Ayuda a la entrada: autocomplete / inputmode / cómo usar ejemplos

6.1 Valores comunes de autocomplete

  • Nombre: name
  • Email: email
  • Teléfono: tel
  • Dirección: street-address / postal-code / address-level1, etc.
  • Código de un solo uso: one-time-code
  • Contraseña: new-password / current-password

6.2 inputmode

  • Números: inputmode="numeric"
  • Teléfono: inputmode="tel"
  • Email: type="email" suele hacer innecesario inputmode

6.3 Precaución con inputs enmascarados (masked inputs)

Las máscaras que insertan guiones automáticamente para códigos postales o teléfonos pueden ser convenientes, pero pueden afectar negativamente:

  • El comportamiento de copiar/pegar
  • La lectura con lectores de pantalla
  • El movimiento del cursor mientras se escribe
    Es más seguro empezar con ejemplos de entrada y validación, y mantener las máscaras al mínimo.

7. Agrupar el formulario: reduce confusión con fieldset/legend

Para entradas agrupadas como dirección o pago, fieldset y legend hacen más clara la salida de lectores de pantalla.

<fieldset class="border p-3">
  <legend class="font-semibold">Shipping Address</legend>

  <label for="zip" class="block mt-2">Postal code</label>
  <input id="zip" name="zip" autocomplete="postal-code" class="border rounded px-2 py-1">

  <label for="addr" class="block mt-2">Address</label>
  <input id="addr" name="addr" autocomplete="street-address" class="border rounded px-2 py-1 w-full">
</fieldset>

8. Subida de archivos: equilibra seguridad y usabilidad

8.1 Validación (ejemplo: imágenes)

$request->validate([
  'avatar' => ['nullable','file','mimetypes:image/jpeg,image/png','max:2048'],
]);

8.2 Puntos de UX

  • Explica formatos permitidos y tamaño máximo por adelantado (p. ej., JPEG/PNG, hasta 2MB)
  • Tras subir, muestra no solo el nombre del archivo sino también una vista previa (para imágenes)
  • Ofrece un botón de eliminar para recuperarse de selecciones erróneas
  • En producción, considera escaneo antivirus asíncrono y notifica el resultado

Accesibilidad

  • Añade alt a imágenes de vista previa (p. ej., “Vista previa de la imagen de perfil seleccionada”)
  • Anuncia progreso/finalización con role="status"
  • Trata el arrastrar-y-soltar como opcional—los usuarios deben poder completar con el selector estándar de archivos

9. Flujo wizard (Entrada → Confirmación → Finalización): usa session/borradores para confianza

Dividir solicitudes o compras complejas en pasos reduce errores.
Sin embargo, preservar el estado cuando el usuario vuelve atrás es crítico.

9.1 Estructura típica

  • Paso 1 Entrada → guardar en sesión
  • Paso 2 Confirmación → enviar para finalizar (PRG)
  • Paso 3 Finalización → mostrar resultado
// Paso 1: guardar
session(['wizard' => $request->validated()]);
return redirect()->route('apply.confirm');

// Paso 2: finalizar
$data = session('wizard');
abort_if(!$data, 419);
$application = Application::create($data);
session()->forget('wizard');
return redirect()->route('apply.done');

Accesibilidad

  • Muestra claramente el paso actual en texto (p. ej., “Paso 2 de 3: Confirmación”)
  • Para barras de progreso, role="progressbar" más valores numéricos ayuda

10. Prevención del doble envío: tanto del lado servidor como del lado UI

10.1 Lado servidor (idempotency key)

Para operaciones importantes de “crear”, asume que el doble envío puede ocurrir y haz la operación idempotente.
Ejemplo: emitir un UUID oculto por formulario y registrarlo en la BD para evitar duplicados.

10.2 Lado UI (deshabilitar el botón de envío)

  • Deshabilita el botón inmediatamente al enviar
  • Añade aria-disabled="true" para dejar el estado explícito
  • Pero asegúrate de que el lado servidor proteja incluso si JS está deshabilitado
<button id="submit" class="px-4 py-2 border rounded">Submit</button>
<div id="submit-status" role="status" aria-live="polite" class="sr-only"></div>

<script>
document.querySelector('form')?.addEventListener('submit', () => {
  const b = document.getElementById('submit');
  b.disabled = true;
  b.setAttribute('aria-disabled','true');
  document.getElementById('submit-status').textContent = 'Submitting. Please wait.';
});
</script>

11. Mensajes de éxito: diseña para que no se pasen por alto

Para éxito, usa flash messages como with('status', ...) y muéstralos cerca de la parte superior.
Para lectores de pantalla, role="status" funciona bien.

@if(session('status'))
  <div role="status" aria-live="polite" class="border p-3 mb-4">
    {{ session('status') }}
  </div>
@endif

12. Testing: los formularios se rompen fácil, así que protégelos

12.1 Feature test (no devuelve 422)

public function test_register_success()
{
    $res = $this->post('/register', [
      'name'=>'Hanako Yamada',
      'email'=>'hanako@example.com',
      'password'=>'StrongPassw0rd!',
      'password_confirmation'=>'StrongPassw0rd!',
      'agree'=>1,
    ]);
    $res->assertRedirect(route('register.done'));
}

12.2 Dusk (enfoque en el resumen de errores)

Comprueba end-to-end:

  • Enviar → error → el foco se mueve al resumen
  • Se añade aria-invalid
  • aria-describedby cambia al mensaje de error
    Esto evita regresiones de accesibilidad.

13. Errores comunes y cómo evitarlos

  • Sin label, solo placeholder
    • Arreglo: proporciona siempre un label
  • Los errores no aparecen cerca de los inputs
    • Arreglo: muestra el error bajo el campo + aria-describedby
  • Mensajes de error vagos (“Inválido”)
    • Arreglo: di qué cambiar, breve y concreto
  • Requerido/errores indicados solo con color rojo
    • Arreglo: añade texto (“Obligatorio”, detalles del error)
  • Ocurre doble envío
    • Arreglo: PRG + idempotency key + deshabilitar botón
  • Los límites de archivos se descubren demasiado tarde
    • Arreglo: explicar por adelantado + validación estricta del lado servidor
  • El wizard no permite volver atrás
    • Arreglo: guardar en sesión/borrador para restaurar estado

14. Checklist (para compartir)

Validación

  • [ ] Centralizar en FormRequest; usar attributes/messages para legibilidad
  • [ ] Reglas mínimas y explícitas (límites / formato / requerido)

Visualización de errores

  • [ ] Resumen de errores + asociación con el campo
  • [ ] aria-invalid, aria-describedby
  • [ ] Explicar con texto, sin depender del color
  • [ ] Enfocar el resumen

Ayuda a la entrada

  • [ ] autocomplete / inputmode / ejemplos
  • [ ] Máscaras mínimas
  • [ ] Agrupar con fieldset/legend

Envío

  • [ ] Prevenir reenvío con PRG
  • [ ] Idempotency key + deshabilitado en UI
  • [ ] Mensaje de éxito con role="status"

Archivos

  • [ ] Límites MIME/tamaño; escanear si hace falta
  • [ ] Guía previa, vista previa, flujo de eliminación

Tests

  • [ ] Feature tests para éxito/fallo
  • [ ] Dusk tests para foco/ARIA (formularios importantes)

15. Resumen

Los formularios en Laravel mejoran mucho cuando centralizas la validación en FormRequest, previenes reenvíos con PRG y reduces la confusión con un resumen de errores más la asociación campo–mensaje. Al añadir atributos de ayuda de entrada y agrupar con fieldset/legend, y al evitar indicadores de requerido dependientes del color mientras mantienes los mensajes de error cortos, la accesibilidad mejora de forma natural. Diseña desde el inicio para puntos de dolor reales—archivos, wizards y doble envío—y estandariza una experiencia de formulario que no se rompa con facilidad.


Enlaces de referencia

por greeden

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)