DOM Manipulation Patterns
Learn professional patterns for efficient, maintainable DOM manipulation.
Event Delegation Deep Dive
Event delegation leverages event bubbling to handle events on a parent element:
javascript1// Anti-pattern: Direct binding to each element2function bindDeleteButtons() {3 document.querySelectorAll('.delete-btn').forEach(btn => {4 btn.addEventListener('click', handleDelete);5 });6}78// Problem: New buttons added dynamically won't have handlers9// Problem: Memory overhead with many elements
javascript1// Pattern: Event delegation2class TodoList {3 constructor(container) {4 this.container = container;5 this.bindEvents();6 }78 bindEvents() {9 // Single listener handles all interactions10 this.container.addEventListener('click', (e) => {11 const target = e.target;1213 // Handle different actions14 if (target.matches('.delete-btn')) {15 this.handleDelete(target.closest('.todo-item'));16 } else if (target.matches('.edit-btn')) {17 this.handleEdit(target.closest('.todo-item'));18 } else if (target.matches('.toggle-btn')) {19 this.handleToggle(target.closest('.todo-item'));20 }21 });22 }2324 handleDelete(item) {25 const id = item.dataset.id;26 item.remove();27 this.onDelete?.(id);28 }2930 handleEdit(item) {31 const id = item.dataset.id;32 this.onEdit?.(id);33 }3435 handleToggle(item) {36 item.classList.toggle('completed');37 }38}
Using closest() for Nested Elements
javascript1// HTML structure2// <li class="item" data-id="1">3// <span class="text">Item text</span>4// <button class="delete">5// <svg>...</svg> <!-- User might click the icon -->6// </button>7// </li>89list.addEventListener('click', (e) => {10 // e.target could be the SVG inside the button11 const deleteBtn = e.target.closest('.delete');12 if (deleteBtn) {13 const item = deleteBtn.closest('.item');14 item.remove();15 }16});
Efficient DOM Updates
Batch Operations with DocumentFragment
javascript1// Inefficient: Multiple reflows2function renderItems(items) {3 const list = document.querySelector('.list');4 list.innerHTML = ''; // Clear56 items.forEach(item => {7 const li = document.createElement('li');8 li.textContent = item.name;9 list.appendChild(li); // Reflow each time10 });11}1213// Efficient: Single reflow14function renderItems(items) {15 const list = document.querySelector('.list');16 const fragment = document.createDocumentFragment();1718 items.forEach(item => {19 const li = document.createElement('li');20 li.textContent = item.name;21 fragment.appendChild(li); // No reflow22 });2324 list.innerHTML = '';25 list.appendChild(fragment); // Single reflow26}
Batch Style Changes
javascript1// Inefficient: Multiple reflows2element.style.width = '100px';3element.style.height = '100px';4element.style.margin = '10px';56// Efficient: Single reflow with class7element.classList.add('box-dimensions');89// Or use cssText for inline styles10element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';
Reading vs Writing
Interleaving reads and writes causes layout thrashing:
javascript1// BAD: Layout thrashing2items.forEach(item => {3 const height = item.offsetHeight; // Read (forces layout)4 item.style.height = height + 10 + 'px'; // Write (invalidates layout)5});67// GOOD: Batch reads, then batch writes8const heights = items.map(item => item.offsetHeight); // All reads910items.forEach((item, i) => {11 item.style.height = heights[i] + 10 + 'px'; // All writes12});
Component Pattern
Encapsulate DOM logic into reusable components:
javascript1class Modal {2 constructor(options = {}) {3 this.title = options.title || 'Modal';4 this.content = options.content || '';5 this.onClose = options.onClose;67 this.element = null;8 this.isOpen = false;9 }1011 render() {12 this.element = document.createElement('div');13 this.element.className = 'modal-overlay';14 this.element.innerHTML = `15 <div class="modal">16 <header class="modal-header">17 <h2>${this.escapeHtml(this.title)}</h2>18 <button class="modal-close" aria-label="Close">×</button>19 </header>20 <div class="modal-body">21 ${this.content}22 </div>23 </div>24 `;2526 this.bindEvents();27 return this.element;28 }2930 bindEvents() {31 // Close button32 this.element.querySelector('.modal-close')33 .addEventListener('click', () => this.close());3435 // Click outside36 this.element.addEventListener('click', (e) => {37 if (e.target === this.element) this.close();38 });3940 // Escape key41 this.handleKeydown = (e) => {42 if (e.key === 'Escape') this.close();43 };44 }4546 open() {47 if (this.isOpen) return;4849 document.body.appendChild(this.render());50 document.addEventListener('keydown', this.handleKeydown);51 document.body.style.overflow = 'hidden';52 this.isOpen = true;53 }5455 close() {56 if (!this.isOpen) return;5758 this.element.remove();59 document.removeEventListener('keydown', this.handleKeydown);60 document.body.style.overflow = '';61 this.isOpen = false;62 this.onClose?.();63 }6465 escapeHtml(text) {66 const div = document.createElement('div');67 div.textContent = text;68 return div.innerHTML;69 }7071 destroy() {72 this.close();73 this.element = null;74 }75}7677// Usage78const modal = new Modal({79 title: 'Confirm Action',80 content: '<p>Are you sure?</p><button class="confirm">Yes</button>',81 onClose: () => console.log('Modal closed')82});8384modal.open();
Observer Pattern for DOM
React to DOM changes:
javascript1class DOMObserver {2 constructor(element, callback) {3 this.element = element;4 this.callback = callback;5 this.observer = null;6 }78 observe(options = {}) {9 const config = {10 childList: true, // Watch for added/removed children11 subtree: true, // Watch all descendants12 attributes: true, // Watch attribute changes13 characterData: true, // Watch text content changes14 ...options15 };1617 this.observer = new MutationObserver((mutations) => {18 this.callback(mutations);19 });2021 this.observer.observe(this.element, config);22 }2324 disconnect() {25 this.observer?.disconnect();26 }27}2829// Usage: Watch for new elements30const watcher = new DOMObserver(document.body, (mutations) => {31 mutations.forEach(mutation => {32 mutation.addedNodes.forEach(node => {33 if (node.classList?.contains('lazy-image')) {34 loadImage(node);35 }36 });37 });38});3940watcher.observe({ childList: true, subtree: true });
Debounce and Throttle
Control event frequency:
javascript1// Debounce: Wait until user stops (good for search input)2function debounce(fn, delay) {3 let timeoutId;4 return function(...args) {5 clearTimeout(timeoutId);6 timeoutId = setTimeout(() => fn.apply(this, args), delay);7 };8}910// Usage11const searchInput = document.querySelector('#search');12searchInput.addEventListener('input', debounce((e) => {13 searchAPI(e.target.value);14}, 300));
javascript1// Throttle: Execute at most once per interval (good for scroll)2function throttle(fn, interval) {3 let lastTime = 0;4 return function(...args) {5 const now = Date.now();6 if (now - lastTime >= interval) {7 lastTime = now;8 fn.apply(this, args);9 }10 };11}1213// Usage14window.addEventListener('scroll', throttle(() => {15 checkInfiniteScroll();16}, 100));
State Management Pattern
Keep DOM in sync with state:
javascript1class StatefulList {2 constructor(container) {3 this.container = container;4 this.state = {5 items: [],6 filter: 'all'7 };8 }910 setState(updates) {11 this.state = { ...this.state, ...updates };12 this.render();13 }1415 addItem(item) {16 this.setState({17 items: [...this.state.items, { id: Date.now(), ...item }]18 });19 }2021 removeItem(id) {22 this.setState({23 items: this.state.items.filter(item => item.id !== id)24 });25 }2627 getFilteredItems() {28 const { items, filter } = this.state;29 switch (filter) {30 case 'completed':31 return items.filter(item => item.completed);32 case 'active':33 return items.filter(item => !item.completed);34 default:35 return items;36 }37 }3839 render() {40 const items = this.getFilteredItems();41 const fragment = document.createDocumentFragment();4243 items.forEach(item => {44 const li = document.createElement('li');45 li.dataset.id = item.id;46 li.className = item.completed ? 'completed' : '';47 li.innerHTML = `48 <span>${item.text}</span>49 <button class="delete">Delete</button>50 `;51 fragment.appendChild(li);52 });5354 this.container.innerHTML = '';55 this.container.appendChild(fragment);56 }57}
Template Pattern
Separate markup from logic:
javascript1// HTML template2// <template id="card-template">3// <div class="card">4// <h3 class="card-title"></h3>5// <p class="card-body"></p>6// <button class="card-action">Learn More</button>7// </div>8// </template>910class Card {11 static template = document.querySelector('#card-template');1213 constructor(data) {14 this.data = data;15 this.element = this.createElement();16 }1718 createElement() {19 const clone = Card.template.content.cloneNode(true);20 const card = clone.querySelector('.card');2122 card.querySelector('.card-title').textContent = this.data.title;23 card.querySelector('.card-body').textContent = this.data.body;2425 const button = card.querySelector('.card-action');26 button.addEventListener('click', () => this.handleAction());2728 return card;29 }3031 handleAction() {32 console.log('Action for:', this.data.title);33 }3435 appendTo(container) {36 container.appendChild(this.element);37 }38}3940// Usage41const card = new Card({ title: 'Hello', body: 'World' });42card.appendTo(document.querySelector('.cards'));
Cleanup Pattern
Prevent memory leaks:
javascript1class Widget {2 constructor(element) {3 this.element = element;4 this.listeners = [];5 this.intervals = [];6 this.init();7 }89 // Track event listeners for cleanup10 on(target, event, handler) {11 target.addEventListener(event, handler);12 this.listeners.push({ target, event, handler });13 }1415 // Track intervals for cleanup16 setInterval(callback, delay) {17 const id = window.setInterval(callback, delay);18 this.intervals.push(id);19 return id;20 }2122 init() {23 this.on(this.element, 'click', this.handleClick.bind(this));24 this.on(window, 'resize', this.handleResize.bind(this));25 this.setInterval(() => this.update(), 1000);26 }2728 handleClick(e) { /* ... */ }29 handleResize(e) { /* ... */ }30 update() { /* ... */ }3132 destroy() {33 // Remove all event listeners34 this.listeners.forEach(({ target, event, handler }) => {35 target.removeEventListener(event, handler);36 });37 this.listeners = [];3839 // Clear all intervals40 this.intervals.forEach(id => clearInterval(id));41 this.intervals = [];4243 // Remove element44 this.element.remove();45 this.element = null;46 }47}
Summary
| Pattern | Use Case |
|---|---|
| Event Delegation | Dynamic elements, many similar elements |
| DocumentFragment | Batch DOM insertions |
| Component | Reusable, encapsulated UI pieces |
| Observer | React to DOM changes |
| Debounce/Throttle | Control event frequency |
| State Management | Keep UI in sync with data |
| Template | Separate markup from logic |
| Cleanup | Prevent memory leaks |
Key principles:
- Minimize DOM access (cache references)
- Batch DOM operations
- Use event delegation for dynamic content
- Always clean up listeners and intervals
- Separate state from DOM