/** * Nested Component Handler * * Manages parent-child relationships for nested LiveComponents on the client-side. * Coordinates event bubbling, state synchronization, and lifecycle management. * * Features: * - Parent-child relationship tracking * - Event bubbling from child to parent * - Child lifecycle coordination * - State synchronization between parent and children * - Automatic cleanup on component destruction * * Architecture: * - Scans DOM for nested components (data-parent-component attribute) * - Registers hierarchies with LiveComponentManager * - Intercepts events to enable bubbling * - Coordinates updates between parents and children */ export class NestedComponentHandler { constructor(liveComponentManager) { this.liveComponentManager = liveComponentManager; // Registry: componentId → { parentId, childIds, depth } this.hierarchyRegistry = new Map(); // Parent → [Children] mapping this.childrenRegistry = new Map(); // Event bubbling callbacks: componentId → [callbacks] this.bubbleCallbacks = new Map(); } /** * Initialize nested component system * Scans DOM for nested components and registers hierarchies */ init() { this.scanNestedComponents(); console.log(`[NestedComponents] Initialized with ${this.hierarchyRegistry.size} components`); } /** * Scan DOM for nested components * Looks for data-parent-component attribute to establish parent-child relationships */ scanNestedComponents() { // Find all components with parent const nestedComponents = document.querySelectorAll('[data-parent-component]'); nestedComponents.forEach(element => { const componentId = element.dataset.liveComponent; const parentId = element.dataset.parentComponent; const depth = parseInt(element.dataset.nestingDepth) || 1; if (componentId && parentId) { this.registerHierarchy(componentId, parentId, depth); } }); // Register root components (no parent) const rootComponents = document.querySelectorAll('[data-live-component]:not([data-parent-component])'); rootComponents.forEach(element => { const componentId = element.dataset.liveComponent; if (componentId && !this.hierarchyRegistry.has(componentId)) { this.registerRoot(componentId); } }); } /** * Register root component (no parent) * * @param {string} componentId - Component ID */ registerRoot(componentId) { this.hierarchyRegistry.set(componentId, { parentId: null, childIds: [], depth: 0, path: [componentId] }); console.log(`[NestedComponents] Registered root: ${componentId}`); } /** * Register component hierarchy * * @param {string} componentId - Child component ID * @param {string} parentId - Parent component ID * @param {number} depth - Nesting depth */ registerHierarchy(componentId, parentId, depth = 1) { // Get parent's path const parentHierarchy = this.hierarchyRegistry.get(parentId); const parentPath = parentHierarchy ? parentHierarchy.path : [parentId]; // Create hierarchy entry this.hierarchyRegistry.set(componentId, { parentId, childIds: [], depth, path: [...parentPath, componentId] }); // Add to parent's children if (!this.childrenRegistry.has(parentId)) { this.childrenRegistry.set(parentId, []); } const children = this.childrenRegistry.get(parentId); if (!children.includes(componentId)) { children.push(componentId); } console.log(`[NestedComponents] Registered child: ${componentId} (parent: ${parentId}, depth: ${depth})`); } /** * Register dynamic nested component at runtime * * @param {string} componentId - Component ID * @param {string} parentId - Parent component ID */ registerDynamicChild(componentId, parentId) { const parentHierarchy = this.hierarchyRegistry.get(parentId); if (!parentHierarchy) { console.warn(`[NestedComponents] Cannot register child - parent not found: ${parentId}`); return; } const depth = parentHierarchy.depth + 1; this.registerHierarchy(componentId, parentId, depth); } /** * Get component hierarchy * * @param {string} componentId - Component ID * @returns {Object|null} Hierarchy object or null */ getHierarchy(componentId) { return this.hierarchyRegistry.get(componentId) || null; } /** * Get parent component ID * * @param {string} componentId - Component ID * @returns {string|null} Parent ID or null if root */ getParentId(componentId) { const hierarchy = this.getHierarchy(componentId); return hierarchy ? hierarchy.parentId : null; } /** * Get child component IDs * * @param {string} componentId - Parent component ID * @returns {Array} Array of child IDs */ getChildIds(componentId) { return this.childrenRegistry.get(componentId) || []; } /** * Check if component has children * * @param {string} componentId - Component ID * @returns {boolean} True if has children */ hasChildren(componentId) { const children = this.getChildIds(componentId); return children.length > 0; } /** * Check if component is root * * @param {string} componentId - Component ID * @returns {boolean} True if root component */ isRoot(componentId) { const hierarchy = this.getHierarchy(componentId); return hierarchy ? hierarchy.parentId === null : true; } /** * Get nesting depth * * @param {string} componentId - Component ID * @returns {number} Nesting depth (0 for root) */ getDepth(componentId) { const hierarchy = this.getHierarchy(componentId); return hierarchy ? hierarchy.depth : 0; } /** * Get all ancestors (parent, grandparent, etc.) * * @param {string} componentId - Component ID * @returns {Array} Array of ancestor IDs (parent first, root last) */ getAncestors(componentId) { const hierarchy = this.getHierarchy(componentId); if (!hierarchy || !hierarchy.path) { return []; } // Path includes current component, remove it const ancestors = [...hierarchy.path]; ancestors.pop(); // Return in reverse order (parent first, root last) return ancestors.reverse(); } /** * Bubble event up through component hierarchy * * Dispatches custom event to each ancestor until stopped or root reached. * * @param {string} sourceId - Component that dispatched the event * @param {string} eventName - Event name * @param {Object} payload - Event payload * @returns {boolean} True if bubbled to root, false if stopped */ bubbleEvent(sourceId, eventName, payload) { console.log(`[NestedComponents] Bubbling event: ${eventName} from ${sourceId}`, payload); let currentId = sourceId; let bubbled = 0; while (true) { const parentId = this.getParentId(currentId); // Reached root if (parentId === null) { console.log(`[NestedComponents] Event bubbled to root (${bubbled} levels)`); return true; } // Get parent element const parentElement = document.querySelector(`[data-live-component="${parentId}"]`); if (!parentElement) { console.warn(`[NestedComponents] Parent element not found: ${parentId}`); return false; } // Dispatch custom event to parent const bubbleEvent = new CustomEvent(`livecomponent:child:${eventName}`, { detail: { sourceId, eventName, payload, currentLevel: bubbled }, bubbles: false, // We handle bubbling manually cancelable: true }); const dispatched = parentElement.dispatchEvent(bubbleEvent); // Event was cancelled - stop bubbling if (!dispatched) { console.log(`[NestedComponents] Event bubbling stopped at ${parentId}`); return false; } // Check for registered callbacks const callbacks = this.bubbleCallbacks.get(parentId); if (callbacks) { for (const callback of callbacks) { const shouldContinue = callback(sourceId, eventName, payload); if (shouldContinue === false) { console.log(`[NestedComponents] Event bubbling stopped by callback at ${parentId}`); return false; } } } // Move to next level currentId = parentId; bubbled++; } } /** * Register callback for child events * * @param {string} parentId - Parent component ID * @param {Function} callback - Callback function (sourceId, eventName, payload) => boolean */ onChildEvent(parentId, callback) { if (!this.bubbleCallbacks.has(parentId)) { this.bubbleCallbacks.set(parentId, []); } this.bubbleCallbacks.get(parentId).push(callback); console.log(`[NestedComponents] Registered child event callback for ${parentId}`); } /** * Sync state from parent to children * * Useful for broadcasting shared state to all children. * * @param {string} parentId - Parent component ID * @param {Object} sharedState - State to share with children */ syncStateToChildren(parentId, sharedState) { const childIds = this.getChildIds(parentId); console.log(`[NestedComponents] Syncing state to ${childIds.length} children of ${parentId}`); childIds.forEach(childId => { const childElement = document.querySelector(`[data-live-component="${childId}"]`); if (!childElement) return; // Dispatch state sync event childElement.dispatchEvent(new CustomEvent('livecomponent:parent:state-sync', { detail: { parentId, sharedState } })); // Update child state if applicable // Child components can listen to this event and update accordingly }); } /** * Update all children when parent changes * * @param {string} parentId - Parent component ID * @param {Object} updates - Updates to apply to children */ async updateChildren(parentId, updates) { const childIds = this.getChildIds(parentId); console.log(`[NestedComponents] Updating ${childIds.length} children of ${parentId}`); // Update children in parallel for performance const updatePromises = childIds.map(async childId => { const childElement = document.querySelector(`[data-live-component="${childId}"]`); if (!childElement) return; // Trigger child component update via LiveComponent action // This depends on how your components handle updates // For now, just dispatch an event childElement.dispatchEvent(new CustomEvent('livecomponent:parent:update', { detail: { parentId, updates } })); }); await Promise.all(updatePromises); } /** * Unregister component and cleanup * * @param {string} componentId - Component to unregister */ unregister(componentId) { // Remove from hierarchy registry const hierarchy = this.hierarchyRegistry.get(componentId); this.hierarchyRegistry.delete(componentId); // Remove from parent's children if (hierarchy && hierarchy.parentId) { const siblings = this.childrenRegistry.get(hierarchy.parentId); if (siblings) { const index = siblings.indexOf(componentId); if (index !== -1) { siblings.splice(index, 1); } } } // Remove children registry this.childrenRegistry.delete(componentId); // Remove bubble callbacks this.bubbleCallbacks.delete(componentId); console.log(`[NestedComponents] Unregistered: ${componentId}`); } /** * Get hierarchy statistics * * @returns {Object} Statistics */ getStats() { let rootCount = 0; let maxDepth = 0; this.hierarchyRegistry.forEach(hierarchy => { if (hierarchy.parentId === null) { rootCount++; } maxDepth = Math.max(maxDepth, hierarchy.depth); }); return { total_components: this.hierarchyRegistry.size, root_components: rootCount, child_components: this.hierarchyRegistry.size - rootCount, max_nesting_depth: maxDepth, parents_with_children: this.childrenRegistry.size }; } /** * Destroy nested component handler */ destroy() { this.hierarchyRegistry.clear(); this.childrenRegistry.clear(); this.bubbleCallbacks.clear(); console.log('[NestedComponents] Destroyed'); } } export default NestedComponentHandler;