Aprende leyendo en orden

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">
Los dos value del input quantity
Atributo valuevalue="1"escrito en el HTMLSigue en 1aunque escribasouterHTMLmuestra value="1"Propiedad valueEntrada actual"3"Cambia concada teclaSe lee conquantity.value
El "3" asignado solo entra en la propiedad value; el atributo value sigue en 1. Los valores ingresados se leen de la propiedad value.

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.

En la reserva de un auto de alquiler, calcula el precio a partir de la cantidad de días y de si se necesita silla infantil. El campo de días rentalDays, que ya tiene un valor asignado, y la casilla childSeat ya están declarados.

① Muestra el tipo del valor del campo de días.

② Convierte los días a número y muestra el precio con una tarifa de 6000 yenes por día.

③ Si la casilla está marcada, suma 500 yenes por día y muestra el total de todo el alquiler.

(Si se ejecuta correctamente, aparecerá una explicación.)

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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
Solo asignar no actualiza el contador
postTitle.value ="Informe semanal"No se disparaningún inputNo se llamaal listenerSigue en0 / 40dispatchEvent(…)tras asignarSe disparaun evento inputEl listener leevalue.lengthPasa a15 / 40
Asignar a value no dispara un evento input, así que el contador sigue en 0 / 40. Si disparas el evento con dispatchEvent, se llama al listener.

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ónEvento inputEvento change
Escribir un carácter en un campoCon cada carácterNo se dispara
Salir del campo (clic fuera o Tab)No se disparaSi el valor cambió
Marcar o desmarcar una casillaSe disparaSe dispara

En la pantalla de compra, muestra los campos de dirección solo cuando «Enviar a otra dirección» está marcado. La casilla otherAddress y los campos de dirección addressFields ya están declarados.

① Registra un listener que quite la clase hidden de los campos de dirección cuando la casilla se marque.

② Antes de provocar un clic, muestra el HTML de los campos de dirección.

③ Provoca un clic en la casilla y luego muestra si está marcada y el HTML de los campos de dirección.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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 @
Qué pasa según haya @ y se llame a preventDefault
Se llama allistener de submitSin @:ana.example.comCon @:ana@example.comSin @:ana.example.compreventDefault()y mensaje de errorNo se llama apreventDefault()Se olvidópreventDefault()No se envía;el error se quedaEl navegadorlo envíaSe envía y elerror desaparece
Si llamas a preventDefault() cuando no hay @, el mensaje de error se queda en la página. Si olvidas llamarlo, el navegador cambia de página y el mensaje de error desaparece con ella.

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.

Comprueba que una contraseña nueva tenga la longitud suficiente. passwordForm, newPassword y passwordError ya están declarados.

① Registra un listener que, si la contraseña tiene menos de 8 caracteres, cancele el envío y ponga un mensaje de error, y que en caso contrario deje vacío el mensaje.

② Con "cat2026", dispara un evento submit cancelable y muestra si se canceló.

③ Con "cat2026!", vuelve a crear el evento, dispáralo y muestra si se canceló y el HTML del campo de error.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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"}
CampoCondiciónValor en FormData
Búsqueda de direcciónSin atributo nameNo se incluye
recipient y zipCon name; campo de textoAna López y 150-0001
dropOffCon name; casilla sin marcarNo se incluye; get da null
notifyCon name; casilla marcadaon (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.

En el formulario de postulación de un sitio de empleo, revisa lo que se escribió antes de enviarlo. El formulario applyForm ya está declarado.

① En un listener de submit, cancela el envío, reúne los valores de los campos en un objeto y muéstralo como cadena JSON.

② En el mismo listener, lee el valor del campo de aceptación de correos y, si no tiene valor, muestra «Sin consentimiento».

③ Dispara un evento submit cancelable.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

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?

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?