33. Event Handling
Events are a fundamental part of web interactivity. Every user action — click, typing, hover, submit — triggers an event that can be handled by JavaScript.
Code Example
addEventListener — Basic Event Handling
// The most common way
const button = document.querySelector("#btnSubmit");
button.addEventListener("click", function(event) {
console.log("Button clicked!");
console.log("Target:", event.target);
console.log("Time:", event.timeStamp);
});
// Arrow function
button.addEventListener("click", (event) => {
console.log("Clicked!");
});
// Named function (can be removed later)
function handleSubmit(event) {
event.preventDefault(); // prevent the default form submit
console.log("Form disubmit!");
}
const form = document.querySelector("#registrationForm");
form.addEventListener("submit", handleSubmit);
removeEventListener
function handleScroll() {
console.log("User scroll!");
// heavy computation
}
// Add the event
window.addEventListener("scroll", handleScroll);
// Remove the event — you must use the same function reference!
window.removeEventListener("scroll", handleScroll);
// Example: auto-hide after 5 seconds
function setupAutoHide() {
const banner = document.querySelector("#infoBanner");
function hideBanner() {
banner.style.display = "none";
window.removeEventListener("scroll", handleScroll);
}
function handleScroll() {
if (window.scrollY > 200) {
hideBanner();
}
}
window.addEventListener("scroll", handleScroll);
// Or auto-hide after a set time
setTimeout(hideBanner, 5000);
}
The Event Object
const form = document.querySelector("#searchForm");
form.addEventListener("submit", (event) => {
// Prevent the default behavior (page reload)
event.preventDefault();
// Event properties
console.log("Type:", event.type); // "submit"
console.log("Target:", event.target); // the form element
console.log("CurrentTarget:", event.currentTarget); // the element with the listener
// Form data
const formData = new FormData(event.target);
const query = formData.get("searchQuery");
console.log("Search:", query);
// Mouse position
console.log("Client:", event.clientX, event.clientY);
console.log("Page:", event.pageX, event.pageY);
// Key info (for keyboard events)
console.log("Key:", event.key);
console.log("Code:", event.code);
console.log("Alt:", event.altKey);
console.log("Ctrl:", event.ctrlKey);
console.log("Shift:", event.shiftKey);
// Stop propagation
event.stopPropagation();
});
Event Types
// Mouse events
document.querySelector("#btn").addEventListener("click", (e) => {
console.log("Click!", e.clientX, e.clientY);
});
document.querySelector("#btn").addEventListener("dblclick", () => {
console.log("Double click!");
});
document.querySelector("#card").addEventListener("mouseenter", () => {
console.log("Mouse enters card");
});
document.querySelector("#card").addEventListener("mouseleave", () => {
console.log("Mouse leaves card");
});
// Keyboard events
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
closeModal();
}
if (e.ctrlKey && e.key === "s") {
e.preventDefault();
saveDocument();
}
});
document.addEventListener("keyup", (e) => {
console.log("Key released:", e.key);
});
// Input events
const input = document.querySelector("#searchInput");
input.addEventListener("input", (e) => {
// Triggered on every character change
console.log("Current value:", e.target.value);
filterResults(e.target.value);
});
input.addEventListener("change", (e) => {
// Triggered on blur (the value is final)
console.log("Final value:", e.target.value);
});
// Focus events
input.addEventListener("focus", () => {
input.parentElement.classList.add("focused");
});
input.addEventListener("blur", () => {
input.parentElement.classList.remove("focused");
});
// Load events
window.addEventListener("load", () => {
console.log("All images are now included!");
});
document.addEventListener("DOMContentLoaded", () => {
console.log("DOM ready, image may not be loaded yet!");
});
The once Option
// An event handler that only runs once
const banner = document.querySelector("#infoBanner");
banner.addEventListener("click", () => {
console.log("Banner clicked!");
banner.style.display = "none";
}, { once: true });
// Automatically removed after being called once!
// Or with the one-time event pattern
function onFirstSubmit(callback) {
const form = document.querySelector("#registrationForm");
form.addEventListener("submit", (event) => {
callback(event);
}, { once: true });
}
onFirstSubmit((event) => {
console.log("Submit first!");
event.target.classList.add("submitted");
});
The passive Option
// passive: true → the handler will NOT call preventDefault()
// Improves scroll event performance
window.addEventListener("scroll", (e) => {
console.log("Scroll position:", window.scrollY);
}, { passive: true });
// Useful for touch events on mobile
document.addEventListener("touchstart", (e) => {
console.log("Touch detected!");
}, { passive: true });
// passive is the default for touchstart/touchmove in Chrome
// => more responsive scrolling on mobile
Event Delegation
// Instead of adding a listener to each child, add one to the parent
const list = document.querySelector("#studentList");
list.addEventListener("click", (event) => {
// Find the nearest relevant element
const item = event.target.closest(".student-item");
if (!item) return; // the click was not on an item
const action = event.target.dataset.action;
const studentId = item.dataset.studentId;
switch (action) {
case "edit":
editStudent(studentId);
break;
case "delete":
removeStudent(studentId);
break;
case "detail":
showDetails(studentId);
break;
default:
// Click on the item, not on an action button
item.classList.toggle("selected");
}
});
// Event delegation for dynamic content
// The listener keeps working for elements added later
Real-Time Form Validation
class FormValidator {
constructor(formSelector) {
this.form = document.querySelector(formSelector);
if (!this.form) return;
this.fields = {};
this.init();
}
init() {
// Set up real-time validation for every input
const inputs = this.form.querySelectorAll("input[required], select[required]");
inputs.forEach(input => {
this.fields[input.name] = {
element: input,
valid: false
};
// Validate while the user is typing
input.addEventListener("input", () => this.validateField(input));
// Validate when the user is done (blur)
input.addEventListener("blur", () => this.validateField(input, true));
});
// Validate on submit
this.form.addEventListener("submit", (e) => {
const allValid = this.validateAll();
if (!allValid) {
e.preventDefault();
this.showSubmitError();
}
});
}
validateField(input, showError = false) {
const value = input.value.trim();
const rules = this.getRules(input);
let errorMsg = "";
// Required check
if (rules.required && !value) {
errorMsg = "This field is required";
}
// Email check
else if (rules.email && value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
errorMsg = "Invalid email format";
}
}
// StudentID check
else if (rules.studentId && value) {
const studentIdRegex = /^\d{10}$/;
if (!studentIdRegex.test(value)) {
errorMsg = "StudentID must be 10 digits";
}
}
// Minimum length
else if (rules.minLength && value.length < rules.minLength) {
errorMsg = `At least ${rules.minLength} characters`;
}
// Update the UI
const errorEl = this.form.querySelector(`[data-error="${input.name}"]`);
const wrapper = input.closest(".form-group");
if (errorMsg && showError) {
input.classList.add("error");
input.classList.remove("valid");
if (errorEl) errorEl.textContent = errorMsg;
wrapper?.classList.add("has-error");
} else if (!errorMsg && value) {
input.classList.remove("error");
input.classList.add("valid");
if (errorEl) errorEl.textContent = "";
wrapper?.classList.remove("has-error");
} else {
input.classList.remove("error", "valid");
if (errorEl) errorEl.textContent = "";
wrapper?.classList.remove("has-error");
}
this.fields[input.name].valid = !errorMsg;
return !errorMsg;
}
getRules(input) {
return {
required: input.hasAttribute("required"),
email: input.type === "email",
studentId: input.name === "studentId",
minLength: input.minLength > 0 ? input.minLength : null
};
}
validateAll() {
let allValid = true;
for (const { element } of Object.values(this.fields)) {
const valid = this.validateField(element, true);
if (!valid) allValid = false;
}
return allValid;
}
showSubmitError() {
const firstError = this.form.querySelector(".error");
if (firstError) {
firstError.focus();
firstError.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
}
// Usage
document.addEventListener("DOMContentLoaded", () => {
new FormValidator("#registrationForm");
});
Custom Events
// Create your own event
const event = new CustomEvent("studentUpdated", {
detail: {
studentId: "2024001",
name: "Mike Johnson",
action: "updateGPA"
},
bubbles: true
});
// Dispatch it
document.dispatchEvent(event);
// Listen for it
document.addEventListener("studentUpdated", (e) => {
console.log("Data updated:", e.detail);
// { studentId: "2024001", name: "Mike Johnson", action: "updateGPA" }
});
Code Explanation
| Concept | Explanation |
|---|---|
addEventListener(type, handler, options) |
Register an event handler |
removeEventListener(type, handler) |
Remove an event handler |
event.preventDefault() |
Prevent the browser default (form submit, link navigation) |
event.stopPropagation() |
Stop the event from bubbling up to the parent |
once: true |
Automatically removed after being called once |
passive: true |
Will not call preventDefault → better performance |
DOMContentLoaded |
The DOM is ready (before images load) |
load |
All resources have finished loading |
input |
The value changes while the user is typing |
change |
The value changes when the user is done (blur) |
Event Flow
CAPTURING PHASE
(from window to the target)
window → document → html → body → form → input
TARGET PHASE
(on the target element)
input (event fires)
BUBBLING PHASE
(from the target to window)
input → form → body → html → document → window
Real-World Examples
A student registration form with real-time validation:
document.addEventListener("DOMContentLoaded", () => {
const form = document.querySelector("#registrationForm");
const nameInput = form.querySelector('input[name="name"]');
const studentIdInput = form.querySelector('input[name="studentId"]');
const emailInput = form.querySelector('input[name="email"]');
const btnSubmit = form.querySelector('button[type="submit"]');
// Status indicators
const statusName = document.querySelector("#statusName");
const statusStudentId = document.querySelector("#statusStudentId");
const statusEmail = document.querySelector("#statusEmail");
// Real-time validation
nameInput.addEventListener("input", (e) => {
const value = e.target.value.trim();
if (value.length < 3) {
showStatus(statusName, "at least 3 characters", "error");
nameInput.classList.add("error");
} else {
showStatus(statusName, "✓ valid", "success");
nameInput.classList.remove("error");
nameInput.classList.add("valid");
}
checkFormReady();
});
studentIdInput.addEventListener("input", (e) => {
const value = e.target.value.trim();
if (!/^\d{10}$/.test(value) && value.length > 0) {
showStatus(statusStudentId, "StudentID must be 10 digits", "error");
studentIdInput.classList.add("error");
} else if (value.length === 10) {
showStatus(statusStudentId, "✓ valid", "success");
studentIdInput.classList.remove("error");
studentIdInput.classList.add("valid");
} else {
showStatus(statusStudentId, "", "");
studentIdInput.classList.remove("error", "valid");
}
checkFormReady();
});
emailInput.addEventListener("input", (e) => {
const value = e.target.value.trim();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (value && !emailRegex.test(value)) {
showStatus(statusEmail, "invalid email format", "error");
emailInput.classList.add("error");
} else if (emailRegex.test(value)) {
showStatus(statusEmail, "✓ valid", "success");
emailInput.classList.remove("error");
emailInput.classList.add("valid");
} else {
showStatus(statusEmail, "", "");
emailInput.classList.remove("error", "valid");
}
checkFormReady();
});
function showStatus(el, msg, type) {
if (!el) return;
el.textContent = msg;
el.className = `status ${type}`;
}
function checkFormReady() {
const nameValid = nameInput.value.trim().length >= 3;
const studentIdValid = /^\d{10}$/.test(studentIdInput.value.trim());
const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailInput.value.trim());
btnSubmit.disabled = !(nameValid && studentIdValid && emailValid);
btnSubmit.classList.toggle("btn-disabled", btnSubmit.disabled);
}
// Submit handler
form.addEventListener("submit", (e) => {
e.preventDefault();
const data = {
name: nameInput.value.trim(),
studentId: studentIdInput.value.trim(),
email: emailInput.value.trim()
};
// Simulate an API call
btnSubmit.disabled = true;
btnSubmit.textContent = "Mengirim...";
setTimeout(() => {
console.log("Data tersend:", data);
alert(`Registration successful!\nName: ${data.name}\nStudentID: ${data.studentId}`);
form.reset();
[statusName, statusStudentId, statusEmail].forEach(el => {
if (el) el.textContent = "";
});
btnSubmit.textContent = "Register";
btnSubmit.disabled = true;
}, 1500);
});
});
Conclusion
addEventListeneris the main way to handle events — avoid inlineonclick.event.preventDefault()prevents the browser default (form submit, link navigation).- Event delegation (a listener on the parent) is more efficient for dynamic lists than per-item listeners.
once: truefor events that only need to fire once — closing a banner, first submit.passive: truefor scroll/touch events — improves mobile performance.- Real-time form validation uses the
inputevent (while typing) andblur(when done).