Last active
September 30, 2025 10:30
-
-
Save Kcko/17685dff01ff40b04b7bbc3f05c8a10e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://medium.com/@ThinkingLoop/12-dom-tricks-no-libraries-required-61268e398c50 | |
// 1) Query once, act many | |
const $ = (sel, root = document) => root.querySelector(sel); | |
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | |
const app = $('#app'); | |
const buttons = $$('.action', app); | |
buttons.forEach(btn => btn.addEventListener('click', onAction)); | |
// 2) Event delegation (one listener, infinite items) | |
$('#list').addEventListener('click', e => { | |
const btn = e.target.closest('[data-action="remove"]'); | |
if (!btn) return; | |
e.preventDefault(); | |
btn.closest('li')?.remove(); | |
}); | |
// when we dont want use e.preventDeafult() -> pasive: true, better optimalization | |
window.addEventListener('scroll', onScroll, { passive: true }); | |
// 3) Build with DocumentFragment (no layout thrash) | |
const frag = document.createDocumentFragment(); | |
for (const item of data) { | |
const li = document.createElement('li'); | |
li.textContent = item.name; | |
frag.appendChild(li); | |
} | |
$('#list').appendChild(frag); // one commit | |
// 4) Render HTML safely with <template> | |
<template id="row-tpl"> | |
<li class="row"> | |
<span class="name"></span> | |
<button data-action="remove">×</button> | |
</li> | |
</template> | |
function renderRow(user) { | |
const tpl = $('#row-tpl').content.cloneNode(true); | |
tpl.querySelector('.name').textContent = user.name; | |
return tpl; | |
} | |
const frag = document.createDocumentFragment(); | |
users.forEach(u => frag.appendChild(renderRow(u))); | |
$('#list').appendChild(frag); | |
// 5) Class toggles beat inline styles | |
.card { transition: transform .2s ease; } | |
.card.is-active { transform: scale(1.02); } | |
$('.card').classList.toggle('is-active', shouldHighlight); | |
// 6) dataset for behavior, not style | |
<button data-action="copy" data-copy-text="Hello!">Copy</button> | |
document.addEventListener('click', e => { | |
const btn = e.target.closest('[data-action="copy"]'); | |
if (!btn) return; | |
navigator.clipboard.writeText(btn.dataset.copyText); | |
}); | |
// 7) Observe, don’t poll (Intersection/Resize/Mutation) | |
// IntersectionObserver: lazy-load images | |
const io = new IntersectionObserver(entries => { | |
entries.forEach(({ isIntersecting, target }) => { | |
if (!isIntersecting) return; | |
target.src = target.dataset.src; | |
io.unobserve(target); | |
}); | |
}, { rootMargin: '200px' }); | |
$$('img[data-src]').forEach(img => io.observe(img)); | |
// ResizeObserver: reflow grids on size change | |
const ro = new ResizeObserver(entries => { | |
for (const { target } of entries) layout(target); | |
}); | |
ro.observe($('#grid')); | |
// 8) Batch mutations with requestAnimationFrame | |
function highlightRow(row) { | |
// READ | |
const { top } = row.getBoundingClientRect(); | |
// Schedule WRITE | |
requestAnimationFrame(() => { | |
row.style.setProperty('--start', `${top}px`); | |
row.classList.add('highlight'); | |
}); | |
} | |
// 9) Virtualize long lists with windowing | |
const viewport = $('#viewport'); | |
const rowHeight = 32; | |
let start = 0, end = 0; | |
viewport.addEventListener('scroll', render); | |
window.addEventListener('resize', render); | |
function render() { | |
const { scrollTop, clientHeight } = viewport; | |
start = Math.floor(scrollTop / rowHeight); | |
end = start + Math.ceil(clientHeight / rowHeight) + 5; | |
const frag = document.createDocumentFragment(); | |
viewport.innerHTML = ''; // clear (or recycle nodes for extra perf) | |
for (let i = start; i < Math.min(end, data.length); i++) { | |
const div = document.createElement('div'); | |
div.className = 'row'; | |
div.style.top = `${i * rowHeight}px`; | |
div.textContent = data[i].name; | |
frag.appendChild(div); | |
} | |
viewport.appendChild(frag); | |
} | |
#viewport { position: relative; overflow: auto; height: 400px; } | |
.row { position: absolute; height: 32px; line-height: 32px; left: 0; right: 0; } | |
// 10) Custom events for clean boundaries | |
/ producer | |
function notifySaved(node, payload) { | |
node.dispatchEvent(new CustomEvent('saved', { detail: payload, bubbles: true })); | |
} | |
// consumer | |
document.addEventListener('saved', e => { | |
const { id } = e.detail; | |
toast(`Saved #${id}`); | |
}); | |
// 11) Progressive enhancement with feature checks | |
const supportsClipboard = !!navigator.clipboard?.writeText; | |
if (supportsClipboard) { | |
document.body.classList.add('has-clipboard'); | |
// enable fancy copy button | |
} else { | |
// use <input> select() + execCommand fallback or plain text | |
} | |
// 12) Micro-router: data-route + history.pushState | |
<nav> | |
<a href="/settings" data-route>Settings</a> | |
<a href="/reports" data-route>Reports</a> | |
</nav> | |
<main id="view"></main> | |
const routes = { | |
'/settings': () => $('#view').textContent = 'Settings ⚙️', | |
'/reports': () => $('#view').textContent = 'Reports 📈', | |
}; | |
function go(path) { | |
history.pushState({}, '', path); | |
routes[path]?.(); | |
} | |
document.addEventListener('click', e => { | |
const link = e.target.closest('a[data-route]'); | |
if (!link) return; | |
e.preventDefault(); | |
go(new URL(link.href).pathname); | |
}); | |
window.addEventListener('popstate', () => routes[location.pathname]?.()); | |
routes[location.pathname]?.(); // initial |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment