Q1If you type three characters in a text field and then press Tab to move on, how many times do input and change fire?
Forms — Input Values and submit
Read fields with value and checked, respond to input and change events, cancel a submit with preventDefault, and collect fields with FormData.
You can handle clicks with addEventListener, but with a form you also need to read the values typed into its fields and check them before sending. Clicking the submit button makes the browser load a new page, so you also need a way to keep the form from being sent before you've finished checking.
This article covers value, which reads a field's value, and the submit event, which fires when a form is submitted.
Reading Entered Values — value and checked
Suppose you want to read the number entered in the quantity field of an order form (a form element that groups fields and a submit button) and calculate the total price. An input element has no closing tag and can't contain child elements or text, so reading its textContent only gives you an empty string.
Fields are input elements (their type attribute sets the kind of field) or textarea elements for multi-line text. You read a field's contents with its value (a property that gets or sets the field's current value as a string), as in quantity.value, and whether a checkbox is checked with checked. The value attribute written in the HTML is the initial value when the page opens, and typing into the field doesn't change it.
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");
// Pretend the user typed 3 and checked the gift-wrap box
quantity.value = "3";
giftWrap.checked = true;
// value is a string even in a number field, so convert it before calculating
console.log(quantity.value + 1); // 31
console.log(Number(quantity.value) * 1200); // 3600
console.log(giftWrap.checked); // true
// outerHTML still shows the value attribute written in the HTML
console.log(quantity.outerHTML); // <input id="quantity" type="number" value="1">
The same goes for checked: checking the box doesn't add a checked attribute to the HTML. If you clear the quantity field, value becomes an empty string, and Number("") returns 0, so once you've converted it to a number, you can't tell an empty field from an entered 0.
Recounting on Every Keystroke — the input and change Events
A company blog's post editor shows a character count like "13 / 40" under the title field, and it should update as the user types. If you read value only once when the page opens, characters typed afterward never show up in the count.
A field fires an input event every time a character is typed, and a change event when its value has changed and focus leaves the field. You can't type in the console, so you fire an event with the same name using dispatchEvent (a method that fires the given event on that element). You create the event to pass by specifying its name, as in 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: recount the characters on every keystroke
postTitle.addEventListener("input", () => {
titleCount.textContent = `${postTitle.value.length} / 40`;
});
// change: save a draft when the value has changed and focus leaves the field
postTitle.addEventListener("change", () => console.log("Draft saved"));
// Just assigning to value
postTitle.value = "Weekly report";
console.log(titleCount.textContent); // 0 / 40
// Firing an event calls the listeners registered under the same name
postTitle.dispatchEvent(new Event("input"));
console.log(titleCount.textContent); // 13 / 40
postTitle.dispatchEvent(new Event("change")); // Draft saved
The input listener is called for every single character, so work that doesn't need to happen on each keystroke, like saving a draft, belongs in change, which fires when the user finishes typing. The table below shows which of the two events, input or change, each user action fires.
| Action | input Event | change Event |
|---|---|---|
| Type a character in a text field | Fires on each character | Doesn't fire |
| Leave the field (click out or Tab) | Doesn't fire | Fires if the value changed |
| Click a checkbox to toggle it | Fires | Fires |
Canceling a Submission — submit and preventDefault
A newsletter sign-up form should show an error message and block submission when the email address has no @. When the submit button is clicked, the browser sends the form's contents and navigates to a new page, so even if your listener puts an error message on the page, it disappears along with the page.
Right before a form is submitted, a submit event fires on the form element. If you call preventDefault (a method that cancels the action the browser would take after the event) in that listener, the form isn't submitted. You can check whether the cancellation took effect with the event's defaultPrevented (a property that's true if it did).
document.body.innerHTML = `<form id="newsletter-form"><input id="newsletter-email"><button>Subscribe</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");
// Called right before submission. Cancels it only when there's no @
newsletterForm.addEventListener("submit", (event) => {
if (!emailInput.value.includes("@")) {
event.preventDefault();
emailError.textContent = "The email address has no @";
}
});
// Instead of a real submission, create and fire a cancelable submit event
emailInput.value = "alice.example.com";
const submitEvent = new Event("submit", { cancelable: true });
newsletterForm.dispatchEvent(submitEvent);
console.log(submitEvent.defaultPrevented); // true (preventDefault was called)
console.log(emailError.textContent); // The email address has no @
The left and right columns have the same input; the only difference is whether preventDefault() was called. submit fires on the form element, not the submit button, so register the listener on the form. The same listener also handles submissions made by pressing Enter in a field.
Calling click() on a Submit Button Really Submits
A button inside a form becomes a submit button if you don't specify its type, and calling click() on it without canceling actually submits the form, and you won't see any results until the page reloads. A submit fired with dispatchEvent isn't actually sent, but without { cancelable: true }, preventDefault() has no effect.
Collecting All the Field Values at Once — FormData
Now take a shipping address form, where you want to gather the recipient's name, postal code, and whether to leave the package at the door into a single object. If you use querySelector and value for each field, every new field means another line of code to read it. When a form is submitted, each field's name attribute (the name given to its value) is sent paired with its value.
You create a FormData (an object that collects name–value pairs, using the name attributes of the fields in a form) with new FormData(form), and read a value by name with get("zip"). If you pass it to Object.fromEntries (a method that, as the reverse of Object.entries, builds an object from a list of [name, value] pairs), you get one object holding all the fields.
document.body.innerHTML = `<form id="shipping-form">
<input id="address-search" value="Shibuya">
<input name="recipient" value="Alice Smith">
<input name="zip" value="150-0001">
<input name="dropOff" type="checkbox">
<input name="notify" type="checkbox" checked>
</form>`;
const shippingForm = document.querySelector("#shipping-form");
// Collect name–value pairs from the fields in the form
const shippingData = new FormData(shippingForm);
console.log(shippingData.get("zip")); // 150-0001
console.log(shippingData.get("dropOff")); // null
// Build an object from the name–value pairs
const shipping = Object.fromEntries(shippingData);
console.log(JSON.stringify(shipping));
// {"recipient":"Alice Smith","zip":"150-0001","notify":"on"}
| Field | Condition | Value in FormData |
|---|---|---|
| Address search field | No name attribute | Not included |
| recipient and zip | Has a name; text field | Alice Smith and 150-0001 |
| dropOff | Has a name; unchecked box | Not included; get returns null |
| notify | Has a name; checked box | on (when value isn't set) |
Like a Map, FormData lets you iterate over its [name, value] pairs in order, so you can pass it straight to Object.fromEntries. As long as you give new fields a name attribute, you don't need to add any lines to read them.
Printing a FormData Directly Shows {}
The exercise console converts objects to JSON strings before printing them, so if you pass a FormData directly, as in console.log(shippingData), it shows {} even though the field values have been collected. Check its contents by reading them with get, or by turning it into an object with Object.fromEntries first.
Knowledge Check
Answer each question one by one.
Q2What does calling event.preventDefault() in a form's submit listener cancel?
Q3For an unchecked checkbox with name="dropOff", what does FormData's get("dropOff") return?