Skip to content
Happy Programming Guide
Start learning
JavaScript

Working with Forms and User Input in JavaScript

Reading form values, collecting a whole form in one line with FormData, validating at the right moment, and the preventDefault everyone forgets.

Lines of source code on a dark computer screen

Reading a single field is one line. Everything worth knowing is about what happens around it.

JavaScript
const email = document.querySelector("#email").value;

Note that .value is always a string, even from type="number". That is the source of most form bugs.

Reading each type#

JavaScript
// Text, email, password, textarea
const name = document.querySelector("#name").value.trim();

// Number — convert it
const age = Number(document.querySelector("#age").value);

// Checkbox — a boolean
const agreed = document.querySelector("#terms").checked;

// Radio group — the chosen one, or null
const chosen = document.querySelector('input[name="plan"]:checked')?.value ?? null;

// Select
const country = document.querySelector("#country").value;

// Multi-select
const langs = [...document.querySelector("#langs").selectedOptions].map(o => o.value);

// File
const file = document.querySelector("#photo").files[0];

.trim() on text is worth making automatic. A trailing space is invisible and breaks comparisons.

The whole form at once#

JavaScript
const form = document.querySelector("#signup");

const data = Object.fromEntries(new FormData(form));
console.log(data);     // { name: "Ada", email: "ada@example.com" }

This is far better than reading each field by id. It uses the name attributes, so adding a field to the HTML needs no JavaScript change.

One caveat: it keeps only the last value for repeated names. For checkbox groups:

JavaScript
const fd = new FormData(form);
const tags = fd.getAll("tags");     // every checked value

Unchecked checkboxes do not appear in FormData at all, so read them with .checked when you need an explicit false.

Handling submit#

JavaScript
form.addEventListener("submit", (event) => {
  event.preventDefault();          // stop the page reloading

  const data = Object.fromEntries(new FormData(form));
  console.log("Sending", data);
});

Reacting as they type#

JavaScript
const search = document.querySelector("#search");

search.addEventListener("input", (event) => {
  console.log(event.target.value);     // fires on every keystroke
});

For anything expensive — filtering a long list, calling an API — wait until they pause:

JavaScript
function debounce(fn, wait = 300) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), wait);
  };
}

search.addEventListener("input", debounce((e) => {
  runSearch(e.target.value);
}, 300));

Without this, typing “python” fires six searches and the results can arrive out of order.

Validating at the right moment#

Timing matters more than the rules. Telling someone their email is invalid after one letter is hostile.

JavaScript
const email = document.querySelector("#email");

email.addEventListener("blur", () => {
  email.dataset.touched = "true";
  check(email);
});

email.addEventListener("input", () => {
  if (email.dataset.touched) check(email);    // only after first blur
});

function check(input) {
  const error = document.querySelector(`#${input.id}-error`);
  if (input.checkValidity()) {
    error.textContent = "";
    input.removeAttribute("aria-invalid");
  } else {
    error.textContent = input.validationMessage;
    input.setAttribute("aria-invalid", "true");
  }
}

checkValidity() uses the HTML attributes — required, type="email", minlength — so you never write an email regular expression. More in JavaScript form validation.

Sending it#

JavaScript
form.addEventListener("submit", async (event) => {
  event.preventDefault();

  const button = form.querySelector("button[type=submit]");
  button.disabled = true;                  // stop double submission

  try {
    const response = await fetch("/api/signup", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(Object.fromEntries(new FormData(form))),
    });

    if (!response.ok) throw new Error(`Server said ${response.status}`);
    form.reset();
    showMessage("Thanks — you are signed up.");
  } catch (err) {
    showMessage("Could not sign you up. Please try again.");
    console.error(err);
  } finally {
    button.disabled = false;
  }
});

Disabling the button while the request is in flight prevents the duplicate submissions that impatient clicking causes.

Questions people ask#

Why is my number field giving me a string?

.value is always a string. Wrap it in Number() and check with Number.isNaN().

How do I clear a form?

form.reset() restores the original values — which is not the same as clearing, if any fields had defaults.

Why is my checkbox missing from FormData?

Unchecked boxes are not submitted at all. Read .checked directly when you need a definite true or false.

Can I upload a file with fetch?

Yes — pass the FormData object itself as the body and do not set Content-Type; the browser sets it with the required boundary.

Where to go next#

Next lessonJavaScript form validation explained

Keep reading

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *