Files
michaelschiemer/docs/claude/livecomponent-nested-components.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

718 lines
20 KiB
Markdown

# 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
<!-- todo-list.view.php -->
<div class="todo-list">
<!-- Parent UI -->
<h2>My Todos ({total_count})</h2>
<!-- Child Components -->
<for items="todos" as="todo">
<div
data-live-component="todo-item:{todo.id}"
data-parent-component="{component_id}"
data-nesting-depth="1"
>
<!-- TodoItemComponent renders here -->
</div>
</for>
</div>
```
### 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
<!-- todo-item.view.php -->
<div class="todo-item {completed|then:todo-item--completed}">
<button data-livecomponent-action="toggle">
<if condition="completed"></if>
</button>
<div class="todo-item__title">{title}</div>
<button data-livecomponent-action="delete"></button>
</div>
```
## 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
<!-- ✅ Correct -->
<div
data-live-component="child:1"
data-parent-component="parent:main"
data-nesting-depth="1"
>
</div>
```
### 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