Module: JavaScript
JavaScript·076·4 MIN READ

076: Browser APIs Beyond the DOM: URL, History, Observers, and Web Components

TOPICS COVERED: Browser APIs Beyond the DOM: URL, History, Observers, and Web Components

Outcomes

By the end of this lesson, you can:

  • read and construct URLs safely;
  • use URLSearchParams;
  • understand location and the History API;
  • observe DOM mutations and element visibility;
  • explain when observers are better than polling;
  • understand the purpose of Web Components and Custom Elements;
  • identify browser APIs as host APIs rather than core ECMAScript.

URL and URLSearchParams

Avoid manual query-string concatenation when the platform has a parser.

js
const url = new URL(
  "https://example.com/products?page=2&sort=price"
);

console.log(url.pathname);
console.log(url.searchParams.get("page"));

Modify:

js
url.searchParams.set("page", "3");
url.searchParams.set("category", "drinks");

console.log(url.toString());

Current-page query parameters:

js
const params = new URLSearchParams(window.location.search);

const page = Number(params.get("page") ?? 1);

location

js
console.log(window.location.href);
console.log(window.location.pathname);
console.log(window.location.origin);

Navigation:

js
window.location.assign("/orders");

Replacing current history entry:

js
window.location.replace("/login");

Do not redirect to untrusted arbitrary URLs without validation. Open redirects are a real security problem.

History API

Single-page interfaces can update the browser URL without a full reload.

js
history.pushState(
  { filter: "open" },
  "",
  "?filter=open"
);

Respond to back/forward navigation:

js
window.addEventListener("popstate", (event) => {
  console.log(event.state);
});

A real router must coordinate URL state, rendering, scroll behavior, accessibility, and server fallback behavior. Do not reinvent a full router casually.

MutationObserver

Observe DOM changes without repeatedly polling.

js
const observer = new MutationObserver((records) => {
  for (const record of records) {
    console.log(record.type);
  }
});

observer.observe(document.querySelector("#orders"), {
  childList: true,
  subtree: true,
});

Disconnect when no longer needed:

js
observer.disconnect();

MutationObserver is not a substitute for proper application state management. Use it when you truly need to observe DOM changes outside your direct control.

IntersectionObserver

Detect when an element enters or exits a viewport/root intersection.

js
const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      console.log("visible", entry.target);
    }
  }
});

document
  .querySelectorAll("[data-lazy-section]")
  .forEach((element) => observer.observe(element));

Common uses:

  • lazy-loading noncritical content;
  • infinite-scroll sentinels;
  • analytics visibility;
  • activating sections.

This is generally better than running expensive scroll calculations on every scroll event.

ResizeObserver

Component size can change without viewport size changing.

js
const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    console.log(entry.contentRect.width);
  }
});

observer.observe(document.querySelector(".panel"));

Use CSS container queries for pure styling decisions. Use ResizeObserver when JavaScript behavior genuinely depends on element dimensions.

Web Components

Web Components are a group of platform capabilities for reusable custom UI elements.

Custom Elements

js
class UserBadge extends HTMLElement {
  connectedCallback() {
    const name = this.getAttribute("name") ?? "Guest";

    this.textContent = `User: ${name}`;
  }
}

customElements.define("user-badge", UserBadge);

HTML:

html
<user-badge name="Maya"></user-badge>

Custom element names must contain a hyphen.

Shadow DOM

Shadow DOM can encapsulate internal DOM and styles.

js
class StatusBadge extends HTMLElement {
  constructor() {
    super();

    const root = this.attachShadow({ mode: "open" });

    root.innerHTML = `
      <style>
        :host {
          display: inline-block;
        }
      </style>

      <span part="label"></span>
    `;
  }

  connectedCallback() {
    this.shadowRoot.querySelector("[part='label']").textContent =
      this.getAttribute("status") ?? "Unknown";
  }
}

customElements.define("status-badge", StatusBadge);

Shadow DOM changes styling and event-boundary behavior. Learn it deliberately before using it as a blanket component strategy.

Templates

html
<template id="product-card-template">
  <article class="product-card">
    <h2></h2>
  </article>
</template>
js
const template = document.querySelector(
  "#product-card-template"
);

const clone = template.content.cloneNode(true);
clone.querySelector("h2").textContent = "Tea";

document.body.append(clone);

Worked Example: URL-Driven Filter

js
function readFilters() {
  const params = new URLSearchParams(location.search);

  return {
    query: params.get("q") ?? "",
    page: Number(params.get("page") ?? 1),
  };
}

function writeFilters(filters) {
  const url = new URL(location.href);

  url.searchParams.set("q", filters.query);
  url.searchParams.set("page", String(filters.page));

  history.pushState(filters, "", url);
}

window.addEventListener("popstate", () => {
  render(readFilters());
});

Now the browser back button participates in UI state.

Advanced Notes: Observer Choice and Custom-Element Lifecycle

Choose the observer that matches the signal:

NeedPrefer
DOM tree/attribute changesMutationObserver
visibility/intersectionIntersectionObserver
element box-size changesResizeObserver
viewport media conditionsCSS media queries / matchMedia
component style response to sizeCSS container queries where possible

Polling with setInterval() is usually the wrong first tool for these problems.

Custom-element lifecycle callbacks

js
class LiveClock extends HTMLElement {
  #timerId;

  connectedCallback() {
    this.#timerId = setInterval(() => {
      this.textContent = new Date().toLocaleTimeString();
    }, 1000);
  }

  disconnectedCallback() {
    clearInterval(this.#timerId);
  }
}

customElements.define("live-clock", LiveClock);

The lifecycle makes cleanup explicit.

Observed attributes

js
class StatusBadge extends HTMLElement {
  static observedAttributes = ["status"];

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === "status" && oldValue !== newValue) {
      this.render();
    }
  }

  render() {
    this.textContent = this.getAttribute("status") ?? "Unknown";
  }
}

Do not build a framework inside a custom element unless the complexity justifies it. Native components are most useful when their lifecycle, encapsulation, and interoperability solve a concrete problem.

Mistakes and Debugging

  • concatenating query strings manually;
  • not encoding user-controlled URL values;
  • using History API without handling back/forward navigation;
  • leaving observers connected forever;
  • using MutationObserver to compensate for unclear state ownership;
  • using scroll events for work IntersectionObserver already solves;
  • building a custom element that is inaccessible without keyboard/name/state support.

Best Practices

  • Prefer platform parsers for URLs.
  • Validate navigation destinations.
  • Disconnect observers.
  • Use observers for observation, not as a replacement for application architecture.
  • Prefer CSS for styling behavior and JS observers for behavioral needs.
  • Treat Web Components as a real component model with lifecycle and accessibility responsibilities.

Exercises

Core

Read q and page from a URL.

Practice

Create an IntersectionObserver that logs when cards become visible.

Professional Extension

Build a custom element that renders an accessible status label and supports an observed status attribute.

Recap

Modern browser JavaScript is larger than the DOM alone. URL, history, observer, and component APIs help build applications that integrate correctly with the browser rather than fighting it.