Forms in JavaScript

Read a form, validate it and submit it without reloading the page.

19of 32
≈ 12Min read
8Parts

Reading the values

  • Only controls with a name are included. A field with no name is invisible here and to the server.
  • An unchecked checkbox is absent, not false. fd.has("consent") is the test.
  • Object.fromEntries keeps only the last value of a repeated name — use getAll for those.
  • Every value is a string, except a file input, which gives you a File.
FormData does the work
const form = document.querySelector("#signup");

const data = Object.fromEntries(new FormData(form));
// { name: "Ulvi", email: "a@b.co", plan: "pro" }

// a field that can appear several times
const fd = new FormData(form);
fd.getAll("interests");        // ["html", "css"]
fd.get("email");
fd.has("consent");             // false when a checkbox is unchecked

Reaching a control

three ways, in order of preference
form.elements.email;          // by name or id — the clearest
form.email;                    // the same thing, shorter
document.querySelector("#email");

form.elements.email.value;
form.elements.consent.checked;
form.elements.plan.value;      // for a radio group: the checked one

Submitting without a reload

the whole pattern
form.addEventListener("submit", async (e) => {
  e.preventDefault();

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

  try {
    const res = await fetch(form.action, {
      method: form.method,
      body: new FormData(form),                 // sends as multipart
    });
    if (!res.ok) throw new Error(`${res.status}`);
    form.reset();
    show("Sent.");
  } catch (err) {
    show("Could not send. Try again.");
  } finally {
    button.disabled = false;                    // even if it threw
  }
});
Send JSON instead when the API wants it

body: JSON.stringify(Object.fromEntries(new FormData(form))) with a Content-Type: application/json header.

Using the browser's validation

the constraint validation API
form.checkValidity();        // true/false, silently
form.reportValidity();       // the same, and shows the native bubbles

field.validity.valid;
field.validity.valueMissing;    // required and empty
field.validity.typeMismatch;    // not an email/url
field.validity.patternMismatch;
field.validity.tooShort;
field.validationMessage;        // the browser's own wording, localised

Your own messages

  • role="alert" makes the message announced when it appears.
  • aria-describedby ties the message to the field, so a screen reader reads it when the field is focused.
  • aria-invalid tells assistive technology the field is in an error state.
  • Focus the first bad field: email.focus(). Otherwise a keyboard user has to hunt for it.
novalidate keeps the API, hides the bubbles
<form id="signup" novalidate>
  <label for="email">Email</label>
  <input type="email" id="email" name="email" required aria-describedby="email-err">
  <p id="email-err" class="error" role="alert"></p>
</form>
script.js
function showError(field, message) {
  const out = document.getElementById(`${field.id}-err`);
  out.textContent = message ?? "";
  field.setAttribute("aria-invalid", message ? "true" : "false");
}

form.addEventListener("submit", (e) => {
  e.preventDefault();
  const email = form.elements.email;

  if (email.validity.valueMissing) return showError(email, "Please enter your email.");
  if (email.validity.typeMismatch) return showError(email, "That does not look like an email address.");

  showError(email, null);
  send();
});

Custom rules

setCustomValidity, and the step everyone forgets
function compare() {
  again.setCustomValidity(
    again.value === pass.value ? "" : "The two passwords do not match."
  );
}

pass.addEventListener("input", compare);
again.addEventListener("input", compare);
Clear it with an empty string

Any non-empty message marks the field invalid forever. The "" branch is not optional.

When to validate

  • On submit — always. This is the one that matters.
  • On blur — after the reader has left the field, so they are not corrected mid-word.
  • On input — only to clear an error that is already showing, never to add one.
  • In CSS, style :user-invalid, not :invalid, so an untouched empty form is not red.

And on the server, again

Everything above is a convenience for the person filling the form. None of it is a defence: a request can be sent with curl, from the console, or by a script that never loaded your page.

Every rule enforced in the browser must be enforced again on the server. Treat the client version as a nicety and the server version as the truth.

Practice