Chapter 8.6☕ 18 min read

Forms & Constraint Validation API

Purana tareeqa: har field pe if/else. Naya tareeqa: checkValidity(), setCustomValidity(), FormData. Browser khud check karta hai, tum bas logic likho.

01Event Object — Andar Kaise Kaam Karta Hai?

HTML5 gives you free validation — no JavaScript needed for basic checks. Just add attributes to your form elements and the browser handles the rest.

Core validation attributes:

required — field must have a value

minlength / maxlength — character count bounds

min / max — numeric range (for type="number")

step — allowed increment for numbers

pattern — regex pattern matching

type="email" / type="url" / type="number" — format checking

When the user submits the form, the browser checks all constraints. If any field is invalid, submission is blocked and a native tooltip appears near the field.

<form id="signup">
  <input type="text" name="name" required minlength="2">
  <input type="email" name="email" required>
  <input type="number" name="age" min="18" max="120" step="1">
  <input type="text" name="username"
    pattern="[a-zA-Z0-9_]{3,15}"
    title="3-15 chars: letters, numbers, underscore">
  <input type="url" name="website">
  <button type="submit">Sign Up</button>
</form>

<!-- Disable built-in validation -->
<form novalidate>
  <!-- Browser will not validate -->
</form>

pattern uses regular expressions. The title attribute provides a hint when pattern validation fails. Without a title, the browser shows a generic "Please match the requested format" message.

Browser tooltips work but look different across Chrome, Firefox, Safari — you cannot fully style them with CSS.

Add novalidate attribute (or set form.noValidate = true) to disable all HTML5 validation. Useful when you want full JavaScript control over validation and error display.

02preventDefault — Default Action Rokna

The Constraint Validation API gives you programmatic access to validation state. Instead of relying on browser tooltips, you can check validity, read specific error flags, and set custom error messages.

element.validity returns a ValidityState object with boolean flags for each validation rule:

valueMissing — empty but required

typeMismatch — wrong format for email/url/number

patternMismatch — regex pattern failed

tooShort / tooLong — length bounds violated

rangeUnderflow / rangeOverflow — number out of min/max range

stepMismatch — number doesn't match step value

badInput — browser cannot convert the input

customError — set via setCustomValidity()

const email = document.querySelector("input[name=email]");
const form = document.getElementById("signup");

email.addEventListener("input", () => {
  email.setCustomValidity(""); // Clear first!
  
  if (email.value.endsWith("@tempmail.com")) {
    email.setCustomValidity("Temporary emails not allowed.");
  }
  
  if (!email.checkValidity()) {
    console.log("Invalid!", email.validity);
    // validity.valueMissing = true if empty + required
    // validity.typeMismatch = true if bad email format
    // validity.customError = true if setCustomValidity called
  }
});

form.addEventListener("submit", (e) => {
  e.preventDefault();
  if (!form.checkValidity()) {
    form.reportValidity();
    return;
  }
  console.log("Form is valid!");
});
checkValidity vs reportValidity: checkValidity() returns true/false with no UI — pure logic. reportValidity() does the same check but ALSO shows the browser's native validation tooltips. Use checkValidity for custom error display, reportValidity when you want browser UI.

element.validationMessage returns the current error message string. Empty string if the field is valid. Includes both built-in and custom messages.

03stopPropagation vs stopImmediatePropagation

Browser tooltips are limited — you can't style them consistently or position them precisely. The solution: custom error display using checkValidity() combined with your own DOM manipulation.

When to validate:

input event — fires on every keystroke, responsive but can be noisy. Use with debounce for API checks.

blur event (focusout) — fires when user leaves the field, less intrusive, good for format checks.

submit event — final check before processing. Always validate here regardless of other events.

function validateField(field) {
  field.setCustomValidity(""); // Clear first!
  
  if (field.name === "username" && field.value.includes(" ")) {
    field.setCustomValidity("Username cannot contain spaces.");
  }
  
  const errorEl = field.parentElement.querySelector(".error-msg");
  
  if (!field.checkValidity()) {
    field.classList.add("invalid");
    if (errorEl) {
      errorEl.textContent = field.validationMessage;
    }
    return false;
  } else {
    field.classList.remove("invalid");
    if (errorEl) errorEl.textContent = "";
    return true;
  }
}

form.addEventListener("input", (e) => {
  if (e.target.matches("input, textarea, select")) {
    validateField(e.target);
  }
});

form.addEventListener("focusout", (e) => {
  if (e.target.matches("input, textarea, select")) {
    validateField(e.target);
  }
});

form.addEventListener("submit", (e) => {
  e.preventDefault();
  let isValid = true;
  form.querySelectorAll("input, textarea, select").forEach(field => {
    if (!validateField(field)) isValid = false;
  });
  if (isValid) processForm();
});
Always clear custom errors first! Call setCustomValidity("") before re-validating. The browser does NOT auto-clear custom errors when the user fixes their input. If you don't clear, the field stays invalid forever — a very common bug.
04event Properties — target, currentTarget, type, timeStamp & More

FormData is the modern way to collect form data — no manual DOM queries needed. One call extracts everything: text inputs, files, checkboxes, all of it.

new FormData(formElement) collects ALL form fields automatically. No need to read each input individually.

form.addEventListener("submit", (e) => {
  e.preventDefault();
  const formData = new FormData(form);
  
  // Get individual values
  const name = formData.get("name");       // "Sai"
  const email = formData.get("email");     // "sai@example.com"
  const age = formData.get("age");         // "25" (always string!)
  const file = formData.get("avatar");     // File object
  
  // Multiple values (checkboxes)
  const skills = formData.getAll("skills"); // ["js", "css", "html"]
  
  // Check if field exists
  formData.has("name"); // true
  
  // Iterate all entries
  for (const [key, value] of formData.entries()) {
    console.log(key, value);
  }
  
  // Send with fetch — Content-Type set AUTOMATICALLY!
  fetch("/api/users", {
    method: "POST",
    body: formData // Do NOT set Content-Type!
  });
  
  // Add extra data not in the form
  formData.append("source", "javascript-course");
});
Never set Content-Type with FormData: When you pass FormData as the fetch body, the browser automatically sets Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXYZ. Setting it manually removes the boundary string, and the server cannot parse the request. Just pass the formData as the body and let the browser handle the header.

FormData methods: .get(name) gets first value, .getAll(name) gets all values (for checkboxes), .has(name) checks existence, .append(key, value) adds data, .delete(key) removes, .entries() returns an iterable.

Key detail: formData.get("age") returns "25" (string), not 25 (number). FormData always returns strings for text inputs. Convert with Number() or parseInt() if needed.

05Creating & Dispatching Synthetic Events

Not every validation should happen on every keystroke. Debounced validation waits for the user to stop typing before checking — perfect for API calls like username availability.

Password strength is another real-world pattern: calculate a score based on length, character variety, and common patterns, then show visual feedback as the user types.

// Debounced username availability check
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const checkUsername = debounce(async (username) => {
  const res = await fetch("/api/check-username?u=" + username);
  const data = await res.json();
  const field = document.querySelector("input[name=username]");
  if (!data.available) {
    field.setCustomValidity("Username already taken!");
  } else {
    field.setCustomValidity("");
  }
  validateField(field);
}, 300);

usernameInput.addEventListener("input", (e) => {
  checkUsername(e.target.value);
});

// Password strength meter
passwordInput.addEventListener("input", (e) => {
  const pwd = e.target.value;
  let strength = 0;
  if (pwd.length >= 8) strength++;
  if (pwd.length >= 12) strength++;
  if (/[A-Z]/.test(pwd)) strength++;
  if (/[0-9]/.test(pwd)) strength++;
  if (/[^A-Za-z0-9]/.test(pwd)) strength++;
  
  const meter = document.getElementById("strength-meter");
  meter.className = "strength-" + Math.min(strength, 4);
});
reportValidity() triggers synchronous reflow: The browser must calculate where to position the validation tooltip, forcing a layout recalculation. Avoid calling it in tight loops or rapid succession. For custom error display, use checkValidity() instead — it is purely logical and does not force layout.

Confirm password matching: Compare the password and confirm fields in real-time. Use setCustomValidity("Passwords do not match") on the confirm field when they differ, and clear it when they match.

Lo kar liya — Key Points:

  • ✅ HTML5 validation attributes (required, minlength, pattern, type) provide free browser-side validation
  • ✅ element.validity returns a ValidityState object with boolean flags for each validation rule
  • ✅ checkValidity() returns true/false without UI; reportValidity() also shows browser validation tooltips
  • ✅ setCustomValidity("message") adds a custom error; setCustomValidity("") clears it — must clear manually
  • ✅ Validate on input for real-time feedback, blur for less intrusive checks, and submit for the final gate
  • ✅ new FormData(form) collects all form data automatically, including files, and works directly with fetch
  • ✅ Don't set Content-Type manually when sending FormData with fetch — the browser sets it with the correct boundary
  • ✅ Use debounce for API-based validation (like username availability) to avoid excessive requests
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase