Pregunta 1Si escribes tres caracteres en un campo de texto y luego pulsas Tab para pasar al siguiente, ¿cuántas veces se disparan input y change?
Formularios — valores de entrada y submit
value y checked para leer campos, los eventos input y change, preventDefault para cancelar un submit y FormData para reunir los valores de todos los campos.
Los clics los puedes recibir con addEventListener, pero en un formulario además tienes que leer los valores que se escriben en los campos y revisarlos antes de enviarlos. Al pulsar el botón de envío, el navegador carga otra página, así que también necesitas una forma de evitar que el formulario se envíe antes de terminar de revisarlo.
En este artículo verás value, que lee el valor de un campo, y el evento submit, que ocurre cuando se envía un formulario.
Leer los valores ingresados — value y checked
En un formulario de pedido (un elemento form que agrupa los campos y el botón de envío), quieres leer la cantidad que se escribió en el campo de cantidad y calcular el precio total. Un elemento input no lleva etiqueta de cierre y no puede contener elementos hijos ni texto, así que, si lees su textContent, solo obtienes una cadena vacía.
Los campos son elementos input (su atributo type define qué tipo de campo son) o elementos textarea para texto de varias líneas. Para leer el contenido de un campo usas su value (una propiedad para leer y escribir el valor actual del campo como cadena), como en quantity.value, y para saber si una casilla está marcada, checked. El atributo value escrito en el HTML es el valor inicial al abrir la página, y no cambia aunque escribas en el campo.
document.body.innerHTML = `<form id="order-form"><input id="quantity" type="number" value="1"><input id="gift-wrap" type="checkbox"></form>`;
const quantity = document.querySelector("#quantity");
const giftWrap = document.querySelector("#gift-wrap");
// Como si el usuario hubiera escrito 3 y marcado la casilla de envolver para regalo
quantity.value = "3";
giftWrap.checked = true;
// value es una cadena incluso en un campo numérico, así que conviértelo antes de calcular
console.log(quantity.value + 1); // 31
console.log(Number(quantity.value) * 1200); // 3600
console.log(giftWrap.checked); // true
// outerHTML sigue mostrando el atributo value que se escribió en el HTML
console.log(quantity.outerHTML); // <input id="quantity" type="number" value="1">
Con checked pasa lo mismo: marcar la casilla no agrega un atributo checked al HTML. Si vacías el campo de cantidad, value pasa a ser una cadena vacía, y Number("") devuelve 0, así que una vez convertido a número no puedes distinguir un campo vacío de un 0 escrito.
Volver a contar en cada tecla — los eventos input y change
En el editor de entradas del blog de una empresa, debajo del campo del título se muestra la cantidad de caracteres, como «15 / 40», y debe actualizarse mientras el usuario escribe. Si lees value una sola vez al abrir la página, los caracteres que se escriban después nunca se reflejan en el contador.
Un campo dispara un evento input cada vez que se escribe un carácter, y un evento change cuando su valor cambió y el foco sale del campo. En la consola no puedes escribir, así que disparas un evento con el mismo nombre usando dispatchEvent (un método que dispara en ese elemento el evento que le pasas). El evento que pasas lo creas indicando su nombre, como en new Event("input").
document.body.innerHTML = `<form id="post-form"><input id="post-title"><p id="title-count">0 / 40</p></form>`;
const postTitle = document.querySelector("#post-title");
const titleCount = document.querySelector("#title-count");
// input: vuelve a contar los caracteres en cada tecla
postTitle.addEventListener("input", () => {
titleCount.textContent = `${postTitle.value.length} / 40`;
});
// change: guarda un borrador cuando el valor cambió y el foco sale del campo
postTitle.addEventListener("change", () => console.log("Borrador guardado"));
// Solo se asigna a value
postTitle.value = "Informe semanal";
console.log(titleCount.textContent); // 0 / 40
// Al disparar un evento, se llaman los listeners registrados con ese mismo nombre
postTitle.dispatchEvent(new Event("input"));
console.log(titleCount.textContent); // 15 / 40
postTitle.dispatchEvent(new Event("change")); // Borrador guardado
El listener de input se llama con cada carácter, así que el trabajo que no hace falta repetir en cada tecla, como guardar un borrador, va en change, que ocurre cuando el usuario termina de escribir. La tabla de abajo muestra cuál de los dos eventos, input o change, dispara cada acción del usuario.
| Acción | Evento input | Evento change |
|---|---|---|
| Escribir un carácter en un campo | Con cada carácter | No se dispara |
| Salir del campo (clic fuera o Tab) | No se dispara | Si el valor cambió |
| Marcar o desmarcar una casilla | Se dispara | Se dispara |
Cancelar el envío — submit y preventDefault
En el formulario de suscripción a un boletín, quieres mostrar un mensaje de error e impedir el envío cuando el correo electrónico no tiene @. Al pulsar el botón de envío, el navegador envía el contenido del formulario y pasa a otra página, así que, aunque tu listener ponga un mensaje de error, este desaparece junto con la página.
Justo antes de que se envíe un formulario, ocurre un evento submit en el elemento form. Si en ese listener llamas a preventDefault (un método que cancela la acción que el navegador haría a continuación del evento), el formulario no se envía. Para saber si la cancelación surtió efecto, lee la propiedad defaultPrevented del evento (vale true si se canceló).
document.body.innerHTML = `<form id="newsletter-form"><input id="newsletter-email"><button>Suscribirme</button><p id="email-error"></p></form>`;
const newsletterForm = document.querySelector("#newsletter-form");
const emailInput = document.querySelector("#newsletter-email");
const emailError = document.querySelector("#email-error");
// Se llama justo antes del envío. Lo cancela solo si no hay @
newsletterForm.addEventListener("submit", (event) => {
if (!emailInput.value.includes("@")) {
event.preventDefault();
emailError.textContent = "El correo no tiene @";
}
});
// En lugar de un envío real, crea y dispara un evento submit cancelable
emailInput.value = "ana.example.com";
const submitEvent = new Event("submit", { cancelable: true });
newsletterForm.dispatchEvent(submitEvent);
console.log(submitEvent.defaultPrevented); // true (se llamó a preventDefault)
console.log(emailError.textContent); // El correo no tiene @
Las columnas izquierda y derecha tienen la misma entrada; lo único que cambia es si se llamó a preventDefault(). El evento submit ocurre en el elemento form, no en el botón de envío, así que registra el listener en el form. Ese mismo listener también recibe los envíos que se hacen pulsando Enter en un campo.
Llamar a click() en un botón de envío envía el formulario de verdad
Un button dentro de un form se convierte en botón de envío si no indicas su type, y si llamas a click() sobre él sin cancelar, el formulario se envía de verdad y no ves ningún resultado hasta que la página se recarga. Un submit disparado con dispatchEvent no envía nada, pero sin { cancelable: true }, preventDefault() no tiene efecto.
Reunir los valores de todos los campos — FormData
Ahora tienes un formulario de dirección de envío y quieres reunir en un solo objeto el nombre del destinatario, el código postal y si se puede dejar el paquete en la puerta. Si usas querySelector y value para cada campo, cada campo nuevo te obliga a agregar otra línea para leerlo. Cuando se envía un formulario, el atributo name de cada campo (el nombre que se le da a su valor) se envía junto con su valor.
Un FormData (un objeto que reúne pares de nombre y valor a partir del atributo name de los campos de un form) se crea con new FormData(form), y con get("zip") lees un valor por su nombre. Si se lo pasas a Object.fromEntries (un método que, al revés que Object.entries, construye un objeto a partir de una lista de pares [nombre, valor]), obtienes un objeto con todos los campos.
document.body.innerHTML = `<form id="shipping-form">
<input id="address-search" value="Shibuya">
<input name="recipient" value="Ana López">
<input name="zip" value="150-0001">
<input name="dropOff" type="checkbox">
<input name="notify" type="checkbox" checked>
</form>`;
const shippingForm = document.querySelector("#shipping-form");
// Reúne los pares de nombre y valor de los campos del form
const shippingData = new FormData(shippingForm);
console.log(shippingData.get("zip")); // 150-0001
console.log(shippingData.get("dropOff")); // null
// Construye un objeto a partir de los pares de nombre y valor
const shipping = Object.fromEntries(shippingData);
console.log(JSON.stringify(shipping));
// {"recipient":"Ana López","zip":"150-0001","notify":"on"}
| Campo | Condición | Valor en FormData |
|---|---|---|
| Búsqueda de dirección | Sin atributo name | No se incluye |
| recipient y zip | Con name; campo de texto | Ana López y 150-0001 |
| dropOff | Con name; casilla sin marcar | No se incluye; get da null |
| notify | Con name; casilla marcada | on (si no hay value) |
Al igual que un Map, FormData te permite recorrer sus pares [nombre, valor] en orden, así que puedes pasárselo directamente a Object.fromEntries. Mientras les pongas un atributo name a los campos nuevos, no necesitas agregar líneas para leerlos.
Si muestras un FormData directamente, aparece {}
La consola de los ejercicios convierte los objetos en cadenas JSON antes de mostrarlos, así que si le pasas un FormData directamente, como en console.log(shippingData), muestra {} aunque los valores de los campos estén reunidos. Revisa su contenido leyéndolo con get o convirtiéndolo antes en un objeto con Object.fromEntries.
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2¿Qué cancela una llamada a event.preventDefault() en el listener de submit de un form?
Pregunta 3En una casilla sin marcar con name="dropOff", ¿qué devuelve get("dropOff") de FormData?