Complete JavaScript Course Module 9: Intermediate: Events and Forms

Working with Forms in JavaScript: Read Values and Validate

  • Beginner
  • Lesson 9.4
  • 5 min read
  • 40 of 76 in the course

In short

Forms are how users send you information. This lesson shows how to handle a form submit in JavaScript, read input values, use FormData, and validate fields with clear, accessible error messages.

Forms are how users send you information. This lesson shows how to handle a form submit in JavaScript, read input values, use FormData, and validate fields with clear, accessible error messages.

Handling the submit event

Always listen for the submit event on the form, not the click event on the button. The submit event also fires when the user presses Enter inside a field, so it works for everyone. Call event.preventDefault() first so the page does not reload.

How to read input, checkbox and select values

Each form control has properties you can read:

  • Text, email, number and textarea fields: .value (always a string).
  • Checkboxes: .checked (true or false).
  • Select menus: .value (the value of the chosen option).
  • Radio buttons: find the checked one with form.querySelector('input[name="plan"]:checked').
<!DOCTYPE html>
<html lang="en">
  <body>
    <form id="signup">
      <label for="email">Email</label>
      <input id="email" name="email" type="email">

      <label for="age">Age</label>
      <input id="age" name="age" type="number">

      <label for="country">Country</label>
      <select id="country" name="country">
        <option value="pt">Portugal</option>
        <option value="br">Brazil</option>
      </select>

      <label>
        <input id="news" name="newsletter" type="checkbox">
        Send me the newsletter
      </label>

      <button>Sign up</button>
    </form>

    <script>
      const form = document.querySelector("#signup");

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

        const email = form.querySelector("#email").value.trim();
        const age = Number(form.querySelector("#age").value);
        const country = form.querySelector("#country").value;
        const wantsNews = form.querySelector("#news").checked;

        console.log({ email, age, country, wantsNews });
      });
    </script>
  </body>
</html>
{ email: "ana@example.com", age: 28, country: "br", wantsNews: true }

This is the output after filling in the form and clicking "Sign up". We use trim() to remove spaces around the email and Number() because .value is always a string.

Collecting everything with FormData and Object.fromEntries

Reading fields one by one gets long. new FormData(form) collects every field that has a name attribute. Object.fromEntries() then turns it into a plain object.

// Use inside the submit listener from the example above
const data = Object.fromEntries(new FormData(form));
console.log(data);
// Output: { email: "ana@example.com", age: "28", country: "br", newsletter: "on" }

Notice two things. All values are strings, so convert numbers yourself. And an unchecked checkbox is left out completely, while a checked one has the value "on". Check it with "newsletter" in data or read .checked directly.

Common mistake: Forgetting the name attribute. FormData ignores fields without a name, so they silently disappear from your data.

Custom validation with clear, accessible error messages

HTML already has built-in validation: required, type="email", minlength, min and max. Use those first. JavaScript adds rules HTML cannot express, like "the two passwords must match".

To show errors in a way that everyone can understand, including people using screen readers:

  • Put the message in an element next to the field, and connect it with aria-describedby.
  • Set aria-invalid="true" on the field while it has an error.
  • Use a container with role="alert" or aria-live="polite" so the message is read out when it appears.
  • Don't rely on red colour alone; write the problem in words.

Using setCustomValidity

setCustomValidity("message") marks a field as invalid with your own message. The browser then blocks submission and shows the message in its normal bubble. Calling setCustomValidity("") makes the field valid again. form.reportValidity() checks all fields and shows the first problem.

<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      .error { color: #b00020; min-height: 1.2em; margin: 4px 0 12px; }
      input[aria-invalid="true"] { border: 2px solid #b00020; }
    </style>
  </head>
  <body>
    <form id="account" novalidate>
      <label for="pw">Password</label>
      <input id="pw" name="password" type="password" required minlength="8" aria-describedby="pw-error">
      <p id="pw-error" class="error" aria-live="polite"></p>

      <label for="pw2">Repeat password</label>
      <input id="pw2" name="confirm" type="password" required aria-describedby="pw2-error">
      <p id="pw2-error" class="error" aria-live="polite"></p>

      <button>Create account</button>
    </form>

    <script>
      const form = document.querySelector("#account");
      const pw = document.querySelector("#pw");
      const pw2 = document.querySelector("#pw2");

      const showError = (input, message) => {
        const errorBox = document.querySelector(`#${input.id}-error`);
        errorBox.textContent = message;
        input.setAttribute("aria-invalid", message ? "true" : "false");
      };

      const validate = () => {
        pw2.setCustomValidity(pw.value === pw2.value ? "" : "The passwords do not match.");

        let allValid = true;
        for (const input of [pw, pw2]) {
          if (input.validity.valueMissing) {
            showError(input, "This field is required.");
          } else if (input.validity.tooShort) {
            showError(input, "Use at least 8 characters.");
          } else {
            showError(input, input.validationMessage);
          }
          if (!input.checkValidity()) allValid = false;
        }
        return allValid;
      };

      form.addEventListener("submit", (event) => {
        event.preventDefault();
        if (validate()) {
          console.log("Account created!");
        } else {
          form.querySelector('[aria-invalid="true"]').focus();
        }
      });

      pw2.addEventListener("input", validate);
    </script>
  </body>
</html>
Account created!

The novalidate attribute turns off the browser's own error bubbles, because we show our own messages under each field instead. The rules still work: input.validity tells us why a field is invalid, and checkValidity() returns true or false. setCustomValidity adds our "passwords do not match" rule to the same system, and validationMessage reads it back. If something is wrong, the message appears below the field, the border turns red, and the first invalid field gets focus. When both passwords are at least 8 characters and match, the console logs "Account created!".

Note: Client-side validation is for a better user experience only. Anyone can bypass it, so a server must always check the data again.

Practice exercise

Build a "contact us" form with name (required), email (required, type="email"), a topic <select> and a message <textarea> with at least 20 characters. On submit, prevent the reload, validate, show accessible error messages under each field, and if all is fine, log the data with Object.fromEntries(new FormData(form)) and reset the form with form.reset().

Hint: input.validity.typeMismatch is true when an email field does not contain a valid email address.

Key takeaways

  • Listen for submit on the form and call event.preventDefault() to handle it in JavaScript.
  • Read .value for text and selects, .checked for checkboxes; values are always strings.
  • Object.fromEntries(new FormData(form)) collects all named fields into one object.
  • Use HTML validation first, then setCustomValidity for custom rules.
  • Show errors in words, next to the field, linked with aria-describedby, and always validate again on the server.