Ir al contenido
wannacode

Formularios en un componente

En resumen: v-model en Vue une un campo del formulario con el estado en los dos sentidos, así que el estado es la única fuente para comprobar y enviar el formulario.

Problema

Escrito, pero vacío

El comprador rellena el pedido y pulsa «Hacer el pedido». El servidor responde que el nombre es obligatorio. Y el campo «Nombre» muestra el nombre.

Vue

Escribe un nombre, un correo y una dirección, acepta las condiciones y pulsa «Hacer el pedido». El campo conserva «Maria Ivanova», pero la petición lleva name: ''. El correo y la dirección tienen v-model, y el nombre no. El código envía el estado, y el estado no sabe lo que hay escrito en el campo.

La idea

Una verdad: el estado

En la lección sobre el DOM el formulario guardaba el texto él mismo, y el código lo leía al enviar. En un componente hay un segundo lugar: el estado. Si los dos no están conectados, cada uno dice lo suyo, como en el ejemplo.

La solución es que el campo no tenga verdad propia. Muestra el valor del estado y en cada tecla le dice al estado el texto nuevo. Un campo así se llama controlado (en inglés, controlled input).

<script setup>
const name = ref('');
</script>
<template>
<input id="name" v-model="name" />
</template>

v-model es una forma corta para los dos sentidos: :value="name" (del estado al campo) y @input="name = $event.target.value" (el camino de vuelta). El evento input llega en cada tecla.

v-model conoce el tipo de campo. Así que en un select y en una casilla escribes lo mismo:

<select v-model="payment">…</select>
<input type="checkbox" v-model="agree" />

En una casilla agree es true o false, y en un select es el value de la opción elegida.

Ahora el estado es la única fuente: lo que hay en él está en el campo, y al revés. El botón se puede desactivar con el estado (!agree), y la petición envía exactamente lo que ve el comprador.

Míralo

Mira el círculo

Una letra recorre todo el camino: campo, evento, estado, nuevo dibujado y otra vez el campo. Después, el mismo campo sin ligadura.

<script setup>
const name = ref('');
</script>
<template>
<form @submit.prevent="submit">
<label for="name">Nombre</label>
<input id="name" v-model="name" />
</form>
</template>
Paso a paso, en palabras
  1. El formulario guarda el nombre en el estado: name = ''. El campo de la página muestra lo que dice el estado, y dice: vacío.
  2. El comprador pulsa M. El navegador pone la letra en el campo y lanza el evento input.
  3. v-model escucha input. El texto del campo es ahora «M».
  4. v-model lo escribe en el ref: name.value = "M". Es el único lugar donde vive el nombre.
  5. El ref cambió, así que Vue vuelve a ejecutar la plantilla. v-model le da al campo el valor de name: «M».
  6. El campo recibe su valor del estado. Es la misma letra, pero ahora el estado decide lo que muestra el campo.
  7. Cada tecla recorre todo el círculo: campo, evento, estado, nuevo dibujado, campo. Una sola fuente de verdad: el estado.
  8. Ahora el mismo campo sin v-model. El campo funciona, pero nadie lo escucha y nadie le da un valor.
  9. El comprador escribe «María». Las letras se quedan en el DOM. El estado sigue diciendo: name = ''.
  10. Al enviar, el código manda el estado: { name: '' }. El servidor responde «El nombre es obligatorio», mientras que el campo muestra «María».
  11. La regla: cada campo tiene v-model, un valor desde el estado y un evento de vuelta a él. Así el campo y el estado no pueden desajustarse.

El círculo parece un rodeo innecesario: la letra está en el campo también sin él. Pero gracias a él el estado y el campo no pueden diferir.

Pruébalo

Liga los campos

El campo Nombre, el select Pago y la casilla Acepto las condiciones no están ligados al estado. Liga los tres. El botón Hacer el pedido está desactivado hasta que se aceptan las condiciones. La comprobación escribe un nombre, un correo y una dirección, elige Tarjeta en la entrega, marca y desmarca la casilla y después hace el pedido. Espera Pedido n.º 61 recibido. y una petición con el nombre escrito y payment: 'card'.

Vue

Pruébalo

Rellena el hueco

¿Qué va en el hueco para que la casilla quede ligada?

<script setup>
const agree = ref(false);
</script>

<template>
  <input type="checkbox" ="agree" />
</template>

Pruébalo

Encuentra el error

El comprador escribe su nombre y se ve, pero el formulario dice que está vacío. El correo funciona. ¿Qué línea tiene la culpa?

Elige la línea con el error.

La idea

Una comprobación junto al campo

El servidor comprueba cada pedido. Pero el comprador no debería esperar una respuesta para enterarse de que olvidó la dirección. Así que el formulario también comprueba por sí mismo antes de enviar.

  1. Cuándo. Al pulsar «Hacer el pedido». Si hay errores, no sale ninguna petición.
  2. Qué. Una función check() devuelve un objeto con los errores por campo: { name: 'Escribe tu nombre', email: … }. Un objeto vacío significa «todo bien». Se guarda en el estado errors.
  3. Dónde. Junto al campo, no en una lista común arriba. El comprador ve qué campo debe corregir.
  4. Para todos. Un lector de pantalla no ve que el texto es rojo. Por eso el campo recibe aria-invalid="true" (el lector dice «no válido»), y el mensaje recibe un id al que el campo apunta con aria-describedby. Cuando el foco llega al campo, el lector lee también el mensaje.
<input
id="email"
v-model="email"
:aria-invalid="!!errors.email"
:aria-describedby="errors.email ? 'email-error' : undefined"
/>
<p v-if="errors.email" id="email-error" class="error">{{ errors.email }}</p>

El formulario tiene novalidate. Sin él, el navegador detiene el envío con sus propios globos antes de que llegue tu código, y las dos comprobaciones se estorban.

El servidor comprueba otra vez. La comprobación del navegador es una comodidad para el comprador. No protege la tienda: el código de la página está en manos del comprador, y una petición puede llegar también sin él. Por qué es así lo explica la lección «Entrada no confiable».

Pruébalo

Comprueba antes de enviar

Añade una comprobación antes de enviar. Un nombre vacío da Escribe tu nombre, un correo sin @ da El correo debe contener @, una dirección vacía da Escribe una dirección de entrega. Cada mensaje está junto a su campo, en un elemento con el id name-error, email-error y address-error, y el campo tiene aria-invalid=true y aria-describedby apuntando a él. Con errores no sale ninguna petición. La comprobación acepta las condiciones, hace el pedido con el formulario vacío, después con un correo sin @ y al final con todo correcto.

Vue

La idea

Enviar y la respuesta del servidor

La petición tarda, y el comprador no sabe si su clic llegó. Si vuelve a hacer clic, sale un segundo pedido. Por eso el estado tiene también sending:

  1. Antes de fetch, sending pasa a true. El botón está disabled, y a su lado dice «Enviando…».
  2. Después de la respuesta, sending pasa a false, sea lo que sea lo que haya vuelto.
  3. 201: el cuerpo es { id }. El formulario se sustituye por «Pedido n.º 61 recibido.»
  4. 400: el cuerpo es { errors: { name, email, address, items } }, un mensaje por campo. Van al mismo errors, así que aparecen en los mismos lugares junto a los campos. items no tiene campo: aparece junto a la línea con el carrito.
sending.value = true;
const res = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name.value, email: email.value, /* … */ items: ITEMS }),
});
const data = await res.json();
sending.value = false;
if (res.ok) orderId.value = data.id;
else errors.value = data.errors;

La comprobación del navegador es más simple que la del servidor: solo busca «@». El correo maria@shop la pasa, y el servidor lo devuelve con «El correo electrónico no es válido». Así que el formulario también debe saber mostrar un error que no conocía.

Más precisamente, para curiosos

Modificadores de v-model. v-model.trim quita los espacios del principio y del final, v-model.number convierte el texto en número (para type="number"), y v-model.lazy actualiza en change (cuando el campo pierde el foco), no en cada tecla.

:value sin evento. El campo muestra name, las letras van al DOM, pero name no cambia. Cuando el componente se dibuja de nuevo por otro motivo (por ejemplo, aparece un mensaje de error), Vue devuelve el campo al valor de name, y lo escrito desaparece.

Un objeto en vez de cinco refs. También puedes tener const form = reactive({ name: '', email: '', … }) con v-model="form.name". Vue vigila las propiedades del objeto, así que v-model funciona en cada una.

v-model en un componente es :modelValue hacia abajo y @update:modelValue hacia arriba, como en la lección sobre elevar el estado. Así se hacen los componentes de campo (<MoneyInput v-model="price" />).

Cuándo comprobar. Al enviar es lo más simple. Una comprobación en cada tecla muestra errores antes de que el comprador termine («el correo no es válido» tras la primera letra). La respuesta habitual: la primera comprobación al enviar o al salir del campo (blur), y después en cada tecla, para que el error desaparezca en cuanto se corrige.

Foco en un error. Tras un envío fallido ayuda mover el foco al primer campo con error. Así el teclado y el lector de pantalla llegan directo al problema.

La comprobación no ve el código. Las tareas de aquí comprueban el comportamiento: qué se muestra, qué está desactivado, qué se envía. No ven si la comprobación está en una función aparte ni cómo se llaman las variables.

Ejercicio

El pedido, hasta el final

Ahora el formulario comprueba en el navegador, pero no muestra que está enviando y mete los errores del servidor en un solo párrafo. Conviértelo en un pedido de verdad: mientras la petición se ejecuta, el botón Hacer el pedido está desactivado y hay un texto Enviando… (con role=status). Ante una respuesta 400, los errores de data.errors van junto a los campos, en los mismos lugares que los tuyos (email-error y aria-invalid en el correo), y el error de items junto a la línea con el carrito. La comprobación escribe el correo maria@shop: pasa tu comprobación, pero el servidor lo devuelve con El correo electrónico no es válido. Después añade .bg y el pedido sale bien: Pedido n.º 61 recibido.

Vue

Test

Ponte a prueba

1. El campo «Nombre» muestra «María», y el estado name está vacío. ¿Cuál llega al servidor?
2. El campo es <input :value="name" />, sin v-model. ¿Qué pasa cuando el comprador escribe?
3. ¿Qué da v-model en una casilla (type="checkbox")?
4. La comprobación del navegador dice que el correo está bien. ¿Debe el servidor comprobarlo otra vez?
5. ¿Por qué el botón «Hacer el pedido» está desactivado mientras se ejecuta la petición?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·