Working with Forms

JavaScript · Lesson 36 of 61 7 min read Updated

Forms are how visitors talk to your site: booking a seat, sending an enquiry, signing up. HTML already gives you a lot for free, including fields, keyboard support and basic validation. JavaScript adds the rest: reading the values without a page reload, showing live totals, checking rules HTML cannot express, and sending the data to a server.

In this lesson you will handle the submit event, read every field with FormData, react to typing with the input event, and add your own validation messages. You should know how to build forms from HTML Forms and how listeners work from Events.

Handle submit, not click

Listen for the submit event on the form, not the click event on the button. A form can be submitted by clicking the button or by pressing Enter in a text field, and only submit catches both. Then call event.preventDefault() so the browser does not reload the page.

Reading values with FormData

new FormData(form) collects every named field in one go. Each field needs a name attribute: fields without one are skipped. Object.fromEntries() turns the result into a plain object.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Booking form</title>
  <style>
    label { display: block; margin-block: 0.5rem; }
    fieldset { margin-block: 0.5rem; }
  </style>
</head>
<body>
  <h1>Book a train seat</h1>
  <form id="booking">
    <label>Passenger name <input name="passenger" required></label>
    <label>Age <input name="age" type="number" min="1" max="120" required></label>
    <label>To
      <select name="destination">
        <option>Jaipur</option>
        <option>Lucknow</option>
        <option>Kochi</option>
      </select>
    </label>
    <fieldset>
      <legend>Meal</legend>
      <label><input type="radio" name="meal" value="veg" checked> Veg</label>
      <label><input type="radio" name="meal" value="non-veg"> Non-veg</label>
    </fieldset>
    <label><input type="checkbox" name="sms" value="yes"> Send me SMS updates</label>
    <button>Book</button>
  </form>
  <p id="summary"></p>

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

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

      const data = new FormData(form);
      const booking = Object.fromEntries(data);
      const age = Number(booking.age);
      const seniorNote = age >= 60 ? " (senior citizen)" : "";
      const sms = data.has("sms") ? "SMS updates on" : "no SMS";

      summary.textContent =
        `${booking.passenger}, age ${age}${seniorNote}, to ${booking.destination}, ` +
        `${booking.meal} meal, ${sms}.`;
    });
  </script>
</body>
</html>

Fill in the form and click Book. For a 64-year-old passenger going to Kochi with SMS ticked, the page shows "Isha, age 64 (senior citizen), to Kochi, veg meal, SMS updates on." Try leaving the age empty: the browser shows its own message and your submit listener never runs, because the field is required.

Four facts about form values save a lot of debugging:

  • Every value is a string, even from type="number". Convert with Number() before doing maths.
  • A radio group gives the value of the checked option.
  • An unticked checkbox is not sent at all, so data.get("sms") is null. Use data.has() to test it.
  • Several checkboxes with the same name need data.getAll("name"), which returns an array.

Reading single fields

You can also reach any field through form.elements and its name. This is handy when you need one value, or a value as a number:

JavaScript
const form = document.querySelector("#booking");
form.elements.passenger.value;   // "Isha" (a string)
form.elements.age.valueAsNumber; // 64 (a number, or NaN when empty)
form.elements.sms.checked;       // true or false
form.elements.meal.value;        // the checked radio's value
form.elements.destination.value; // the selected option's value

Use form.elements.age rather than form.age. Form elements have their own properties, such as action, method and submit, and a field with one of those names would hide them. For the same reason, never name a field submit, and avoid names like item and length, which clash with built-in properties of form.elements.

Live feedback with the input event

The input event fires on every change: each key press, each paste, each new option chosen. The change event fires less often, when the user finishes and leaves the field. Because input bubbles, one listener on the form hears every field.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Live total</title>
</head>
<body>
  <h1>Order sweets</h1>
  <form id="order">
    <label>Box
      <select name="box">
        <option value="450">Kaju katli, 500 g (₹450)</option>
        <option value="320">Motichoor ladoo, 500 g (₹320)</option>
        <option value="280">Soan papdi, 500 g (₹280)</option>
      </select>
    </label>
    <label>Quantity <input name="qty" type="number" value="1" min="1" max="20"></label>
    <p>Total: <output name="total">₹450</output></p>
  </form>

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

    // "input" bubbles, so one listener on the form hears every field.
    form.addEventListener("input", () => {
      const price = Number(form.elements.box.value);
      const qty = form.elements.qty.valueAsNumber; // a number, or NaN if empty

      if (Number.isNaN(qty) || qty < 1) {
        form.elements.total.value = "Enter a quantity";
        return;
      }
      form.elements.total.value = `₹${(price * qty).toLocaleString("en-IN")}`;
    });
  </script>
</body>
</html>

Set the quantity to 3 with kaju katli selected, and the total shows ₹1,350. Clear the box and it asks for a quantity. The <output> element is made for calculated results, and screen readers can announce its changes.

Validation: HTML first, JavaScript for the rest

Start with HTML attributes such as required, type="email", min, max, minlength and pattern (see Form Validation). The browser checks them before it fires submit, shows helpful messages in the user's language, and works even if your script fails to load.

JavaScript steps in for rules that HTML cannot describe, such as "both passwords must match". The key method is field.setCustomValidity(message): a non-empty message marks the field as invalid and becomes the error text, and an empty string marks it valid again.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Custom validation</title>
  <style>
    label { display: block; margin-block: 0.5rem; }
    input:user-invalid { outline: 2px solid crimson; }
  </style>
</head>
<body>
  <h1>Create an account</h1>
  <form id="signup">
    <label>Mobile number
      <input name="mobile" type="tel" inputmode="numeric" required
             pattern="[6-9][0-9]{9}" title="10 digits, starting with 6, 7, 8 or 9">
    </label>
    <label>Password <input name="password" type="password" required minlength="8"></label>
    <label>Confirm password <input name="confirm" type="password" required></label>
    <button>Sign up</button>
  </form>
  <p id="message"></p>

  <script>
    const form = document.querySelector("#signup");
    const { password, confirm } = form.elements;

    function checkMatch() {
      // An empty string means "valid"; any other text marks the field invalid.
      confirm.setCustomValidity(
        confirm.value === password.value ? "" : "Passwords do not match"
      );
    }
    password.addEventListener("input", checkMatch);
    confirm.addEventListener("input", checkMatch);

    form.addEventListener("submit", (event) => {
      event.preventDefault(); // the browser only fires submit if every field is valid
      const button = form.querySelector("button");
      button.disabled = true; // stop double submits
      document.querySelector("#message").textContent =
        `Account created for ${form.elements.mobile.value}.`;
    });
  </script>
</body>
</html>

Other useful tools:

  • form.checkValidity() returns true or false without showing messages; form.reportValidity() also shows them.
  • field.validity tells you why a field is invalid, with flags such as valueMissing, typeMismatch, tooShort and patternMismatch.
  • The CSS :user-invalid selector styles a field only after the user has interacted with it, so an empty form does not start out covered in red.

Sending the data

To send a form to a server without reloading, pass the FormData object straight to fetch(). You will learn the details in Fetch API.

JavaScript
const response = await fetch("/api/enquiry", {
  method: "POST",
  body: new FormData(form), // sent in the same format as a normal form submit
});

Common mistakes

  • Listening for click on the button. Pressing Enter submits the form without a click. Listen for submit on the form.
  • Forgetting preventDefault(). The page reloads and your result vanishes.
  • Doing maths on strings. "2" + 1 is "21". Use Number() or valueAsNumber.
  • Reading values when the page loads. Read them inside the listener, when the user has actually typed something.
  • Calling form.submit(). It skips validation and your submit listeners. Use form.requestSubmit() if you need to submit from code.

Key points

  • Handle the form's submit event and call preventDefault() to stay on the page.
  • new FormData(form) reads every named field; Object.fromEntries() turns it into an object.
  • Form values are strings; unticked checkboxes are missing; use getAll() for groups.
  • The input event gives live feedback, and it bubbles to the form.
  • Use HTML validation first, then setCustomValidity() for custom rules.
  • Always validate again on the server.

Exercise

Build a feedback form with a <textarea name="message" maxlength="160"> and a paragraph that shows "120 characters left" as the user types (update it on the input event). On submit, prevent the reload, show "Thanks! We received 40 characters." with the real number, and reset the form.

Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Feedback</title>
</head>
<body>
  <form id="feedback">
    <textarea name="message" maxlength="160" required></textarea>
    <p id="left">160 characters left</p>
    <button>Send</button>
  </form>
  <p id="thanks"></p>

  <script>
    const form = document.querySelector("#feedback");
    const message = form.elements.message;
    const left = document.querySelector("#left");

    message.addEventListener("input", () => {
      left.textContent = `${160 - message.value.length} characters left`;
    });

    form.addEventListener("submit", (event) => {
      event.preventDefault();
      document.querySelector("#thanks").textContent =
        `Thanks! We received ${message.value.length} characters.`;
      form.reset();
      left.textContent = "160 characters left";
    });
  </script>
</body>
</html>

form.reset() does not fire an input event, so the counter is reset by hand.

Quick Quiz

1. Why should you listen for submit on the form instead of click on the button?

2. What does data.get("sms") return when the sms checkbox is not ticked?

3. How do you mark a field invalid with your own message?

4. form.elements.qty.value is "3". What is form.elements.qty.value + 2?

Learn JavaScript with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call