Skillversitas

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

  • addEventListener is the main way to handle events — avoid inline onclick.
  • 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: true for events that only need to fire once — closing a banner, first submit.
  • passive: true for scroll/touch events — improves mobile performance.
  • Real-time form validation uses the input event (while typing) and blur (when done).