javascript

examples

examples.js
/**
 * 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,
  };
}
Examples - JavaScript Tutorial | DeepML