DOM Manipulation - Exercise 3
Event delegation attaches a single event listener to a parent element instead of multiple listeners on child elements. When an event occurs, it bubbles up to the parent where you check the target to handle it. This works for elements added dynamically after the page loads.
// HTML: <ul id="list"><li>Item 1</li><li>Item 2</li></ul>
const list = document.getElementById('list');
// Single listener on parent handles all li clicks
list.addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
console.log('Clicked:', event.target.textContent);
}
});
// Works for dynamically added items too
const newItem = document.createElement('li');
newItem.textContent = 'Item 3';
list.appendChild(newItem); // Click on this also works!
The stopPropagation() method prevents an event from bubbling up to parent elements or capturing down to child elements. This is useful when you want to handle an event on a specific element without triggering handlers on ancestor elements.
// HTML: <div id="outer"><button id="inner">Click</button></div>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
outer.addEventListener('click', function() {
console.log('Outer clicked');
});
inner.addEventListener('click', function(event) {
event.stopPropagation(); // Stops bubbling to outer
console.log('Inner clicked');
});
// Clicking button only logs "Inner clicked"
// Without stopPropagation, both messages would appear
The preventDefault() method stops the browser from performing its default action for an event. Common uses include preventing form submission, stopping links from navigating, or blocking context menus.
// Prevent form submission
const form = document.getElementById('myForm');
form.addEventListener('submit', function(event) {
event.preventDefault(); // Form won't submit
console.log('Form submission prevented');
});
// Prevent link navigation
const link = document.getElementById('myLink');
link.addEventListener('click', function(event) {
event.preventDefault(); // Link won't navigate
console.log('Navigation prevented');
});
// Prevent right-click context menu
document.addEventListener('contextmenu', function(event) {
event.preventDefault();
console.log('Context menu blocked');
});
The getComputedStyle() method returns the final computed values of all CSS properties for an element. This includes styles from stylesheets, inline styles, and browser defaults after all calculations are applied.
const element = document.getElementById('myElement');
// Get all computed styles
const styles = window.getComputedStyle(element);
// Access specific properties
console.log(styles.color); // "rgb(0, 0, 0)"
console.log(styles.fontSize); // "16px"
console.log(styles.backgroundColor); // "rgba(0, 0, 0, 0)"
// Use getPropertyValue for hyphenated names
console.log(styles.getPropertyValue('font-size')); // "16px"
console.log(styles.getPropertyValue('margin-top')); // "0px"
// Get pseudo-element styles
const beforeStyles = window.getComputedStyle(element, '::before');
console.log(beforeStyles.content);
The style property lets you read and set inline CSS styles directly on an element. CSS properties with hyphens are converted to camelCase in JavaScript. These styles have the highest specificity.
const box = document.getElementById('box');
// Set individual styles (camelCase for hyphenated properties)
box.style.backgroundColor = 'blue';
box.style.fontSize = '20px';
box.style.marginTop = '10px';
box.style.border = '2px solid red';
// Read inline styles
console.log(box.style.backgroundColor); // "blue"
// Set multiple styles at once using cssText
box.style.cssText = 'color: white; padding: 20px; display: flex;';
// Remove a style by setting to empty string
box.style.backgroundColor = '';
// Use setProperty for CSS variable support
box.style.setProperty('--custom-color', '#ff0000');
The offsetWidth and offsetHeight properties return the total visible size of an element including padding, border, and scrollbar. They return integers in pixels and are read only properties.
const element = document.getElementById('box');
// offsetWidth = content + padding + border + scrollbar
console.log(element.offsetWidth); // e.g., 300
console.log(element.offsetHeight); // e.g., 200
// clientWidth = content + padding (no border, no scrollbar)
console.log(element.clientWidth);
console.log(element.clientHeight);
// scrollWidth = full content width including overflow
console.log(element.scrollWidth);
console.log(element.scrollHeight);
// Get position relative to offset parent
console.log(element.offsetTop);
console.log(element.offsetLeft);
console.log(element.offsetParent); // nearest positioned ancestor
The getBoundingClientRect() method returns a DOMRect object with the size and position of an element relative to the viewport. It provides precise measurements including fractional pixels and updates when the page scrolls.
const element = document.getElementById('box');
const rect = element.getBoundingClientRect();
// Position relative to viewport
console.log(rect.top); // distance from viewport top
console.log(rect.left); // distance from viewport left
console.log(rect.bottom); // top + height
console.log(rect.right); // left + width
// Element dimensions
console.log(rect.width); // element width
console.log(rect.height); // element height
console.log(rect.x); // same as left
console.log(rect.y); // same as top
// Get position relative to document (accounting for scroll)
const docTop = rect.top + window.scrollY;
const docLeft = rect.left + window.scrollX;
The insertAdjacentHTML() method parses an HTML string and inserts the resulting elements at a specified position. It is faster than innerHTML because it does not reparse existing content. The position can be beforebegin, afterbegin, beforeend, or afterend.
const container = document.getElementById('container');
// beforebegin: before the element itself
container.insertAdjacentHTML('beforebegin', '<p>Before container</p>');
// afterbegin: inside element, before first child
container.insertAdjacentHTML('afterbegin', '<p>First inside</p>');
// beforeend: inside element, after last child
container.insertAdjacentHTML('beforeend', '<p>Last inside</p>');
// afterend: after the element itself
container.insertAdjacentHTML('afterend', '<p>After container</p>');
// Insert complex HTML
container.insertAdjacentHTML('beforeend', `
<div class="card">
<h3>Title</h3>
<p>Content here</p>
</div>
`);
A DocumentFragment is a lightweight container that holds DOM nodes without being part of the document tree. You can build multiple elements in memory first, then add them to the DOM in a single operation. This reduces reflows and improves performance.
const list = document.getElementById('list');
const fragment = document.createDocumentFragment();
// Build elements in memory (no DOM updates yet)
for (let i = 1; i <= 100; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
// Single DOM update with all 100 items
list.appendChild(fragment);
// The fragment becomes empty after appending
console.log(fragment.childNodes.length); // 0
// Compare with slow approach (100 DOM updates):
// for (let i = 1; i <= 100; i++) {
// const li = document.createElement('li');
// li.textContent = `Item ${i}`;
// list.appendChild(li); // Triggers reflow each time
// }
The MutationObserver API watches for changes to the DOM tree and calls a callback when mutations occur. You can observe child additions, attribute changes, text content changes, and more. It is efficient and batches multiple mutations together.
const target = document.getElementById('container');
// Create observer with callback
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log('Type:', mutation.type);
console.log('Added nodes:', mutation.addedNodes);
console.log('Removed nodes:', mutation.removedNodes);
});
});
// Configure what to observe
const config = {
childList: true, // Watch for added/removed children
attributes: true, // Watch for attribute changes
subtree: true, // Watch all descendants
characterData: true // Watch text content changes
};
// Start observing
observer.observe(target, config);
// Later, stop observing
observer.disconnect();
The dataset property provides read and write access to custom data attributes on elements. Data attributes start with data- in HTML and are accessed using camelCase in JavaScript. They are useful for storing extra information on elements.
// HTML: <div id="user" data-user-id="123" data-role="admin"></div>
const element = document.getElementById('user');
// Read data attributes (data-user-id becomes userId)
console.log(element.dataset.userId); // "123"
console.log(element.dataset.role); // "admin"
// Set data attributes
element.dataset.status = 'active';
element.dataset.lastLogin = '2024-01-15';
// Creates: data-status="active" data-last-login="2024-01-15"
// Delete a data attribute
delete element.dataset.role;
// Check if attribute exists
if (element.dataset.userId) {
console.log('User ID found');
}
// Loop through all data attributes
for (const key in element.dataset) {
console.log(key, element.dataset[key]);
}
The scrollIntoView() method scrolls the page so that the element becomes visible in the viewport. You can control the alignment and scrolling behavior. It accepts a boolean for simple alignment or an options object for more control.
const element = document.getElementById('target');
// Simple usage: scroll to top of element
element.scrollIntoView();
// Align to top (default) or bottom
element.scrollIntoView(true); // align to top
element.scrollIntoView(false); // align to bottom
// Options object for more control
element.scrollIntoView({
behavior: 'smooth', // 'auto' or 'smooth' scrolling
block: 'center', // 'start', 'center', 'end', 'nearest'
inline: 'nearest' // horizontal alignment
});
// Scroll to section when clicking a link
document.querySelectorAll('a[href^="#"]').forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});