Learn › JavaScript ES6+

DOM & Events

Learn to interact with the browser DOM, handle events with delegation, and make HTTP requests with the Fetch API.

querySelector and DOM Manipulation

The querySelector and querySelectorAll methods allow you to select DOM elements using CSS selector syntax, replacing the older getElementById and getElementsByClassName methods. querySelector returns the first matching element, while querySelectorAll returns a static NodeList of all matches. Once you have a reference to an element, you can modify its content, attributes, styles, and class list. The DOM API provides methods for creating, appending, removing, and cloning elements to build dynamic interfaces.

// Selecting elements
const header = document.querySelector("h1");
const buttons = document.querySelectorAll(".btn");
const nav = document.querySelector("#main-nav");

// Modifying content
header.textContent = "Welcome"; // Text only
header.innerHTML = "<span>Welcome</span>"; // HTML

// Modifying attributes
const link = document.querySelector("a");
link.setAttribute("href", "https://example.com");
link.dataset.id = "42"; // data-id="42"
console.log(link.getAttribute("href"));

// Modifying classes
const card = document.querySelector(".card");
card.classList.add("active");
card.classList.remove("hidden");
card.classList.toggle("expanded");
console.log(card.classList.contains("active")); // true

// Creating and appending elements
const list = document.querySelector("#todo-list");
const item = document.createElement("li");
item.textContent = "New task";
item.classList.add("todo-item");
list.appendChild(item);

// Remove elements
const old = document.querySelector(".deprecated");
old?.remove();

// Modify styles
const box = document.querySelector(".box");
box.style.backgroundColor = "#3498db";
box.style.padding = "20px";
box.style.borderRadius = "8px";

Event Listeners

The addEventListener method attaches event handlers to DOM elements, supporting multiple handlers for the same event type. Events propagate through the DOM in two phases: capturing (top-down) and bubbling (bottom-up), with bubbling being the default. The event object passed to handlers contains information about the event, including the target element, event type, and methods to control propagation. The removeEventListener method detaches handlers, but requires a reference to the same function that was attached.

// Basic event listener
const button = document.querySelector("#submit-btn");

button.addEventListener("click", (event) => {
  console.log("Clicked!", event.target);
  console.log("Button text:", event.target.textContent);
});

// Keyboard events
const input = document.querySelector("#search");

input.addEventListener("keydown", (event) => {
  if (event.key === "Enter") {
    console.log("Search for:", event.target.value);
  }
  if (event.key === "Escape") {
    event.target.value = "";
  }
});

// Input event for real-time updates
input.addEventListener("input", (event) => {
  console.log("Current value:", event.target.value);
});

// Prevent default behavior
const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
  event.preventDefault();
  const formData = new FormData(form);
  console.log("Name:", formData.get("name"));
  console.log("Email:", formData.get("email"));
});

// Remove listener
function handleClick(e) {
  console.log("Clicked once");
  button.removeEventListener("click", handleClick);
}
button.addEventListener("click", handleClick);

// Or use the once option
button.addEventListener("click", () => console.log("Once!"), { once: true });

Event Delegation

Event delegation is a pattern where a single event listener on a parent element handles events for all its children, leveraging event bubbling. This approach is more efficient than attaching individual listeners to many elements, especially for dynamic content where elements are added or removed after the initial page load. The event.target property identifies which child element triggered the event, and you can use closest() to find the nearest ancestor matching a selector. Delegation is essential for managing events in modern single-page applications.

// Instead of adding listeners to each item...
const todoList = document.querySelector("#todo-list");

// ...add one listener to the parent
todoList.addEventListener("click", (event) => {
  const item = event.target.closest(".todo-item");
  if (!item) return;

  // Handle different actions based on target
  if (event.target.matches(".delete-btn")) {
    item.remove();
    console.log("Deleted:", item.dataset.id);
  } else if (event.target.matches(".toggle-btn")) {
    item.classList.toggle("completed");
    console.log("Toggled:", item.dataset.id);
  }
});

// Dynamic list with delegation
function createTodoApp(containerSelector) {
  const container = document.querySelector(containerSelector);

  container.innerHTML = `
    <input type="text" class="new-todo" placeholder="Add a todo...">
    <ul class="todo-list"></ul>
  `;

  const input = container.querySelector(".new-todo");
  const list = container.querySelector(".todo-list");
  let nextId = 1;

  input.addEventListener("keydown", (e) => {
    if (e.key === "Enter" && e.target.value.trim()) {
      const li = document.createElement("li");
      li.className = "todo-item";
      li.dataset.id = nextId++;
      li.innerHTML = `
        <span>${e.target.value}</span>
        <button class="delete-btn">Delete</button>
      `;
      list.appendChild(li);
      e.target.value = "";
    }
  });

  // Works for all items, even those added later
  list.addEventListener("click", (e) => {
    if (e.target.matches(".delete-btn")) {
      e.target.closest(".todo-item").remove();
    }
  });
}

Fetch API

The Fetch API provides a modern, Promise-based interface for making HTTP requests, replacing the older XMLHttpRequest. The fetch function returns a Promise that resolves to a Response object, which has methods for parsing the body as JSON, text, Blob, or FormData. Note that fetch only rejects on network errors — HTTP error responses like 404 or 500 still resolve normally, so you must check the response.ok property or status code. The Fetch API supports all HTTP methods, custom headers, request body, and various other options through the init object.

// Basic GET request
async function getUsers() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users");

  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }

  const users = await response.json();
  return users;
}

// POST request with JSON body
async function createPost(title, body, userId) {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ title, body, userId }),
  });

  if (!response.ok) {
    throw new Error(`Failed to create post: ${response.status}`);
  }

  return response.json();
}

// Reusable API client
class ApiClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  async request(path, options = {}) {
    const url = `${this.baseUrl}${path}`;
    const config = {
      headers: { "Content-Type": "application/json", ...options.headers },
      ...options,
    };

    const response = await fetch(url, config);

    if (!response.ok) {
      const error = await response.json().catch(() => ({}));
      throw new Error(error.message || `HTTP ${response.status}`);
    }

    return response.json();
  }

  get(path) {
    return this.request(path);
  }

  post(path, data) {
    return this.request(path, { method: "POST", body: JSON.stringify(data) });
  }

  put(path, data) {
    return this.request(path, { method: "PUT", body: JSON.stringify(data) });
  }

  delete(path) {
    return this.request(path, { method: "DELETE" });
  }
}

// Usage
const api = new ApiClient("https://jsonplaceholder.typicode.com");

async function main() {
  const users = await api.get("/users");
  console.log(`Found ${users.length} users`);

  const newPost = await api.post("/posts", {
    title: "Hello",
    body: "World",
    userId: 1,
  });
  console.log("Created post:", newPost.id);
}

main().catch(console.error);

← Modules & Classes