EduFlow
JavaScript Fundamentals
0%

Section 4 · JavaScript in the Browser

Forms and Validation

17 min3 min readCode playground

Lesson at a glance

Handle a form’s submit event without reloading the page, read and convert its values, validate them with HTML attributes and your own function, and show clear, accessible error messages.

  1. 1The form and the submit event
  2. 2Reading the values
  3. 3Validation in a pure function
  4. 4Putting it together
  5. 5Clearing errors as the user types
  6. 6HTML validation: the first layer

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Pocket needs a way to add expenses that is not a “sample” button: a form with a label, an amount and a category. Forms look simple and are where many real bugs live, because they sit on the border between people, who type anything, and your code, which expects clean data.

The form and the submit event

Start with proper HTML. Every input has a <label>, and the button is a submit button inside the <form>:

index.html
<form id="expense-form" novalidate>
  <label for="label">What</label>
  <input id="label" name="label" required>

  <label for="amount">Amount ($)</label>
  <input id="amount" name="amount" type="number" step="0.01" min="0.01" required>

  <label for="category">Category</label>
  <select id="category" name="category">
    <option value="food">Food</option>
    <option value="travel">Travel</option>
    <option value="fun">Fun</option>
  </select>

  <p id="form-error" class="error" role="alert"></p>
  <button type="submit">Add expense</button>
</form>

A form can be submitted by clicking the button, by pressing Enter in a field, or by assistive technology. All of these fire one event on the form: submit. Listen to that, not to the button’s click:

app.js
const form = document.querySelector("#expense-form");

form.addEventListener("submit", (event) => {
  event.preventDefault();
  // read, validate, add, render
});

The browser’s default reaction to a submit is to send the data to a server and load a new page. For an app that runs in the page, that reload wipes out everything. event.preventDefault() cancels the default and keeps you on the page.

Reading the values

You can read each field’s value, or collect them all at once with FormData, which uses each field’s name attribute:

app.js
const data = new FormData(form);
const rawLabel = data.get("label");     // "  Coffee with Sam "
const rawAmount = data.get("amount");   // "4.50": a string!
const category = data.get("category");  // "food"

Every value is a string, even from a number input. This is the “typed input” problem from section 1, now for real. Clean the values up immediately: trim() the label, and convert the amount once, into cents:

app.js
const label = rawLabel.trim();
const amount = Math.round(Number(rawAmount) * 100);

Validation in a pure function

Put the rules in one function that takes values and returns what is wrong with them. It does not touch the page, so it is easy to read and easy to test:

JavaScript
function validateExpense({ label, amount }) {
  const errors = {};
  if (label.trim() === "") {
    errors.label = "Enter what you spent money on.";
  }
  if (!Number.isFinite(amount) || amount <= 0) {
    errors.amount = "Enter an amount greater than zero.";
  }
  return errors;
}

console.log(validateExpense({ label: "  ", amount: Number("") }));
console.log(validateExpense({ label: "Coffee", amount: 4.5 }));

An empty object means “valid”, which you test with Object.keys(errors).length === 0. Notice Number("") is 0, the empty-input trap from section 1, so “nothing typed” correctly fails the amount <= 0 rule, and Number.isFinite rejects NaN and Infinity in one check.

On submit: prevent the default reload, read and convert the values, then validate. If there are errors, show them and focus the first invalid field. If valid, add the expense, render, and reset the form. submit preventDefault, read + convert validate show errors, focus the field add, render, reset the form errors valid
What happens on submit

Putting it together

app.js
form.addEventListener("submit", (event) => {
  event.preventDefault();
  const data = new FormData(form);
  const values = {
    label: data.get("label").trim(),
    amount: Math.round(Number(data.get("amount")) * 100),
    category: data.get("category"),
  };

  const errors = validateExpense(values);
  const errorEl = document.querySelector("#form-error");
  if (Object.keys(errors).length > 0) {
    errorEl.textContent = Object.values(errors).join(" ");
    const firstBad = form.elements[Object.keys(errors)[0]];
    firstBad.setAttribute("aria-invalid", "true");
    firstBad.focus();
    return;
  }

  errorEl.textContent = "";
  expenses = [...expenses, { id: nextId(), ...values }];
  renderExpenses();
  form.reset();
  form.elements.label.focus();
});

On failure, the message goes in an element with role="alert", which screen readers announce as soon as its text changes; the first invalid field is marked with aria-invalid and receives focus, so the user can fix it straight away. On success, the data changes, the list re-renders, form.reset() empties the fields, and focus returns to the first field, ready for the next expense. A real app would also clear aria-invalid once a field is fixed. form.elements.label looks up a field by its name.

Clearing errors as the user types

Showing an error is half the job; the other half is taking it away at the right moment. If the message stays on screen while the user is already typing the fix, the form feels like it is scolding them. Listen for input events on the form (they bubble, so one listener covers every field) and clear the error state of whichever field changed:

app.js
form.addEventListener("input", (event) => {
  event.target.removeAttribute("aria-invalid");
  document.querySelector("#form-error").textContent = "";
});

Resist the opposite urge, validating every keystroke and showing “Enter an amount” while someone is halfway through typing it. A good default: validate on submit, clear on input.

HTML validation: the first layer

The required, type="number", min and step attributes let the browser check basic rules and show its own messages. The novalidate attribute on the form above turns those messages off so yours are shown consistently, while the attributes still document intent and give phones the right keyboard. You can also ask the browser directly with form.checkValidity().

Your form now adds real expenses. Reload the page, though, and they are gone, because they only ever lived in a variable. Next lesson fixes that with JSON and localStorage.

Key takeaways

  • Listen for submit on the form, not click on the button, so every way of submitting (button, Enter key, assistive technology) runs your code, and call event.preventDefault() to stop the page reloading.
  • Every value read from a form is a string; trim text and convert numbers before using them.
  • Keep validation in a pure function that takes values and returns errors, so the rules are easy to read and test.
  • HTML attributes such as required and min are a useful first layer, but your JavaScript must still validate, and the server must too.
  • Show errors next to the field, mark it with aria-invalid, and move focus so keyboard and screen-reader users notice.

Further reading

Check your understanding

3 questions

Question 1 of 3

You attach your “add expense” logic to the Save button’s click event. What breaks?

Question 2 of 3

A number input contains 12.50. What is input.value?

Question 3 of 3

Your form has required and min="0.01" on the amount field. Why validate again in JavaScript?

Practice

Code playground

Add expenses with a validated form

Make Pocket’s form add expenses safely.

  1. Write validateExpense({ label, amount }). It returns an object with a label message when the label is empty or only spaces, and an amount message when the amount is not a finite number greater than 0. Valid values give {}.
  2. Fix the submit handler: stop the page from reloading, trim the label and convert the amount from dollars to whole cents, then validate.
  3. If there are errors, put a message in #form-error, set aria-invalid="true" on the first invalid field and add nothing.
  4. If valid, add the expense, re-render, clear #form-error and reset the form.
<form id="expense-form" novalidate>
  <label for="label">What</label>
  <input id="label" name="label">
  <label for="amount">Amount ($)</label>
  <input id="amount" name="amount" type="number" step="0.01">
  <p id="form-error" role="alert"></p>
  <button type="submit">Add expense</button>
</form>
<ul id="expense-list"></ul>
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Preview

Console

(no output)

Checks

  • validateExpense returns the right messages, and {} for valid values
  • An empty form shows an error, marks the label field invalid and adds nothing
  • The submit handler calls preventDefault()
  • A valid entry is added with a trimmed label and the amount in cents
  • After a successful add, the form and error message are cleared