15 minlesson

DOM Manipulation Patterns

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:

javascript
1// Anti-pattern: Direct binding to each element
2function bindDeleteButtons() {
3 document.querySelectorAll('.delete-btn').forEach(btn => {
4 btn.addEventListener('click', handleDelete);
5 });
6}
7
8// Problem: New buttons added dynamically won't have handlers
9// Problem: Memory overhead with many elements
javascript
1// Pattern: Event delegation
2class TodoList {
3 constructor(container) {
4 this.container = container;
5 this.bindEvents();
6 }
7
8 bindEvents() {
9 // Single listener handles all interactions
10 this.container.addEventListener('click', (e) => {
11 const target = e.target;
12
13 // Handle different actions
14 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 }
23
24 handleDelete(item) {
25 const id = item.dataset.id;
26 item.remove();
27 this.onDelete?.(id);
28 }
29
30 handleEdit(item) {
31 const id = item.dataset.id;
32 this.onEdit?.(id);
33 }
34
35 handleToggle(item) {
36 item.classList.toggle('completed');
37 }
38}

Using closest() for Nested Elements

javascript
1// HTML structure
2// <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>
8
9list.addEventListener('click', (e) => {
10 // e.target could be the SVG inside the button
11 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

javascript
1// Inefficient: Multiple reflows
2function renderItems(items) {
3 const list = document.querySelector('.list');
4 list.innerHTML = ''; // Clear
5
6 items.forEach(item => {
7 const li = document.createElement('li');
8 li.textContent = item.name;
9 list.appendChild(li); // Reflow each time
10 });
11}
12
13// Efficient: Single reflow
14function renderItems(items) {
15 const list = document.querySelector('.list');
16 const fragment = document.createDocumentFragment();
17
18 items.forEach(item => {
19 const li = document.createElement('li');
20 li.textContent = item.name;
21 fragment.appendChild(li); // No reflow
22 });
23
24 list.innerHTML = '';
25 list.appendChild(fragment); // Single reflow
26}

Batch Style Changes

javascript
1// Inefficient: Multiple reflows
2element.style.width = '100px';
3element.style.height = '100px';
4element.style.margin = '10px';
5
6// Efficient: Single reflow with class
7element.classList.add('box-dimensions');
8
9// Or use cssText for inline styles
10element.style.cssText = 'width: 100px; height: 100px; margin: 10px;';

Reading vs Writing

Interleaving reads and writes causes layout thrashing:

javascript
1// BAD: Layout thrashing
2items.forEach(item => {
3 const height = item.offsetHeight; // Read (forces layout)
4 item.style.height = height + 10 + 'px'; // Write (invalidates layout)
5});
6
7// GOOD: Batch reads, then batch writes
8const heights = items.map(item => item.offsetHeight); // All reads
9
10items.forEach((item, i) => {
11 item.style.height = heights[i] + 10 + 'px'; // All writes
12});

Component Pattern

Encapsulate DOM logic into reusable components:

javascript
1class Modal {
2 constructor(options = {}) {
3 this.title = options.title || 'Modal';
4 this.content = options.content || '';
5 this.onClose = options.onClose;
6
7 this.element = null;
8 this.isOpen = false;
9 }
10
11 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">&times;</button>
19 </header>
20 <div class="modal-body">
21 ${this.content}
22 </div>
23 </div>
24 `;
25
26 this.bindEvents();
27 return this.element;
28 }
29
30 bindEvents() {
31 // Close button
32 this.element.querySelector('.modal-close')
33 .addEventListener('click', () => this.close());
34
35 // Click outside
36 this.element.addEventListener('click', (e) => {
37 if (e.target === this.element) this.close();
38 });
39
40 // Escape key
41 this.handleKeydown = (e) => {
42 if (e.key === 'Escape') this.close();
43 };
44 }
45
46 open() {
47 if (this.isOpen) return;
48
49 document.body.appendChild(this.render());
50 document.addEventListener('keydown', this.handleKeydown);
51 document.body.style.overflow = 'hidden';
52 this.isOpen = true;
53 }
54
55 close() {
56 if (!this.isOpen) return;
57
58 this.element.remove();
59 document.removeEventListener('keydown', this.handleKeydown);
60 document.body.style.overflow = '';
61 this.isOpen = false;
62 this.onClose?.();
63 }
64
65 escapeHtml(text) {
66 const div = document.createElement('div');
67 div.textContent = text;
68 return div.innerHTML;
69 }
70
71 destroy() {
72 this.close();
73 this.element = null;
74 }
75}
76
77// Usage
78const 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});
83
84modal.open();

Observer Pattern for DOM

React to DOM changes:

javascript
1class DOMObserver {
2 constructor(element, callback) {
3 this.element = element;
4 this.callback = callback;
5 this.observer = null;
6 }
7
8 observe(options = {}) {
9 const config = {
10 childList: true, // Watch for added/removed children
11 subtree: true, // Watch all descendants
12 attributes: true, // Watch attribute changes
13 characterData: true, // Watch text content changes
14 ...options
15 };
16
17 this.observer = new MutationObserver((mutations) => {
18 this.callback(mutations);
19 });
20
21 this.observer.observe(this.element, config);
22 }
23
24 disconnect() {
25 this.observer?.disconnect();
26 }
27}
28
29// Usage: Watch for new elements
30const 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});
39
40watcher.observe({ childList: true, subtree: true });

Debounce and Throttle

Control event frequency:

javascript
1// 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}
9
10// Usage
11const searchInput = document.querySelector('#search');
12searchInput.addEventListener('input', debounce((e) => {
13 searchAPI(e.target.value);
14}, 300));
javascript
1// 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}
12
13// Usage
14window.addEventListener('scroll', throttle(() => {
15 checkInfiniteScroll();
16}, 100));

State Management Pattern

Keep DOM in sync with state:

javascript
1class StatefulList {
2 constructor(container) {
3 this.container = container;
4 this.state = {
5 items: [],
6 filter: 'all'
7 };
8 }
9
10 setState(updates) {
11 this.state = { ...this.state, ...updates };
12 this.render();
13 }
14
15 addItem(item) {
16 this.setState({
17 items: [...this.state.items, { id: Date.now(), ...item }]
18 });
19 }
20
21 removeItem(id) {
22 this.setState({
23 items: this.state.items.filter(item => item.id !== id)
24 });
25 }
26
27 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 }
38
39 render() {
40 const items = this.getFilteredItems();
41 const fragment = document.createDocumentFragment();
42
43 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 });
53
54 this.container.innerHTML = '';
55 this.container.appendChild(fragment);
56 }
57}

Template Pattern

Separate markup from logic:

javascript
1// HTML template
2// <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>
9
10class Card {
11 static template = document.querySelector('#card-template');
12
13 constructor(data) {
14 this.data = data;
15 this.element = this.createElement();
16 }
17
18 createElement() {
19 const clone = Card.template.content.cloneNode(true);
20 const card = clone.querySelector('.card');
21
22 card.querySelector('.card-title').textContent = this.data.title;
23 card.querySelector('.card-body').textContent = this.data.body;
24
25 const button = card.querySelector('.card-action');
26 button.addEventListener('click', () => this.handleAction());
27
28 return card;
29 }
30
31 handleAction() {
32 console.log('Action for:', this.data.title);
33 }
34
35 appendTo(container) {
36 container.appendChild(this.element);
37 }
38}
39
40// Usage
41const card = new Card({ title: 'Hello', body: 'World' });
42card.appendTo(document.querySelector('.cards'));

Cleanup Pattern

Prevent memory leaks:

javascript
1class Widget {
2 constructor(element) {
3 this.element = element;
4 this.listeners = [];
5 this.intervals = [];
6 this.init();
7 }
8
9 // Track event listeners for cleanup
10 on(target, event, handler) {
11 target.addEventListener(event, handler);
12 this.listeners.push({ target, event, handler });
13 }
14
15 // Track intervals for cleanup
16 setInterval(callback, delay) {
17 const id = window.setInterval(callback, delay);
18 this.intervals.push(id);
19 return id;
20 }
21
22 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 }
27
28 handleClick(e) { /* ... */ }
29 handleResize(e) { /* ... */ }
30 update() { /* ... */ }
31
32 destroy() {
33 // Remove all event listeners
34 this.listeners.forEach(({ target, event, handler }) => {
35 target.removeEventListener(event, handler);
36 });
37 this.listeners = [];
38
39 // Clear all intervals
40 this.intervals.forEach(id => clearInterval(id));
41 this.intervals = [];
42
43 // Remove element
44 this.element.remove();
45 this.element = null;
46 }
47}

Summary

PatternUse Case
Event DelegationDynamic elements, many similar elements
DocumentFragmentBatch DOM insertions
ComponentReusable, encapsulated UI pieces
ObserverReact to DOM changes
Debounce/ThrottleControl event frequency
State ManagementKeep UI in sync with data
TemplateSeparate markup from logic
CleanupPrevent 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