javascript
examples
examples.js⚡javascript
/**
* 19.2 Mutation Observer - Examples
*
* MutationObserver watches for DOM changes asynchronously
*/
// ============================================
// BASIC MUTATION OBSERVER
// ============================================
/**
* Simple observer watching for child changes
*/
function basicChildListObserver() {
const container = document.getElementById('container');
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
console.log('=== Child List Mutation ===');
console.log('Added nodes:', mutation.addedNodes.length);
console.log('Removed nodes:', mutation.removedNodes.length);
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
console.log(' Added:', node.tagName, node.id || '');
}
});
mutation.removedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
console.log(' Removed:', node.tagName, node.id || '');
}
});
}
});
});
// Start observing
observer.observe(container, {
childList: true,
});
return observer;
}
// ============================================
// ATTRIBUTE OBSERVER
// ============================================
/**
* Observe attribute changes with old value tracking
*/
function attributeObserver() {
const element = document.getElementById('myElement');
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes') {
console.log('=== Attribute Mutation ===');
console.log('Attribute:', mutation.attributeName);
console.log('Old value:', mutation.oldValue);
console.log(
'New value:',
mutation.target.getAttribute(mutation.attributeName)
);
}
});
});
observer.observe(element, {
attributes: true,
attributeOldValue: true,
});
return observer;
}
/**
* Filter specific attributes to watch
*/
function filteredAttributeObserver() {
const form = document.querySelector('form');
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
console.log(`${mutation.attributeName} changed on`, mutation.target);
});
});
// Only watch specific attributes
observer.observe(form, {
attributes: true,
subtree: true,
attributeFilter: ['class', 'disabled', 'data-valid'],
});
return observer;
}
// ============================================
// CHARACTER DATA OBSERVER
// ============================================
/**
* Watch text content changes
*/
function characterDataObserver() {
const textContainer = document.getElementById('editable');
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'characterData') {
console.log('=== Text Content Changed ===');
console.log('Old text:', mutation.oldValue);
console.log('New text:', mutation.target.textContent);
}
});
});
observer.observe(textContainer, {
characterData: true,
characterDataOldValue: true,
subtree: true, // Needed to observe text nodes
});
return observer;
}
// ============================================
// SUBTREE OBSERVATION
// ============================================
/**
* Deep observation of entire subtree
*/
function subtreeObserver() {
const root = document.getElementById('app');
const observer = new MutationObserver((mutations) => {
console.log(`Received ${mutations.length} mutations`);
const summary = {
childList: 0,
attributes: 0,
characterData: 0,
addedNodes: 0,
removedNodes: 0,
};
mutations.forEach((mutation) => {
summary[mutation.type]++;
summary.addedNodes += mutation.addedNodes.length;
summary.removedNodes += mutation.removedNodes.length;
});
console.log('Mutation summary:', summary);
});
// Watch everything in the subtree
observer.observe(root, {
childList: true,
attributes: true,
characterData: true,
subtree: true,
});
return observer;
}
// ============================================
// PRACTICAL USE CASES
// ============================================
/**
* Auto-save when form content changes
*/
function formAutoSave() {
const form = document.querySelector('form');
let saveTimeout = null;
const observer = new MutationObserver(() => {
// Debounce saves
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
console.log('Auto-saving form data...');
const formData = new FormData(form);
// Save to localStorage or server
localStorage.setItem(
'formDraft',
JSON.stringify(Object.fromEntries(formData))
);
}, 1000);
});
observer.observe(form, {
attributes: true,
subtree: true,
attributeFilter: ['value'],
});
// Also watch for input events (MutationObserver doesn't catch value property changes)
form.addEventListener('input', () => {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
localStorage.setItem(
'formDraft',
JSON.stringify(Object.fromEntries(new FormData(form)))
);
}, 1000);
});
return observer;
}
/**
* Track dynamically added elements (e.g., from third-party scripts)
*/
function thirdPartyContentTracker() {
const adContainer = document.getElementById('ads');
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Track or modify third-party content
console.log('Third-party content added:', node);
// Example: Add rel="noopener" to external links
const links = node.querySelectorAll
? node.querySelectorAll('a[target="_blank"]')
: [];
links.forEach((link) => {
link.setAttribute('rel', 'noopener noreferrer');
});
}
});
});
});
observer.observe(adContainer, {
childList: true,
subtree: true,
});
return observer;
}
/**
* Image lazy loading trigger
*/
function lazyImageObserver() {
const content = document.getElementById('content');
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
// Find lazy images in added content
const lazyImages = node.matches?.('[data-lazy-src]')
? [node]
: node.querySelectorAll?.('[data-lazy-src]') || [];
lazyImages.forEach((img) => {
// Trigger lazy load
if ('IntersectionObserver' in window) {
// Let IntersectionObserver handle it
return;
}
// Fallback: load immediately
img.src = img.dataset.lazySrc;
img.removeAttribute('data-lazy-src');
});
});
});
});
observer.observe(content, {
childList: true,
subtree: true,
});
return observer;
}
// ============================================
// OBSERVER MANAGEMENT
// ============================================
/**
* Process pending mutations before disconnect
*/
function safeDisconnect(observer) {
// Get any unprocessed mutations
const pending = observer.takeRecords();
if (pending.length > 0) {
console.log(
`Processing ${pending.length} pending mutations before disconnect`
);
// Process pending mutations...
}
observer.disconnect();
console.log('Observer disconnected');
}
/**
* Observer with pause/resume capability
*/
class PausableMutationObserver {
constructor(callback) {
this.callback = callback;
this.observer = new MutationObserver(callback);
this.target = null;
this.config = null;
this.isPaused = false;
}
observe(target, config) {
this.target = target;
this.config = config;
this.observer.observe(target, config);
}
pause() {
if (!this.isPaused) {
this.observer.disconnect();
this.isPaused = true;
}
}
resume() {
if (this.isPaused && this.target) {
this.observer.observe(this.target, this.config);
this.isPaused = false;
}
}
disconnect() {
this.observer.disconnect();
this.target = null;
this.config = null;
}
takeRecords() {
return this.observer.takeRecords();
}
}
// ============================================
// AVOIDING INFINITE LOOPS
// ============================================
/**
* Prevent callback from triggering itself
*/
function safeObserverCallback() {
const container = document.getElementById('container');
let isProcessing = false;
const observer = new MutationObserver((mutations) => {
// Guard against recursive calls
if (isProcessing) return;
isProcessing = true;
try {
mutations.forEach((mutation) => {
// This might modify the DOM
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Safe modification
node.classList.add('processed');
}
});
});
} finally {
isProcessing = false;
}
});
observer.observe(container, {
childList: true,
subtree: true,
});
return observer;
}
/**
* Use disconnect/reconnect pattern for safe modifications
*/
function disconnectReconnectPattern() {
const container = document.getElementById('container');
let config = { childList: true, subtree: true };
const observer = new MutationObserver((mutations) => {
// Temporarily disconnect
observer.disconnect();
try {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Safe to modify now
const wrapper = document.createElement('div');
wrapper.className = 'wrapper';
node.parentNode.insertBefore(wrapper, node);
wrapper.appendChild(node);
}
});
});
} finally {
// Reconnect
observer.observe(container, config);
}
});
observer.observe(container, config);
return observer;
}
// ============================================
// PERFORMANCE PATTERNS
// ============================================
/**
* Batch process mutations efficiently
*/
function batchedMutationProcessor() {
const root = document.body;
let pendingMutations = [];
let processTimeout = null;
const processMutations = () => {
if (pendingMutations.length === 0) return;
console.log(`Processing batch of ${pendingMutations.length} mutations`);
// Group by type
const grouped = {
added: [],
removed: [],
attributes: [],
};
pendingMutations.forEach((mutation) => {
if (mutation.type === 'childList') {
grouped.added.push(...mutation.addedNodes);
grouped.removed.push(...mutation.removedNodes);
} else if (mutation.type === 'attributes') {
grouped.attributes.push({
target: mutation.target,
attr: mutation.attributeName,
});
}
});
// Process each group
if (grouped.added.length > 0) {
console.log(`Processing ${grouped.added.length} added nodes`);
}
if (grouped.removed.length > 0) {
console.log(`Cleaning up ${grouped.removed.length} removed nodes`);
}
if (grouped.attributes.length > 0) {
console.log(`Handling ${grouped.attributes.length} attribute changes`);
}
pendingMutations = [];
};
const observer = new MutationObserver((mutations) => {
pendingMutations.push(...mutations);
// Debounce processing
clearTimeout(processTimeout);
processTimeout = setTimeout(processMutations, 100);
});
observer.observe(root, {
childList: true,
attributes: true,
subtree: true,
});
return {
observer,
flush: processMutations,
};
}
// ============================================
// NODE.JS SIMULATION
// ============================================
console.log('=== MutationObserver Examples ===');
console.log('Note: MutationObserver is a browser API.');
console.log('');
console.log('Example configurations:');
console.log('');
// Show configuration examples
const configs = [
{
name: 'Watch children only',
config: { childList: true },
},
{
name: 'Watch attributes with filter',
config: { attributes: true, attributeFilter: ['class', 'style'] },
},
{
name: 'Deep observation',
config: { childList: true, subtree: true },
},
{
name: 'Full observation with old values',
config: {
childList: true,
attributes: true,
characterData: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true,
},
},
];
configs.forEach(({ name, config }) => {
console.log(`${name}:`);
console.log(` ${JSON.stringify(config)}`);
console.log('');
});
// Export for browser use
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
PausableMutationObserver,
basicChildListObserver,
attributeObserver,
filteredAttributeObserver,
characterDataObserver,
subtreeObserver,
formAutoSave,
thirdPartyContentTracker,
lazyImageObserver,
safeDisconnect,
safeObserverCallback,
disconnectReconnectPattern,
batchedMutationProcessor,
};
}