# LiveComponents Nested Components System Comprehensive guide for building nested component hierarchies with parent-child relationships, event bubbling, and state synchronization. ## Overview The Nested Components System enables complex UI compositions through parent-child component relationships. Parents can manage global state while children handle localized behavior, with events bubbling up for coordination. ## Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ Parent Component │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Global State Management │ │ │ │ • Manages list of items │ │ │ │ • Provides data to children │ │ │ │ • Handles child events │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ┌────────────┴────────────┐ │ │ │ │ │ │ ┌─────────▼────────┐ ┌──────────▼────────┐ │ │ │ Child Component │ │ Child Component │ │ │ │ • Local State │ │ • Local State │ │ │ │ • Dispatch Events│ │ • Dispatch Events│ │ │ └──────────────────┘ └───────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ## Core Concepts ### 1. Component Hierarchy **ComponentHierarchy Value Object** - Represents parent-child relationships: ```php use App\Framework\LiveComponents\ValueObjects\ComponentHierarchy; use App\Framework\LiveComponents\ValueObjects\ComponentId; // Root component (no parent) $rootHierarchy = ComponentHierarchy::root(); // depth=0, path=[] // First-level child $childHierarchy = ComponentHierarchy::fromParent( parentId: ComponentId::fromString('parent:main'), childId: ComponentId::fromString('child:1') ); // depth=1, path=['parent:main', 'child:1'] // Add another level $grandchildHierarchy = $childHierarchy->withChild( ComponentId::fromString('grandchild:1') ); // depth=2, path=['parent:main', 'child:1', 'grandchild:1'] ``` **Hierarchy Queries:** ```php $hierarchy->isRoot(); // true if no parent $hierarchy->isChild(); // true if has parent $hierarchy->getLevel(); // nesting depth (0, 1, 2, ...) $hierarchy->isDescendantOf($componentId); // check ancestry ``` ### 2. NestedComponentManager **Server-Side Hierarchy Management:** ```php use App\Framework\LiveComponents\NestedComponentManager; $manager = new NestedComponentManager(); // Register root component $parentId = ComponentId::fromString('todo-list:main'); $manager->registerHierarchy($parentId, ComponentHierarchy::root()); // Register child $childId = ComponentId::fromString('todo-item:1'); $childHierarchy = ComponentHierarchy::fromParent($parentId, $childId); $manager->registerHierarchy($childId, $childHierarchy); // Query hierarchy $manager->hasChildren($parentId); // true $manager->getChildIds($parentId); // [ComponentId('todo-item:1')] $manager->getParentId($childId); // ComponentId('todo-list:main') $manager->isRoot($parentId); // true $manager->getDepth($childId); // 1 // Get all ancestors/descendants $ancestors = $manager->getAncestors($childId); // [parentId] $descendants = $manager->getDescendants($parentId); // [childId] // Statistics $stats = $manager->getStats(); // ['total_components' => 2, 'root_components' => 1, ...] ``` ### 3. SupportsNesting Interface **Parent components must implement this interface:** ```php use App\Framework\LiveComponents\Contracts\SupportsNesting; interface SupportsNesting { /** * Get list of child component IDs */ public function getChildComponents(): array; /** * Handle event from child component * * @return bool Return false to stop event bubbling, true to continue */ public function onChildEvent(ComponentId $childId, string $eventName, array $payload): bool; /** * Validate child component compatibility */ public function canHaveChild(ComponentId $childId): bool; } ``` ### 4. Event Bubbling **Events flow from child to parent:** ``` ┌─────────────────────────┐ │ Child Component │ │ • User clicks button │ │ • Dispatches event │ └───────────┬─────────────┘ │ Event Bubbles Up ▼ ┌─────────────────────────┐ │ Parent Component │ │ • Receives event │ │ • Updates state │ │ • Re-renders children │ └─────────────────────────┘ ``` **Event Dispatcher:** ```php use App\Framework\LiveComponents\NestedComponentEventDispatcher; $dispatcher = new NestedComponentEventDispatcher(); // Child dispatches event $dispatcher->dispatch( componentId: ComponentId::fromString('todo-item:1'), eventName: 'todo-completed', payload: [ 'todo_id' => '1', 'completed' => true ] ); // Check dispatched events $dispatcher->hasEvents(); // true $dispatcher->count(); // 1 $events = $dispatcher->getEvents(); ``` ## Implementation Guide ### Creating a Parent Component **1. Implement SupportsNesting:** ```php use App\Framework\LiveComponents\Contracts\LiveComponentContract; use App\Framework\LiveComponents\Contracts\SupportsNesting; use App\Framework\LiveComponents\Attributes\LiveComponent; #[LiveComponent('todo-list')] final readonly class TodoListComponent implements LiveComponentContract, SupportsNesting { private ComponentId $id; private TodoListState $state; public function __construct( ComponentId $id, ?ComponentData $initialData = null, array $todos = [] ) { $this->id = $id; $this->state = $initialData ? TodoListState::fromComponentData($initialData) : new TodoListState(todos: $todos); } // LiveComponentContract methods public function getId(): ComponentId { return $this->id; } public function getData(): ComponentData { return $this->state->toComponentData(); } public function getRenderData(): ComponentRenderData { /* ... */ } // SupportsNesting methods public function getChildComponents(): array { // Return array of child component IDs $childIds = []; foreach ($this->state->todos as $todo) { $childIds[] = "todo-item:{$todo['id']}"; } return $childIds; } public function onChildEvent(ComponentId $childId, string $eventName, array $payload): bool { // Handle events from children match ($eventName) { 'todo-completed' => $this->handleTodoCompleted($payload), 'todo-deleted' => $this->handleTodoDeleted($payload), default => null }; return true; // Continue bubbling } public function canHaveChild(ComponentId $childId): bool { // Only accept TodoItem components return str_starts_with($childId->name, 'todo-item'); } private function handleTodoCompleted(array $payload): void { $todoId = $payload['todo_id']; $completed = $payload['completed']; // Log or trigger side effects error_log("Todo {$todoId} marked as " . ($completed ? 'completed' : 'active')); // Note: State updates happen through Actions, not event handlers // Event handlers are for logging, analytics, side effects } } ``` **2. Create Parent State:** ```php final readonly class TodoListState { public function __construct( public array $todos = [], public string $filter = 'all' ) {} public static function fromComponentData(ComponentData $data): self { $array = $data->toArray(); return new self( todos: $array['todos'] ?? [], filter: $array['filter'] ?? 'all' ); } public function toComponentData(): ComponentData { return ComponentData::fromArray([ 'todos' => $this->todos, 'filter' => $this->filter ]); } public function withTodoAdded(array $todo): self { return new self( todos: [...$this->todos, $todo], filter: $this->filter ); } // More transformation methods... } ``` **3. Create Parent Template:** ```html

My Todos ({total_count})

``` ### Creating a Child Component **1. Implement Component with Event Dispatcher:** ```php use App\Framework\LiveComponents\NestedComponentEventDispatcher; #[LiveComponent('todo-item')] final readonly class TodoItemComponent implements LiveComponentContract { private ComponentId $id; private TodoItemState $state; public function __construct( ComponentId $id, private NestedComponentEventDispatcher $eventDispatcher, ?ComponentData $initialData = null, ?array $todoData = null ) { $this->id = $id; $this->state = $initialData ? TodoItemState::fromComponentData($initialData) : TodoItemState::fromTodoArray($todoData ?? []); } #[Action] public function toggle(): ComponentData { $newState = $this->state->withToggled(); // Dispatch event to parent $this->eventDispatcher->dispatch( componentId: $this->id, eventName: 'todo-completed', payload: [ 'todo_id' => $this->state->id, 'completed' => $newState->completed ] ); return $newState->toComponentData(); } #[Action] public function delete(): ComponentData { // Dispatch delete event to parent $this->eventDispatcher->dispatch( componentId: $this->id, eventName: 'todo-deleted', payload: ['todo_id' => $this->state->id] ); return $this->state->toComponentData(); } } ``` **2. Create Child State:** ```php final readonly class TodoItemState { public function __construct( public string $id, public string $title, public bool $completed = false, public int $createdAt = 0 ) {} public static function fromTodoArray(array $todo): self { return new self( id: $todo['id'] ?? '', title: $todo['title'] ?? '', completed: $todo['completed'] ?? false, createdAt: $todo['created_at'] ?? time() ); } public function withToggled(): self { return new self( id: $this->id, title: $this->title, completed: !$this->completed, createdAt: $this->createdAt ); } } ``` **3. Create Child Template:** ```html
{title}
``` ## Client-Side Integration **Automatic Initialization:** ```javascript // NestedComponentHandler automatically initializes with LiveComponentManager import { LiveComponentManager } from './livecomponent/index.js'; // Scans DOM for nested components const nestedHandler = liveComponentManager.nestedHandler; // Get hierarchy info const parentId = nestedHandler.getParentId('todo-item:1'); // 'todo-list:main' const childIds = nestedHandler.getChildIds('todo-list:main'); // ['todo-item:1', ...] // Event bubbling nestedHandler.bubbleEvent('todo-item:1', 'todo-completed', { todo_id: '1', completed: true }); // Statistics const stats = nestedHandler.getStats(); // { total_components: 5, root_components: 1, max_nesting_depth: 2, ... } ``` ## Best Practices ### 1. State Management **✅ Parent owns the data:** ```php // Parent manages list private TodoListState $state; // Contains all todos // Child manages display state only private TodoItemState $state; // id, title, completed, isEditing ``` **❌ Don't duplicate state:** ```php // Bad: Both parent and child store todo data // This leads to synchronization issues ``` ### 2. Event Handling **✅ Use event handlers for side effects:** ```php public function onChildEvent(ComponentId $childId, string $eventName, array $payload): bool { // ✅ Logging error_log("Child event: {$eventName}"); // ✅ Analytics $this->analytics->track($eventName, $payload); // ✅ External system updates $this->cache->invalidate($payload['todo_id']); return true; // Continue bubbling } ``` **❌ Don't modify state in event handlers:** ```php // ❌ Bad: Event handlers shouldn't modify component state // State changes happen through Actions that return new ComponentData ``` ### 3. Child Compatibility **✅ Validate child types:** ```php public function canHaveChild(ComponentId $childId): bool { // Only accept specific component types return str_starts_with($childId->name, 'todo-item'); } ``` ### 4. Circular Dependencies **✅ Framework automatically prevents:** ```php // This will throw InvalidArgumentException: $manager->registerHierarchy($componentId, $hierarchy); // "Circular dependency detected: Component cannot be its own ancestor" ``` ## Performance Considerations ### Hierarchy Depth - **Recommended:** Max 3-4 levels deep - **Reason:** Each level adds overhead for event bubbling - **Alternative:** Flatten hierarchy when possible ### Event Bubbling - **Cost:** O(depth) for each event - **Optimization:** Stop bubbling early when not needed - **Pattern:** Return `false` from `onChildEvent()` to stop ```php public function onChildEvent(ComponentId $childId, string $eventName, array $payload): bool { if ($eventName === 'internal-event') { // Handle locally, don't bubble further return false; } // Let other events bubble return true; } ``` ### State Synchronization - **Pattern:** Parent as single source of truth - **Benefit:** Avoids synchronization bugs - **Trade-off:** More re-renders, but simpler logic ## Testing ### Unit Tests ```php describe('NestedComponentManager', function () { it('tracks parent-child relationships', function () { $manager = new NestedComponentManager(); $parentId = ComponentId::fromString('parent:1'); $childId = ComponentId::fromString('child:1'); $manager->registerHierarchy($parentId, ComponentHierarchy::root()); $manager->registerHierarchy( $childId, ComponentHierarchy::fromParent($parentId, $childId) ); expect($manager->hasChildren($parentId))->toBeTrue(); expect($manager->getParentId($childId))->toEqual($parentId); }); it('prevents circular dependencies', function () { $manager = new NestedComponentManager(); $id = ComponentId::fromString('self:1'); expect(fn() => $manager->registerHierarchy( $id, ComponentHierarchy::fromParent($id, $id) ))->toThrow(InvalidArgumentException::class); }); }); ``` ### Integration Tests ```php describe('TodoList with nested TodoItems', function () { it('handles child events', function () { $todoList = new TodoListComponent( id: ComponentId::fromString('todo-list:test'), todos: [ ['id' => '1', 'title' => 'Test', 'completed' => false] ] ); $childId = ComponentId::fromString('todo-item:1'); // Simulate child event $result = $todoList->onChildEvent( $childId, 'todo-completed', ['todo_id' => '1', 'completed' => true] ); expect($result)->toBeTrue(); // Event bubbled successfully }); }); ``` ## Troubleshooting ### Problem: Children not rendering **Cause:** Missing `data-parent-component` attribute **Solution:** ```html
``` ### Problem: Events not bubbling **Cause:** Wrong ComponentId or event name **Solution:** ```php // ✅ Use exact component ID $this->eventDispatcher->dispatch( componentId: $this->id, // ✅ Correct: use component's own ID eventName: 'todo-completed', payload: [...] ); ``` ### Problem: Circular dependency error **Cause:** Component trying to be its own ancestor **Solution:** ```php // ❌ Wrong: Same component as parent and child $hierarchy = ComponentHierarchy::fromParent($sameId, $sameId); // ✅ Correct: Different components $hierarchy = ComponentHierarchy::fromParent($parentId, $childId); ``` ## Advanced Patterns ### Multi-Level Nesting ```php // Grandparent → Parent → Child $grandparent = ComponentHierarchy::root(); $parent = ComponentHierarchy::fromParent( ComponentId::fromString('grandparent:1'), ComponentId::fromString('parent:1') ); $child = $parent->withChild( ComponentId::fromString('child:1') ); // depth=2, path=['grandparent:1', 'parent:1', 'child:1'] ``` ### Conditional Children ```php public function getChildComponents(): array { // Only show children if filter matches $filteredTodos = $this->state->getFilteredTodos(); return array_map( fn($todo) => "todo-item:{$todo['id']}", $filteredTodos ); } ``` ### Dynamic Child Addition ```php #[Action] public function addTodo(string $title): ComponentData { $newTodo = [ 'id' => uniqid('todo_', true), 'title' => $title, 'completed' => false ]; // State includes new todo $newState = $this->state->withTodoAdded($newTodo); // Framework automatically creates child component // based on getChildComponents() result return $newState->toComponentData(); } ``` ## Summary **Nested Components enable:** - ✅ Complex UI compositions - ✅ Parent-child communication via events - ✅ Hierarchical state management - ✅ Reusable component patterns - ✅ Type-safe relationships **Key Classes:** - `ComponentHierarchy` - Relationship value object - `NestedComponentManager` - Server-side hierarchy - `NestedComponentHandler` - Client-side hierarchy - `NestedComponentEventDispatcher` - Event bubbling - `SupportsNesting` - Parent component interface **Next Steps:** - Implement Slot System for flexible composition - Add SSE integration for real-time updates - Explore advanced caching strategies