javascript

exercises

exercises.js
/**
 * 19.2 Mutation Observer - Exercises
 *
 * Practice implementing MutationObserver patterns
 */

// ============================================
// EXERCISE 1: DOM Change Logger
// ============================================

/**
 * Create a comprehensive DOM change logger that:
 * - Tracks all types of mutations
 * - Provides detailed change reports
 * - Supports filtering by mutation type
 *
 * Requirements:
 * - Track childList, attributes, and characterData
 * - Record timestamps for each change
 * - Provide getHistory() method
 */

class DOMChangeLogger {
  // Your implementation here
}

/*
// SOLUTION:
class DOMChangeLogger {
    constructor(target, options = {}) {
        this.target = target;
        this.options = options;
        this.history = [];
        this.observer = null;
        this.maxHistory = options.maxHistory || 1000;
    }
    
    start() {
        this.observer = new MutationObserver((mutations) => {
            mutations.forEach(mutation => {
                const record = {
                    timestamp: Date.now(),
                    type: mutation.type,
                    target: mutation.target,
                    targetPath: this.getNodePath(mutation.target)
                };
                
                if (mutation.type === 'childList') {
                    record.addedNodes = Array.from(mutation.addedNodes);
                    record.removedNodes = Array.from(mutation.removedNodes);
                } else if (mutation.type === 'attributes') {
                    record.attributeName = mutation.attributeName;
                    record.oldValue = mutation.oldValue;
                    record.newValue = mutation.target.getAttribute(mutation.attributeName);
                } else if (mutation.type === 'characterData') {
                    record.oldValue = mutation.oldValue;
                    record.newValue = mutation.target.textContent;
                }
                
                this.history.push(record);
                
                // Trim history if needed
                if (this.history.length > this.maxHistory) {
                    this.history = this.history.slice(-this.maxHistory);
                }
            });
        });
        
        this.observer.observe(this.target, {
            childList: true,
            attributes: true,
            characterData: true,
            subtree: true,
            attributeOldValue: true,
            characterDataOldValue: true
        });
    }
    
    stop() {
        if (this.observer) {
            const pending = this.observer.takeRecords();
            // Process pending if needed
            this.observer.disconnect();
            this.observer = null;
        }
    }
    
    getNodePath(node) {
        const path = [];
        let current = node;
        
        while (current && current !== document) {
            let selector = current.nodeName.toLowerCase();
            if (current.id) {
                selector += '#' + current.id;
            } else if (current.className && typeof current.className === 'string') {
                selector += '.' + current.className.split(' ').join('.');
            }
            path.unshift(selector);
            current = current.parentNode;
        }
        
        return path.join(' > ');
    }
    
    getHistory(filter = null) {
        if (!filter) return [...this.history];
        
        return this.history.filter(record => {
            if (filter.type && record.type !== filter.type) return false;
            if (filter.since && record.timestamp < filter.since) return false;
            if (filter.attributeName && record.attributeName !== filter.attributeName) return false;
            return true;
        });
    }
    
    clear() {
        this.history = [];
    }
    
    getSummary() {
        return {
            total: this.history.length,
            byType: this.history.reduce((acc, r) => {
                acc[r.type] = (acc[r.type] || 0) + 1;
                return acc;
            }, {}),
            firstChange: this.history[0]?.timestamp,
            lastChange: this.history[this.history.length - 1]?.timestamp
        };
    }
}
*/

// ============================================
// EXERCISE 2: Element Change Tracker
// ============================================

/**
 * Create a change tracker for specific elements:
 * - Track when elements gain/lose specific classes
 * - Track when elements become visible/hidden
 * - Track when elements are added/removed from DOM
 *
 * Requirements:
 * - Emit events for each change type
 * - Support multiple tracked elements
 */

class ElementChangeTracker {
  // Your implementation here
}

/*
// SOLUTION:
class ElementChangeTracker {
    constructor() {
        this.observers = new Map();
        this.listeners = new Map();
    }
    
    track(element, options = {}) {
        const {
            classes = [],
            visibilityAttribute = 'hidden',
            trackRemoval = true
        } = options;
        
        const state = {
            classes: new Set(classes.filter(c => element.classList.contains(c))),
            visible: !element.hasAttribute(visibilityAttribute),
            inDOM: document.contains(element)
        };
        
        const observer = new MutationObserver((mutations) => {
            mutations.forEach(mutation => {
                if (mutation.type === 'attributes') {
                    if (mutation.attributeName === 'class') {
                        // Check class changes
                        classes.forEach(className => {
                            const hasClass = element.classList.contains(className);
                            const hadClass = state.classes.has(className);
                            
                            if (hasClass && !hadClass) {
                                state.classes.add(className);
                                this.emit('classAdded', { element, className });
                            } else if (!hasClass && hadClass) {
                                state.classes.delete(className);
                                this.emit('classRemoved', { element, className });
                            }
                        });
                    }
                    
                    if (mutation.attributeName === visibilityAttribute) {
                        const visible = !element.hasAttribute(visibilityAttribute);
                        if (visible !== state.visible) {
                            state.visible = visible;
                            this.emit(visible ? 'shown' : 'hidden', { element });
                        }
                    }
                }
            });
        });
        
        observer.observe(element, {
            attributes: true,
            attributeFilter: ['class', visibilityAttribute]
        });
        
        // Track removal from DOM
        if (trackRemoval && element.parentNode) {
            const parentObserver = new MutationObserver((mutations) => {
                mutations.forEach(mutation => {
                    mutation.removedNodes.forEach(node => {
                        if (node === element || node.contains(element)) {
                            state.inDOM = false;
                            this.emit('removed', { element });
                            parentObserver.disconnect();
                        }
                    });
                });
            });
            
            parentObserver.observe(document.body, {
                childList: true,
                subtree: true
            });
            
            this.observers.set(element + '_parent', parentObserver);
        }
        
        this.observers.set(element, observer);
        return this;
    }
    
    untrack(element) {
        const observer = this.observers.get(element);
        if (observer) {
            observer.disconnect();
            this.observers.delete(element);
        }
        
        const parentObserver = this.observers.get(element + '_parent');
        if (parentObserver) {
            parentObserver.disconnect();
            this.observers.delete(element + '_parent');
        }
    }
    
    on(event, callback) {
        if (!this.listeners.has(event)) {
            this.listeners.set(event, []);
        }
        this.listeners.get(event).push(callback);
        return this;
    }
    
    off(event, callback) {
        const listeners = this.listeners.get(event);
        if (listeners) {
            const index = listeners.indexOf(callback);
            if (index > -1) {
                listeners.splice(index, 1);
            }
        }
        return this;
    }
    
    emit(event, data) {
        const listeners = this.listeners.get(event) || [];
        listeners.forEach(callback => callback(data));
    }
    
    destroy() {
        this.observers.forEach(observer => observer.disconnect());
        this.observers.clear();
        this.listeners.clear();
    }
}
*/

// ============================================
// EXERCISE 3: DOM Diff Tracker
// ============================================

/**
 * Create a DOM diff tracker that:
 * - Takes snapshots of DOM state
 * - Computes differences between snapshots
 * - Can undo/redo changes
 *
 * Requirements:
 * - Capture element attributes and content
 * - Support nested elements
 * - Provide diff report
 */

class DOMDiffTracker {
  // Your implementation here
}

/*
// SOLUTION:
class DOMDiffTracker {
    constructor(root) {
        this.root = root;
        this.snapshots = [];
        this.currentIndex = -1;
    }
    
    takeSnapshot(label = '') {
        const snapshot = {
            label,
            timestamp: Date.now(),
            state: this.serializeElement(this.root)
        };
        
        // Remove any redo history
        this.snapshots = this.snapshots.slice(0, this.currentIndex + 1);
        this.snapshots.push(snapshot);
        this.currentIndex = this.snapshots.length - 1;
        
        return snapshot;
    }
    
    serializeElement(element) {
        if (element.nodeType === Node.TEXT_NODE) {
            return {
                type: 'text',
                content: element.textContent
            };
        }
        
        if (element.nodeType !== Node.ELEMENT_NODE) {
            return null;
        }
        
        const attributes = {};
        for (const attr of element.attributes) {
            attributes[attr.name] = attr.value;
        }
        
        return {
            type: 'element',
            tagName: element.tagName.toLowerCase(),
            attributes,
            children: Array.from(element.childNodes)
                .map(child => this.serializeElement(child))
                .filter(Boolean)
        };
    }
    
    diff(index1, index2) {
        if (index1 < 0 || index1 >= this.snapshots.length ||
            index2 < 0 || index2 >= this.snapshots.length) {
            return null;
        }
        
        const state1 = this.snapshots[index1].state;
        const state2 = this.snapshots[index2].state;
        
        return this.computeDiff(state1, state2, '');
    }
    
    computeDiff(obj1, obj2, path) {
        const changes = [];
        
        if (!obj1 && !obj2) return changes;
        
        if (!obj1) {
            changes.push({ type: 'added', path, value: obj2 });
            return changes;
        }
        
        if (!obj2) {
            changes.push({ type: 'removed', path, value: obj1 });
            return changes;
        }
        
        if (obj1.type !== obj2.type) {
            changes.push({ type: 'replaced', path, oldValue: obj1, newValue: obj2 });
            return changes;
        }
        
        if (obj1.type === 'text') {
            if (obj1.content !== obj2.content) {
                changes.push({
                    type: 'textChanged',
                    path,
                    oldValue: obj1.content,
                    newValue: obj2.content
                });
            }
            return changes;
        }
        
        // Compare attributes
        const allAttrs = new Set([
            ...Object.keys(obj1.attributes || {}),
            ...Object.keys(obj2.attributes || {})
        ]);
        
        allAttrs.forEach(attr => {
            const val1 = obj1.attributes?.[attr];
            const val2 = obj2.attributes?.[attr];
            
            if (val1 !== val2) {
                changes.push({
                    type: 'attributeChanged',
                    path: `${path}@${attr}`,
                    attribute: attr,
                    oldValue: val1,
                    newValue: val2
                });
            }
        });
        
        // Compare children
        const maxChildren = Math.max(
            obj1.children?.length || 0,
            obj2.children?.length || 0
        );
        
        for (let i = 0; i < maxChildren; i++) {
            const childPath = `${path}/${i}`;
            changes.push(...this.computeDiff(
                obj1.children?.[i],
                obj2.children?.[i],
                childPath
            ));
        }
        
        return changes;
    }
    
    canUndo() {
        return this.currentIndex > 0;
    }
    
    canRedo() {
        return this.currentIndex < this.snapshots.length - 1;
    }
    
    undo() {
        if (!this.canUndo()) return null;
        
        this.currentIndex--;
        const snapshot = this.snapshots[this.currentIndex];
        this.applySnapshot(snapshot.state);
        return snapshot;
    }
    
    redo() {
        if (!this.canRedo()) return null;
        
        this.currentIndex++;
        const snapshot = this.snapshots[this.currentIndex];
        this.applySnapshot(snapshot.state);
        return snapshot;
    }
    
    applySnapshot(state) {
        this.rebuildElement(this.root, state);
    }
    
    rebuildElement(element, state) {
        if (state.type === 'text') {
            element.textContent = state.content;
            return;
        }
        
        // Update attributes
        const currentAttrs = new Set(Array.from(element.attributes).map(a => a.name));
        
        for (const [name, value] of Object.entries(state.attributes || {})) {
            element.setAttribute(name, value);
            currentAttrs.delete(name);
        }
        
        // Remove extra attributes
        currentAttrs.forEach(attr => element.removeAttribute(attr));
        
        // Rebuild children
        element.innerHTML = '';
        state.children?.forEach(childState => {
            if (childState.type === 'text') {
                element.appendChild(document.createTextNode(childState.content));
            } else {
                const child = document.createElement(childState.tagName);
                element.appendChild(child);
                this.rebuildElement(child, childState);
            }
        });
    }
    
    getHistory() {
        return this.snapshots.map((s, i) => ({
            index: i,
            label: s.label,
            timestamp: s.timestamp,
            isCurrent: i === this.currentIndex
        }));
    }
}
*/

// ============================================
// EXERCISE 4: Reactive DOM Bindings
// ============================================

/**
 * Create a simple reactive binding system:
 * - Bind data to DOM elements
 * - Automatically update DOM when data changes
 * - Support two-way binding for inputs
 *
 * Requirements:
 * - Use MutationObserver for DOM monitoring
 * - Use Proxy for data reactivity
 */

class ReactiveBindings {
  // Your implementation here
}

/*
// SOLUTION:
class ReactiveBindings {
    constructor(root, data = {}) {
        this.root = root;
        this.bindings = new Map();
        this.observer = null;
        this.updating = false;
        
        // Create reactive data
        this.data = this.createReactiveData(data);
        
        this.init();
    }
    
    createReactiveData(data) {
        const self = this;
        
        return new Proxy(data, {
            set(target, property, value) {
                target[property] = value;
                self.updateDOM(property, value);
                return true;
            },
            get(target, property) {
                return target[property];
            }
        });
    }
    
    init() {
        this.scanBindings();
        this.setupDOMObserver();
        this.setupInputListeners();
    }
    
    scanBindings() {
        // Find all bound elements
        const textBindings = this.root.querySelectorAll('[data-bind]');
        const inputBindings = this.root.querySelectorAll('[data-model]');
        
        textBindings.forEach(element => {
            const property = element.dataset.bind;
            if (!this.bindings.has(property)) {
                this.bindings.set(property, { text: [], inputs: [] });
            }
            this.bindings.get(property).text.push(element);
            
            // Initial render
            if (this.data[property] !== undefined) {
                element.textContent = this.data[property];
            }
        });
        
        inputBindings.forEach(element => {
            const property = element.dataset.model;
            if (!this.bindings.has(property)) {
                this.bindings.set(property, { text: [], inputs: [] });
            }
            this.bindings.get(property).inputs.push(element);
            
            // Initial render
            if (this.data[property] !== undefined) {
                element.value = this.data[property];
            }
        });
    }
    
    setupDOMObserver() {
        this.observer = new MutationObserver((mutations) => {
            if (this.updating) return;
            
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        // Scan new elements for bindings
                        if (node.dataset?.bind) {
                            const property = node.dataset.bind;
                            if (!this.bindings.has(property)) {
                                this.bindings.set(property, { text: [], inputs: [] });
                            }
                            this.bindings.get(property).text.push(node);
                            if (this.data[property] !== undefined) {
                                node.textContent = this.data[property];
                            }
                        }
                        
                        if (node.dataset?.model) {
                            const property = node.dataset.model;
                            if (!this.bindings.has(property)) {
                                this.bindings.set(property, { text: [], inputs: [] });
                            }
                            this.bindings.get(property).inputs.push(node);
                            if (this.data[property] !== undefined) {
                                node.value = this.data[property];
                            }
                            this.addInputListener(node, property);
                        }
                    }
                });
            });
        });
        
        this.observer.observe(this.root, {
            childList: true,
            subtree: true
        });
    }
    
    setupInputListeners() {
        this.bindings.forEach((binding, property) => {
            binding.inputs.forEach(input => {
                this.addInputListener(input, property);
            });
        });
    }
    
    addInputListener(input, property) {
        const eventType = input.type === 'checkbox' ? 'change' : 'input';
        
        input.addEventListener(eventType, () => {
            const value = input.type === 'checkbox' ? input.checked : input.value;
            this.updating = true;
            this.data[property] = value;
            this.updating = false;
        });
    }
    
    updateDOM(property, value) {
        const binding = this.bindings.get(property);
        if (!binding) return;
        
        this.updating = true;
        
        binding.text.forEach(element => {
            element.textContent = value;
        });
        
        binding.inputs.forEach(input => {
            if (input.type === 'checkbox') {
                input.checked = !!value;
            } else {
                input.value = value;
            }
        });
        
        this.updating = false;
    }
    
    destroy() {
        if (this.observer) {
            this.observer.disconnect();
        }
        this.bindings.clear();
    }
}
*/

// ============================================
// EXERCISE 5: Virtual DOM Reconciler
// ============================================

/**
 * Create a simple virtual DOM reconciler:
 * - Build virtual DOM tree
 * - Compute minimal patches
 * - Apply patches efficiently
 *
 * Use MutationObserver to verify patches
 */

class SimpleVDOM {
  // Your implementation here
}

/*
// SOLUTION:
class SimpleVDOM {
    constructor(container) {
        this.container = container;
        this.currentVTree = null;
        this.patchLog = [];
        
        // Set up observer for debugging
        this.observer = new MutationObserver((mutations) => {
            this.patchLog.push({
                timestamp: Date.now(),
                mutations: mutations.length,
                types: mutations.map(m => m.type)
            });
        });
        
        this.observer.observe(container, {
            childList: true,
            attributes: true,
            characterData: true,
            subtree: true
        });
    }
    
    // Create virtual node
    createElement(type, props = {}, ...children) {
        return {
            type,
            props: props || {},
            children: children.flat().map(child =>
                typeof child === 'string' || typeof child === 'number'
                    ? { type: 'TEXT', props: { nodeValue: String(child) } }
                    : child
            )
        };
    }
    
    // Render virtual tree to real DOM
    render(vTree) {
        if (!this.currentVTree) {
            const dom = this.createDOM(vTree);
            this.container.innerHTML = '';
            this.container.appendChild(dom);
        } else {
            this.patch(this.container.firstChild, this.currentVTree, vTree);
        }
        this.currentVTree = vTree;
    }
    
    createDOM(vNode) {
        if (vNode.type === 'TEXT') {
            return document.createTextNode(vNode.props.nodeValue);
        }
        
        const dom = document.createElement(vNode.type);
        
        // Set properties
        Object.entries(vNode.props).forEach(([name, value]) => {
            if (name.startsWith('on')) {
                const eventName = name.slice(2).toLowerCase();
                dom.addEventListener(eventName, value);
            } else if (name === 'className') {
                dom.setAttribute('class', value);
            } else {
                dom.setAttribute(name, value);
            }
        });
        
        // Create children
        vNode.children?.forEach(child => {
            dom.appendChild(this.createDOM(child));
        });
        
        return dom;
    }
    
    patch(dom, oldVNode, newVNode) {
        // Node removed
        if (!newVNode) {
            dom.parentNode.removeChild(dom);
            return;
        }
        
        // Node added
        if (!oldVNode) {
            const newDOM = this.createDOM(newVNode);
            dom.parentNode.appendChild(newDOM);
            return;
        }
        
        // Different node type - replace
        if (oldVNode.type !== newVNode.type) {
            const newDOM = this.createDOM(newVNode);
            dom.parentNode.replaceChild(newDOM, dom);
            return;
        }
        
        // Text node
        if (newVNode.type === 'TEXT') {
            if (oldVNode.props.nodeValue !== newVNode.props.nodeValue) {
                dom.nodeValue = newVNode.props.nodeValue;
            }
            return;
        }
        
        // Update properties
        this.updateProps(dom, oldVNode.props, newVNode.props);
        
        // Patch children
        this.patchChildren(dom, oldVNode.children || [], newVNode.children || []);
    }
    
    updateProps(dom, oldProps, newProps) {
        // Remove old props
        Object.keys(oldProps).forEach(name => {
            if (!(name in newProps)) {
                if (name.startsWith('on')) {
                    const eventName = name.slice(2).toLowerCase();
                    dom.removeEventListener(eventName, oldProps[name]);
                } else {
                    dom.removeAttribute(name === 'className' ? 'class' : name);
                }
            }
        });
        
        // Set new props
        Object.entries(newProps).forEach(([name, value]) => {
            if (oldProps[name] !== value) {
                if (name.startsWith('on')) {
                    const eventName = name.slice(2).toLowerCase();
                    if (oldProps[name]) {
                        dom.removeEventListener(eventName, oldProps[name]);
                    }
                    dom.addEventListener(eventName, value);
                } else if (name === 'className') {
                    dom.setAttribute('class', value);
                } else {
                    dom.setAttribute(name, value);
                }
            }
        });
    }
    
    patchChildren(parent, oldChildren, newChildren) {
        const maxLen = Math.max(oldChildren.length, newChildren.length);
        
        for (let i = 0; i < maxLen; i++) {
            const oldChild = oldChildren[i];
            const newChild = newChildren[i];
            const childDOM = parent.childNodes[i];
            
            if (!oldChild && newChild) {
                parent.appendChild(this.createDOM(newChild));
            } else if (oldChild && !newChild) {
                if (childDOM) {
                    parent.removeChild(childDOM);
                }
            } else if (oldChild && newChild) {
                this.patch(childDOM, oldChild, newChild);
            }
        }
    }
    
    getPatchLog() {
        return [...this.patchLog];
    }
    
    destroy() {
        this.observer.disconnect();
    }
}

// Usage:
// const vdom = new SimpleVDOM(document.getElementById('app'));
// const h = vdom.createElement.bind(vdom);
// 
// vdom.render(
//     h('div', { className: 'container' },
//         h('h1', null, 'Hello'),
//         h('p', null, 'World')
//     )
// );
*/

// ============================================
// TEST UTILITIES
// ============================================

console.log('=== MutationObserver Exercises ===');
console.log('');
console.log('Exercises:');
console.log('1. DOMChangeLogger - Comprehensive DOM change logging');
console.log('2. ElementChangeTracker - Track specific element changes');
console.log('3. DOMDiffTracker - DOM snapshots and diffing');
console.log('4. ReactiveBindings - Two-way data binding');
console.log('5. SimpleVDOM - Virtual DOM reconciler');
console.log('');
console.log('These exercises require a browser DOM environment.');
console.log('Uncomment solutions to see implementations.');

// Export for browser use
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    DOMChangeLogger,
    ElementChangeTracker,
    DOMDiffTracker,
    ReactiveBindings,
    SimpleVDOM,
  };
}
Exercises - JavaScript Tutorial | DeepML