JavaScript Forms — Validation, FormData, File Uploads, and Progressive Enhancement
In this tutorial, you will learn about JavaScript Forms. We cover key concepts, practical examples, and best practices to help you master this topic.
Forms are the primary way users interact with web applications. JavaScript transforms forms from simple HTML submissions into rich, interactive experiences with instant validation, file uploads, and real-time feedback.
DodaTech uses these patterns throughout the dashboard for configuring security scans, managing users, and setting Compliance policies.
What You'll Learn
- Form validation (HTML5 + JavaScript)
- FormData API
- File uploads with progress
- Progressive enhancement
- Accessible form patterns
- Real-time validation
- Form state management
HTML5 Constraint Validation
// HTML5 built-in validation attributes:
// <input required minlength="3" maxlength="20" pattern="[A-Z]+" type="email">
// <input min="0" max="100" type="number">
// Check validity in JavaScript
const form = document.querySelector("#myForm");
const email = document.querySelector("#email");
// Check on blur
email.addEventListener("blur", () => {
if (!email.validity.valid) {
showError(email);
}
});
function showError(input) {
const error = input.parentElement.querySelector(".error-message");
if (input.validity.valueMissing) {
error.textContent = "This field is required";
} else if (input.validity.typeMismatch) {
error.textContent = "Please enter a valid email";
} else if (input.validity.tooShort) {
error.textContent = `Minimum ${input.minLength} characters`;
} else if (input.validity.patternMismatch) {
error.textContent = "Format doesn't match required pattern";
}
}
// Custom validation
email.setCustomValidity(""); // Reset
if (!email.value.includes(".")) {
email.setCustomValidity("Email must have a domain extension");
}
Custom Form Validation
class FormValidator {
constructor(form) {
this.form = form;
this.rules = new Map();
this.errors = new Map();
this.setupValidation();
}
addRule(fieldName, validator, message) {
if (!this.rules.has(fieldName)) {
this.rules.set(fieldName, []);
}
this.rules.get(fieldName).push({ validator, message });
}
setupValidation() {
this.form.addEventListener("submit", (e) => {
if (!this.validate()) {
e.preventDefault();
this.showAllErrors();
}
});
// Real-time validation on blur
this.form.querySelectorAll("input, select, textarea").forEach((input) => {
input.addEventListener("blur", () => {
this.validateField(input.name);
this.showFieldError(input.name);
});
input.addEventListener("input", () => {
this.clearFieldError(input.name);
});
});
}
validate() {
this.errors.clear();
for (const [fieldName, fieldRules] of this.rules) {
this.validateField(fieldName);
}
return this.errors.size === 0;
}
validateField(fieldName) {
const input = this.form.elements[fieldName];
if (!input) return;
const value = input.value;
const fieldRules = this.rules.get(fieldName) || [];
// Clear previous error for this field
this.errors.delete(fieldName);
for (const { validator, message } of fieldRules) {
if (!validator(value, input)) {
this.errors.set(fieldName, message);
return;
}
}
}
showFieldError(fieldName) {
const input = this.form.elements[fieldName];
const errorEl = input.parentElement.querySelector(".field-error");
if (errorEl) {
errorEl.textContent = this.errors.get(fieldName) || "";
input.classList.toggle("invalid", this.errors.has(fieldName));
}
}
clearFieldError(fieldName) {
this.errors.delete(fieldName);
const input = this.form.elements[fieldName];
const errorEl = input.parentElement.querySelector(".field-error");
if (errorEl) {
errorEl.textContent = "";
input.classList.remove("invalid");
}
}
showAllErrors() {
for (const [fieldName] of this.rules) {
this.showFieldError(fieldName);
}
}
}
// Usage
const form = document.querySelector("#registrationForm");
const validator = new FormValidator(form);
validator.addRule("username", (v) => v.length >= 3, "Minimum 3 characters");
validator.addRule("email", (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), "Invalid email");
validator.addRule("password", (v) => v.length >= 8, "Minimum 8 characters");
validator.addRule("password", (v) => /[A-Z]/.test(v), "Must contain uppercase");
validator.addRule("password", (v) => /[0-9]/.test(v), "Must contain a number");
validator.addRule("confirmPassword", (v) => {
const password = form.elements.password.value;
return v === password;
}, "Passwords must match");
FormData API
// Create from form
const form = document.querySelector("#myForm");
const formData = new FormData(form);
// Access values
console.log(formData.get("username"));
console.log(formData.getAll("hobbies")); // Multiple values
// Iterate
for (const [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}
// Build manually
const data = new FormData();
data.append("name", "Alice");
data.append("avatar", fileInput.files[0]);
data.append("metadata", JSON.stringify({ role: "admin" }));
// Submit
fetch("/api/users", {
method: "POST",
body: formData // multipart/form-data
// Don't set Content-Type: browser sets it with boundary
});
// Convert to object
function formDataToObject(formData) {
const obj = {};
for (const [key, value] of formData.entries()) {
if (obj[key] !== undefined) {
if (!Array.isArray(obj[key])) {
obj[key] = [obj[key]];
}
obj[key].push(value);
} else {
obj[key] = value;
}
}
return obj;
}
File Uploads with Progress
class FileUploader {
constructor(options = {}) {
this.endpoint = options.endpoint || "/upload";
this.maxSize = options.maxSize || 10 * 1024 * 1024; // 10MB
this.allowedTypes = options.allowedTypes || [];
}
async upload(file) {
this.validateFile(file);
const formData = new FormData();
formData.append("file", file);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
this.onProgress?.(percent, file.name);
}
});
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener("error", () => {
reject(new Error("Network error during upload"));
});
xhr.open("POST", this.endpoint);
xhr.send(formData);
});
}
validateFile(file) {
if (file.size > this.maxSize) {
throw new Error(`File too large (max ${this.maxSize / 1024 / 1024}MB)`);
}
if (this.allowedTypes.length && !this.allowedTypes.includes(file.type)) {
throw new Error(`File type ${file.type} not allowed`);
}
}
onProgress(percent, filename) {
console.log(`${filename}: ${percent.toFixed(1)}%`);
}
async uploadMultiple(files) {
return Promise.all(files.map((f) => this.upload(f)));
}
}
// Usage
const uploader = new FileUploader({
endpoint: "/api/upload",
maxSize: 5 * 1024 * 1024,
allowedTypes: ["image/jpeg", "image/png", "application/pdf"],
});
uploader.onProgress = (percent, filename) => {
progressBar.style.width = `${percent}%`;
};
fileInput.addEventListener("change", async () => {
try {
const result = await uploader.upload(fileInput.files[0]);
console.log("Uploaded:", result.url);
} catch (err) {
console.error("Upload failed:", err.message);
}
});
Progressive Enhancement
// Start with a working HTML form (no JS needed)
// Then enhance with JavaScript
class EnhancedForm {
constructor(formEl) {
this.form = formEl;
if (!this.form || !window.FormData) return; // No enhancement
this.enhance();
}
enhance() {
this.form.setAttribute("novalidate", ""); // Disable browser validation
this.addRealTimeValidation();
this.addAsyncSubmit();
}
addRealTimeValidation() {
this.form.querySelectorAll("input, select, textarea").forEach((input) => {
input.addEventListener("input", debounce(() => {
this.validateField(input);
}, 300));
});
}
addAsyncSubmit() {
this.form.addEventListener("submit", async (e) => {
e.preventDefault();
if (!this.validate()) return;
const submitBtn = this.form.querySelector("[type=submit]");
submitBtn.disabled = true;
submitBtn.textContent = "Saving...";
try {
const formData = new FormData(this.form);
const response = await fetch(this.form.action, {
method: this.form.method,
body: new URLSearchParams(formData),
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
if (!response.ok) throw new Error("Server error");
this.showSuccess("Saved successfully!");
this.form.reset();
} catch (err) {
this.showError(err.message);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = "Save";
}
});
}
showSuccess(message) {
const el = this.form.querySelector(".form-status");
if (el) {
el.textContent = message;
el.className = "form-status success";
}
}
}
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
Practice Questions
Implement a credit card number validator with Luhn algorithm and formatting (add spaces every 4 digits).
Build a password strength indicator (weak/medium/strong based on length, chars, patterns).
Create a multi-step form wizard with validation at each step.
Implement drag-and-drop file upload with preview.
Build an autocomplete/typeahead component that fetches suggestions from an API.
Challenge: Dynamic Form Builder
Build a class that generates forms from a JSON schema:
const schema = [
{ type: "text", name: "username", label: "Username", required: true },
{ type: "email", name: "email", label: "Email", required: true },
{ type: "select", name: "role", label: "Role", options: ["Admin", "User", "Viewer"] },
{ type: "checkbox", name: "agree", label: "I agree to terms" },
{ type: "file", name: "avatar", label: "Avatar", accept: "image/*" }
];
The builder should:
- Render accessible HTML for each field
- Apply validation rules from schema
- Support dynamic fields (add/remove)
- Serialize/deserialize form state
- Export to JSON on submit
This is how DodaTech dynamically renders configuration forms for different scan types — the schema drives the UI, not hardcoded HTML.
Real-World Task: Draft-Saving Form
Implement a form that automatically saves draft state:
- Save to localStorage on each change (debounced)
- Restore draft on page load
- Show "Unsaved changes" indicator
- Clear draft on successful submit
- Handle version conflicts (stale draft vs new form)
- Support multiple drafts (one per URL)
This is the pattern DodaTech uses for long compliance policy forms — teams often work on policies over hours or days, and drafts prevent lost work.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro