Skip to main content

What is the DOM?

The DOM (Document Object Model) is the browser’s representation of your HTML as JavaScript objects. Every HTML tag is a “node” that you can manipulate: change text, styles, add events, etc.
<div id="app">
  <h1>Hello</h1>
  <button id="btn">Click me</button>
</div>
Becomes:
Document
  └─ div#app
      ├─ h1 ("Hello")
      └─ button#btn ("Click me")

Selecting Elements

querySelector

Finds one element using CSS selectors:
// By ID
const button = document.querySelector("#my-button");

// By class
const card = document.querySelector(".product-card");

// By tag
const heading = document.querySelector("h1");

// Complex selectors
const firstButton = document.querySelector(".card button");

getElementById

Finds an element by ID (faster than querySelector for IDs):
const button = document.getElementById("my-button");

querySelectorAll

Finds all matching elements:
const allButtons = document.querySelectorAll("button");
// Returns a NodeList (array-like)

The Type Problem

querySelector returns a very generic type:
const element = document.querySelector("#my-input");
// Type: Element | null
// Problem: No access to .value, .checked, etc.

Solution: Generic Helper Function

From our project, we created a reusable helper:
main.ts
// T extends HTMLElement → T must be HTMLElement or more specific
function getElement<T extends HTMLElement>(selector: string): T {
  // document.querySelector<T>(selector) → Search and type as T
  const element = document.querySelector<T>(selector);
  
  // If element not found, throw error
  // This prevents code from continuing with null
  if (!element) {
    throw new Error(`Element not found: ${selector}`);
  }
  
  // Return the element already typed as T
  return element;
}

Using the Generic Helper

// Get a button - has .disabled, .click(), etc.
const btn = getElement<HTMLButtonElement>("#my-btn");
btn.disabled = true; // ✅ TypeScript knows this exists

// Get an input - has .value, .checked, etc.
const input = getElement<HTMLInputElement>("#my-input");
const value = input.value; // ✅ TypeScript knows this exists

// Get a div - general container element
const grid = getElement<HTMLDivElement>("#products-grid");
grid.innerHTML = "<p>Hello</p>"; // ✅ Works

Common HTML Element Types

ElementTypeScript Type
<button>HTMLButtonElement
<input>HTMLInputElement
<div>HTMLDivElement
<a>HTMLAnchorElement
<img>HTMLImageElement
<form>HTMLFormElement
<select>HTMLSelectElement
Any elementHTMLElement

Generics Explained: <T>

function getElement<T extends HTMLElement>(selector: string): T
──────────────────────────────────────────────────────────────
                │                      │               │
                │                      │               │
    <T extends HTMLElement>            │               │
    "T is a type that MUST be          │               │
     HTMLElement or something          │               │
     more specific"                    │               │
                                 (selector: string)     │
                                 "Receives a CSS
                                  selector string"      │
                                                    : T
                                               "Returns
                                                type T" │

Changing Element Content

textContent

Set plain text (safer - escapes HTML):
const heading = getElement<HTMLHeadingElement>("h1");
heading.textContent = "New Title";

// Safe - won't execute scripts
heading.textContent = "<script>alert('xss')</script>";
// Shows literally: <script>alert('xss')</script>

innerHTML

Set HTML content:
main.ts
const grid = getElement<HTMLDivElement>("#products-grid");
grid.innerHTML = `
  <article class="card">
    <h2>Product</h2>
  </article>
`;
Warning: Only use innerHTML with trusted content! Never with user input.

Changing Element Attributes

Direct Property Access

const button = getElement<HTMLButtonElement>("#btn");
button.disabled = true;
button.textContent = "Loading...";

const link = getElement<HTMLAnchorElement>("#link");
link.href = "https://example.com";

setAttribute

const element = getElement<HTMLElement>("#my-element");
element.setAttribute("data-count", "5");

Data Attributes (dataset)

<article data-product-id="123">
main.ts
const card = element.closest('.product-card') as HTMLElement;
const productId = card.dataset.productId; // "123"

Hiding/Showing Elements

main.ts
const loading = getElement<HTMLDivElement>("#products-loading");
const error = getElement<HTMLDivElement>("#products-error");

// Hide elements
loading.hidden = true;
error.hidden = true;

// Show loading
loading.hidden = false;

Event Listeners

Basic Click Event

main.ts
function setupEventListeners(): void {
  const loadBtn = getElement<HTMLButtonElement>("#load-products-btn");
  
  // When clicked, run loadProducts
  loadBtn.addEventListener('click', loadProducts);
}

Event with Type

button.addEventListener('click', (event: MouseEvent) => {
  console.log('Clicked at:', event.clientX, event.clientY);
});

Common Events

EventTrigger
clickElement clicked
submitForm submitted
inputInput value changed
changeSelect/checkbox changed
keydownKey pressed
mouseoverMouse enters element
focusElement receives focus

Event Delegation

Instead of adding listeners to many elements, add ONE to the parent:
main.ts
function setupAddToCartButtons(): void {
  // Get parent container
  const grid = getElement<HTMLDivElement>("#products-grid");
  
  // ONE listener on the container
  grid.addEventListener('click', (event: MouseEvent) => {
    // event.target is the exact element clicked
    const target = event.target as HTMLElement;
    
    // .closest() searches up the DOM tree
    // Finds element with data-action="add-to-cart"
    const button = target.closest('[data-action="add-to-cart"]');
    
    // If we didn't find the button, click was elsewhere; exit
    if (!button) return;
    
    // Find parent product card to get ID
    const card = button.closest('.product-card') as HTMLElement;
    if (!card) return;
    
    // dataset.productId accesses data-product-id from HTML
    const productId = card.dataset.productId;
    if (productId) {
      addToCart(parseInt(productId, 10));
    }
  });
}

Why Event Delegation?

<div id="grid">                    ← ONE listener here
  <article>
    <button>Add</button>           ← Click here
  </article>
  <article>
    <button>Add</button>           ← Or here
  </article>
  <!-- 100 more products... -->
</div>
Advantages:
  • One listener instead of 100
  • Works with dynamically added elements
  • Better performance

Creating Elements Dynamically

main.ts
function showNotification(message: string, duration: number = 3000): void {
  // Create new div
  const notification = document.createElement('div');
  
  // Apply styles
  notification.style.cssText = `
    position: fixed;
    bottom: 20px;
    right: 20px;
    padding: 16px 24px;
    background-color: #333;
    color: white;
    border-radius: 8px;
  `;
  
  // Set content
  notification.textContent = message;
  
  // Add to page
  document.body.appendChild(notification);
  
  // Remove after duration
  setTimeout(() => {
    notification.remove();
  }, duration);
}

Template Literals for HTML

Create HTML strings with variables:
main.ts
function createProductCardHTML(product: Product): string {
  const formattedPrice = product.price.toLocaleString('es-MX', {
    style: 'currency',
    currency: 'MXN'
  });
  
  const imageUrl = product.images[0] || 'https://placehold.co/400x300?text=No+image';
  const cleanImageUrl = imageUrl.replace(/["\[\]]/g, '');
  
  return `
    <article class="product-card" data-product-id="${product.id}">
      <figure class="product-card__figure">
        <img 
          src="${cleanImageUrl}" 
          alt="${product.title}"
          class="product-card__image"
          loading="lazy"
        />
      </figure>
      <div class="product-card__content">
        <span class="product-card__category">${product.category.name}</span>
        <h3 class="product-card__title">${product.title}</h3>
        <p class="product-card__price">
          ${formattedPrice}
        </p>
        <button type="button" class="product-card__btn" data-action="add-to-cart">
          Add to Cart
        </button>
      </div>
    </article>
  `;
}

Rendering Arrays with map() and join()

main.ts
function renderProducts(): void {
  const grid = getElement<HTMLDivElement>("#products-grid");
  
  if (appState.products.length === 0) {
    grid.innerHTML = `<p class="products__empty-state">No products found</p>`;
    return;
  }
  
  // 1. .map() → Convert each Product to HTML string
  //    Result: ["<article>...</article>", "<article>...</article>", ...]
  // 2. .join('') → Join all strings into one (no separator)
  //    Result: "<article>...</article><article>...</article>..."
  grid.innerHTML = appState.products
    .map(product => createProductCardHTML(product))
    .join('');
  
  // After inserting HTML, set up event listeners
  setupAddToCartButtons();
}

DOM Manipulation Flow

  1. Select element
  2. Check if exists (or use our helper that throws)
  3. Modify content/attributes/styles
  4. Listen to events if needed
// 1. Select
const button = getElement<HTMLButtonElement>("#btn");

// 2. Modify
button.textContent = "Click me!";
button.disabled = false;

// 3. Listen
button.addEventListener('click', () => {
  console.log('Clicked!');
});

Next Steps

Build docs developers (and LLMs) love