4 min read
DOM & Events
The DOM
The Document Object Model is a tree of nodes representing the HTML document. JavaScript interacts with it via the document API.
document
└── <html>
├── <head>
│ └── <title>
└── <body>
├── <header>
└── <main>
└── <p id="intro">Hello</p>Selecting Elements
js// Single element
document.getElementById('intro') // fastest, by id
document.querySelector('.card') // first match, CSS selector
document.querySelector('[data-id="3"]') // attribute selector
// Collections (live HTMLCollection vs static NodeList)
document.getElementsByClassName('card') // live HTMLCollection
document.getElementsByTagName('p') // live HTMLCollection
document.querySelectorAll('.card') // static NodeList
// Relative traversal
el.parentElement
el.children // live HTMLCollection of element children
el.firstElementChild
el.lastElementChild
el.nextElementSibling
el.previousElementSibling
el.closest('.container') // walk up until selector matchesManipulating Elements
js// Reading/writing content
el.textContent = 'Hello' // text only, safe
el.innerHTML = '<b>Bold</b>' // parses HTML — XSS risk if user-controlled
el.outerHTML // includes element itself
// Attributes
el.getAttribute('href')
el.setAttribute('aria-expanded', 'true')
el.removeAttribute('disabled')
el.hasAttribute('required')
el.dataset.userId // reads data-user-id attribute
// Classes
el.classList.add('active')
el.classList.remove('active')
el.classList.toggle('open')
el.classList.contains('visible')
el.classList.replace('old', 'new')
// Styles (prefer classes over inline styles)
el.style.color = '#4f46e5'
el.style.setProperty('--custom-prop', 'value')
getComputedStyle(el).getPropertyValue('color') // resolved final valueCreating & Inserting Elements
js// Create
const div = document.createElement('div')
div.className = 'card'
div.textContent = 'Hello'
// Insert
parent.appendChild(div)
parent.prepend(div) // insert at start
parent.insertBefore(div, refNode)
refNode.after(div) // modern — inserts after refNode
refNode.before(div)
// Template literals → fragment (avoids repeated reflows)
function createCard(title, body) {
const tpl = document.createElement('template')
tpl.innerHTML = `<div class="card"><h2>${title}</h2><p>${body}</p></div>`
return tpl.content.cloneNode(true)
}
document.querySelector('.grid').append(createCard('Hello', 'World'))
// Remove
el.remove()
parent.removeChild(child)
// Clone
const copy = el.cloneNode(true) // true = deep clone (includes children)Events
addEventListener
js// Preferred over el.onclick = ... (multiple listeners, removeEventListener)
el.addEventListener('click', handler, options)
// Options
el.addEventListener('click', handler, {
once: true, // auto-remove after first call
passive: true, // signal no preventDefault — improves scroll perf
capture: true, // fire during capture phase instead of bubble
})
el.removeEventListener('click', handler) // same function reference requiredEvent Phases
Document
│ (capture phase — top down)
▼
<div>
│
▼
<button> ← target phase
│
▲ (bubble phase — bottom up)
│
<div>jsel.addEventListener('click', handler) // bubble (default)
el.addEventListener('click', handler, true) // capture
// Stop propagation
e.stopPropagation() // don't bubble/capture further
e.stopImmediatePropagation() // also skip other listeners on same element
// Prevent default browser behavior (form submit, link navigation, etc.)
e.preventDefault()Event Delegation
Attach one listener to a parent instead of many to children — efficient for dynamic lists.
js// ❌ One listener per item — expensive for large lists
items.forEach(item => item.addEventListener('click', handler))
// ✅ One listener on parent
document.querySelector('.list').addEventListener('click', e => {
const item = e.target.closest('.item')
if (!item) return // click was on whitespace
console.log(item.dataset.id)
})Common Events
js// Mouse
click, dblclick, mouseenter, mouseleave, mousemove, mousedown, mouseup
// Keyboard
keydown, keyup, keypress (deprecated)
e.key // 'Enter', 'ArrowUp', 'a'
e.code // 'KeyA' — physical key, layout-independent
e.metaKey, e.ctrlKey, e.shiftKey, e.altKey
// Form
submit, change, input, focus, blur, focusin, focusout
e.target.value
// Window / Document
DOMContentLoaded // DOM ready, before images/stylesheets
load // everything loaded
resize, scroll
visibilitychange // tab hidden/shown — document.visibilityState
// Pointer (replaces mouse + touch)
pointerdown, pointerup, pointermove, pointercancel
e.pointerType // 'mouse' | 'touch' | 'pen'Custom Events
js// Dispatch
const event = new CustomEvent('cart:updated', {
detail: { itemCount: 3 },
bubbles: true,
cancelable: true,
})
document.dispatchEvent(event)
// Listen
document.addEventListener('cart:updated', e => {
console.log(e.detail.itemCount)
})Performance Tips
js// Batch DOM reads before writes — avoid layout thrashing
// ❌ Forces multiple reflows
items.forEach(item => {
const h = item.offsetHeight // read — forces reflow
item.style.height = h * 2 + 'px' // write — invalidates layout
})
// ✅ Read all, then write all
const heights = items.map(item => item.offsetHeight) // all reads
items.forEach((item, i) => item.style.height = heights[i] * 2 + 'px') // all writes
// Debounce resize/scroll handlers
function debounce(fn, delay) {
let timer
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn(...args), delay)
}
}
window.addEventListener('resize', debounce(onResize, 150), { passive: true })
// IntersectionObserver — lazy-load or animate on enter
const observer = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) {
e.target.classList.add('visible')
observer.unobserve(e.target)
}
})
}, { threshold: 0.1 })
document.querySelectorAll('.animate').forEach(el => observer.observe(el))MutationObserver
Watch for DOM changes without polling.
jsconst observer = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.type === 'childList') {
console.log('children changed', m.addedNodes, m.removedNodes)
}
if (m.type === 'attributes') {
console.log(`${m.attributeName} changed`)
}
}
})
observer.observe(targetNode, {
childList: true, // watch children add/remove
attributes: true, // watch attribute changes
subtree: true, // watch all descendants
attributeFilter: ['class', 'aria-expanded'], // only these attributes
})
observer.disconnect()[prev·next]