IntermediateJavaScript · Lesson 1 of 10

Events, Forms & Validation

Handle form submission, validate input, show errors and use event delegation for lists.

Listen to a form's submit event rather than a button's click — it also fires when the user presses Enter. Call event.preventDefault() to stop the browser from reloading the page, then read the values with new FormData(form).

Validate on the client to give instant feedback (required fields, ranges, formats), and show clear error messages next to the inputs. HTML attributes like required, min and max help too. Always validate again on the server — client checks can be bypassed.

Event delegation: instead of attaching a listener to every row, attach one to the parent list and use event.target.closest(...) to find which item was clicked. It works for items added later and uses less memory.

index.htmlHTML
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Register students</title>
    <style>
      .error { color: #b00020; font-size: 0.9rem; min-height: 1.2em; }
    </style>
    <script src="app.js" defer></script>
  </head>
  <body>
    <form id="student-form" novalidate>
      <label>Name <input name="name" /></label>
      <label>Score <input name="score" type="number" /></label>
      <button type="submit">Add</button>
      <p class="error" id="error" role="alert"></p>
    </form>
    <ul id="students"></ul>
  </body>
</html>
Live preview in a sandboxed frame
app.jsJavaScript
const form = document.querySelector("#student-form");
const errorEl = document.querySelector("#error");
const list = document.querySelector("#students");

function validate({ name, score }) {
  if (name.trim().length < 2) return "Name must have at least 2 letters.";
  if (score === "" || Number.isNaN(Number(score))) return "Score must be a number.";
  if (Number(score) < 0 || Number(score) > 100) return "Score must be between 0 and 100.";
  return null;
}

form.addEventListener("submit", (event) => {
  event.preventDefault();
  const data = Object.fromEntries(new FormData(form));   // { name, score }
  const error = validate(data);
  errorEl.textContent = error ?? "";
  if (error) return;

  const li = document.createElement("li");
  li.dataset.score = data.score;
  li.textContent = `${data.name.trim()} - ${data.score} `;
  const remove = document.createElement("button");
  remove.textContent = "Remove";
  remove.className = "remove";
  li.append(remove);
  list.append(li);

  form.reset();
  form.elements.name.focus();
});

// One listener for all current and future "Remove" buttons
list.addEventListener("click", (event) => {
  const button = event.target.closest("button.remove");
  if (button) button.closest("li").remove();
});

Key points

  • Handle submit on the form and call preventDefault().
  • Show validation messages near the input; re-validate on the server.
  • Delegate events to a parent and use closest() to find the clicked item.

Exercise

Extend the form with an email field (validate it contains "@" and a dot after it) and a form selector (1–4). Show each field's error under that field instead of one shared message, and add an "Edit" button per row using delegation.

Show solution

Try the exercise yourself first — then compare your approach with this one.

Each field has its own error element, and validate returns an object of field → message, so every problem shows next to the right input. The list's single click listener handles both Remove and Edit; Edit loads the row back into the form and removes it, so saving re-adds it.

index.htmlHTML
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Register students</title>
    <style>.error { color: #b00020; font-size: 0.85rem; min-height: 1.1em; margin: 2px 0 8px; }</style>
    <script src="app.js" defer></script>
  </head>
  <body>
    <form id="student-form" novalidate>
      <label>Name <input name="name" /></label>
      <p class="error" data-error-for="name"></p>
      <label>Email <input name="email" /></label>
      <p class="error" data-error-for="email"></p>
      <label>Form
        <select name="form">
          <option value="">Choose...</option>
          <option>1</option><option>2</option><option>3</option><option>4</option>
        </select>
      </label>
      <p class="error" data-error-for="form"></p>
      <button type="submit">Save</button>
    </form>
    <ul id="students"></ul>
  </body>
</html>
Live preview in a sandboxed frame
app.jsJavaScript
const form = document.querySelector("#student-form");
const list = document.querySelector("#students");

function validate({ name, email, form: formNumber }) {
  const errors = {};
  if (name.trim().length < 2) errors.name = "Name must have at least 2 letters.";
  const at = email.indexOf("@");
  if (at < 1 || !email.slice(at).includes(".")) errors.email = "Enter a valid email, e.g. amina@example.com.";
  if (!["1", "2", "3", "4"].includes(formNumber)) errors.form = "Choose a form from 1 to 4.";
  return errors;
}

function showErrors(errors) {
  for (const el of form.querySelectorAll("[data-error-for]")) {
    el.textContent = errors[el.dataset.errorFor] ?? "";
  }
}

function button(label, className) {
  const b = document.createElement("button");
  b.type = "button";
  b.textContent = label;
  b.className = className;
  return b;
}

form.addEventListener("submit", (event) => {
  event.preventDefault();
  const data = Object.fromEntries(new FormData(form));
  const errors = validate(data);
  showErrors(errors);
  if (Object.keys(errors).length > 0) return;

  const li = document.createElement("li");
  Object.assign(li.dataset, { name: data.name.trim(), email: data.email, form: data.form });
  li.append(`${data.name.trim()} (Form ${data.form}) - ${data.email} `, button("Edit", "edit"), " ", button("Remove", "remove"));
  list.append(li);
  form.reset();
});

list.addEventListener("click", (event) => {
  const li = event.target.closest("li");
  if (!li) return;
  if (event.target.matches(".remove")) li.remove();
  if (event.target.matches(".edit")) {
    form.elements.name.value = li.dataset.name;
    form.elements.email.value = li.dataset.email;
    form.elements.form.value = li.dataset.form;
    li.remove();
    form.elements.name.focus();
  }
});

Check your understanding

  1. Why listen for the form's submit event instead of the button's click?

  2. What does event.preventDefault() do in a submit handler?

  3. What is event delegation?

  4. Why validate again on the server if the page already validates?

Ask AI