Learn JavaScript - Form Validation and User Interaction
Episode 17 of 23

Learn JavaScript - Form Validation and User Interaction

This episode covers forms from two sides: built-in HTML5 validation that's fast, and custom JavaScript validation for more complex rules. You also learn to display clear error messages, validate as you type, and process submission asynchronously.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

Forms are the main gateway for a web application to receive data from users — and also the biggest source of dirty data. Users can submit empty strings, emails without @, or numbers where text belongs. Validation ensures incoming data follows the rules, both before it reaches the server and before it's processed.

Episode 17 covers layered validation. The first layer is HTML5 validation, built into the browser — fast and with no JavaScript code. The second layer is custom JavaScript validation for rules HTML can't express. Finally, you build good feedback: clear error messages, validation as you type, and submission processed asynchronously.

The principle to hold: client-side validation for user experience, server-side validation for security. Both are required — neither replaces the other.

Forms and Input Elements

The Basic Form Structure

Start with the correct HTML structure. The key elements: a form, labels connected to inputs, and appropriate input types:

JSindex.html - a registration form
<form id="form-daftar" novalidate>
  <label for="nama">Nama</label>
  <input type="text" id="nama" name="nama" required minlength="3" />
 
  <label for="email">Email</label>
  <input type="email" id="email" name="email" required />
 
  <button type="submit">Daftar</button>
  <p id="pesan-error" hidden></p>
</form>

<input type="email" tells the browser this field must be email-formatted. required marks a mandatory field, minlength="3" limits the minimum length. The novalidate attribute on the form is deliberately turned on — so you can apply JavaScript validation with full control. The p element with id pesan-error will hold the feedback.

Built-in HTML5 Validation

Leveraging the Constraint Validation API

Even without novalidate, the browser has built-in HTML5 validation. When validation fails, the invalid event and the validity property give detailed information:

JSUsing the built-in validity
const inputEmail = document.querySelector("#email");
 
inputEmail.addEventListener("input", () => {
  console.log("valid:", inputEmail.validity.valid);
  console.log("typeMismatch:", inputEmail.validity.typeMismatch);
  console.log("valueMissing:", inputEmail.validity.valueMissing);
});

inputEmail.validity.valid is true when the input passes all HTML5 rules. validity.typeMismatch flags a wrong format, validity.valueMissing flags an empty required field. When a form tries to submit and something fails, the invalid event fires and validationMessage holds the browser's built-in message — which you can display yourself without the default popup.

Custom JavaScript Validation

Rules HTML Can't Handle

Rules like "password must contain a number" or "confirmation must match the password" can't be expressed with HTML attributes. That's JavaScript's job:

JSCustom validation with a function
function validasiPassword(kataSandi, konfirmasi) {
  if (kataSandi.length < 8) {
    return "Password minimal 8 karakter";
  }
  if (!/[0-9]/.test(kataSandi)) {
    return "Password harus mengandung angka";
  }
  if (kataSandi !== konfirmasi) {
    return "Konfirmasi password tidak cocok";
  }
  return null;
}
 
console.log(validasiPassword("rahasia", "rahasia"));
console.log(validasiPassword("rahasia12", "rahasia12"));

validasiPassword returns an error message string, or null if valid. Separating rules into pure functions like this makes them easy to test. Using null as the "valid" marker is a clear, easy-to-check convention.

Displaying Error Messages

The best error messages appear near the problematic input, not piled up in one place:

JSDisplaying error messages
function tampilkanError(input, pesan) {
  const errorEl = input.nextElementSibling;
 
  if (pesan === null) {
    errorEl.hidden = true;
    input.classList.remove("salah");
  } else {
    errorEl.textContent = pesan;
    errorEl.hidden = false;
    input.classList.add("salah");
  }
}
 
const inputNama = document.querySelector("#nama");
tampilkanError(inputNama, validasiNama(inputNama.value));

tampilkanError uses input.nextElementSibling — assuming the error element sits right after the input in the HTML. When valid, the message is hidden and the salah class is removed; when not, the message shows and the input is marked red. Consistent error placement means users don't have to hunt around.

Validating While Typing and on Submit

Real-time Validation on Every Input

Validation that only happens on submit feels sluggish. A common pattern: light validation during input, full validation on blur and submit:

JSValidation while typing
const inputNama = document.querySelector("#nama");
const form = document.querySelector("#form-daftar");
 
inputNama.addEventListener("input", () => {
  const pesan = inputNama.value.length < 3 ? "Nama minimal 3 karakter" : null;
  tampilkanError(inputNama, pesan);
});
 
form.addEventListener("submit", (event) => {
  event.preventDefault();
 
  const pesan = validasiNama(inputNama.value);
  tampilkanError(inputNama, pesan);
 
  if (pesan === null) {
    console.log("Form valid, siap dikirim");
  }
});

inputNama.addEventListener("input", ...) updates the message on every keystroke. On submit, event.preventDefault() stops the default submission, final validation runs, and only if clean is the data processed. Users get instant feedback without waiting for submit.

Asynchronous Submit with fetch

If all fields are valid, send the data to the server with fetch and display the result:

JSSubmitting a form with fetch
async function kirimPendaftaran(data) {
  const respons = await fetch("/api/daftar", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });
 
  if (!respons.ok) {
    throw new Error(`HTTP ${respons.status}`);
  }
 
  return respons.json();
}
 
const dataForm = {
  nama: document.querySelector("#nama").value,
  email: document.querySelector("#email").value,
};
 
await kirimPendaftaran(dataForm);
console.log("Pendaftaran berhasil");

async function kirimPendaftaran(data) uses the fetch POST pattern from episode 15, complete with the respons.ok check. Separating the send function from the form logic makes the code easy to test, and await errors are caught with try...catch as in episode 16.

Wrap-Up

Episode 17 enabled you to build reliable forms: HTML5 validation with the Constraint Validation API, custom validation for complex rules, clear error messages shown near inputs, validation while typing, and asynchronous submission with fetch.

Key takeaways:

  • HTML5 validation handles common rules without JavaScript.
  • validity and validationMessage give programmatic access to browser validation.
  • novalidate gives full control to JavaScript validation.
  • Complex rules like password matching need custom validation functions.
  • Show errors near the input, not piled in one place.
  • Client-side validation for UX; server-side validation for security.

In the next episode 18 we'll cover Web Storage, cookies, and client-side state — storing data in the browser with localStorage and sessionStorage, working with cookies, and safe, effective state patterns. You'll know when to use which.

Learn JavaScript - Form Validation and User Interaction | Learn JavaScript