Files
michaelschiemer/docs/livecomponents/ui-integration-guide.md
2025-11-24 21:28:25 +01:00

26 KiB

LiveComponents UI Integration Guide

Complete Guide to UI Features: Tooltips, Loading States, Dialogs, and Notifications

This guide covers the integrated UI features available in LiveComponents, including tooltips, skeleton loading, dialogs, modals, notifications, and loading states.


Table of Contents

  1. Tooltip System
  2. Loading States & Skeleton Loading
  3. UI Helper System
  4. Event-Based UI Integration
  5. Notification Component
  6. Dialog & Modal Integration
  7. Best Practices

Tooltip System

Overview

The Tooltip System provides automatic tooltip initialization and management for LiveComponent elements. Tooltips are automatically initialized when components are mounted and cleaned up when components are destroyed.

Features:

  • Automatic initialization for data-tooltip attributes
  • Accessibility support (ARIA attributes)
  • Smart positioning (viewport-aware)
  • Validation error tooltips
  • Automatic cleanup

Basic Usage

<!-- Simple tooltip -->
<button
    data-lc-action="save"
    data-tooltip="Save your changes"
>
    Save
</button>

<!-- Tooltip with validation error -->
<input
    type="email"
    data-lc-action="validateEmail"
    data-tooltip="Enter a valid email address"
    data-tooltip-error="Invalid email format"
/>

Server-Side Validation Tooltips

use App\Framework\LiveComponents\Attributes\Action;
use App\Framework\LiveComponents\ValueObjects\LiveComponentError;

final class UserForm extends LiveComponent
{
    #[Action]
    public function validateEmail(string $email): void
    {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            // Error will automatically show tooltip if element has data-tooltip-error
            throw LiveComponentError::validation(
                'Invalid email format',
                ['field' => 'email'],
                $this->id->toString(),
                'validateEmail'
            );
        }
    }
}

Custom Tooltip Configuration

// Configure tooltip behavior globally
import { tooltipManager } from './modules/livecomponent/TooltipManager.js';

// Adjust delays
tooltipManager.tooltipDelay = 500; // Show after 500ms
tooltipManager.hideDelay = 200;   // Hide after 200ms

Tooltip Events

// Listen for tooltip events
window.addEventListener('livecomponent:tooltip-shown', (e) => {
    const { element, tooltipText } = e.detail;
    console.log(`Tooltip shown: ${tooltipText}`);
});

window.addEventListener('livecomponent:tooltip-hidden', (e) => {
    const { element } = e.detail;
    console.log('Tooltip hidden');
});

Loading States & Skeleton Loading

Overview

The Loading State System provides configurable loading indicators during LiveComponent actions, including skeleton loaders, spinners, and progress indicators.

Features:

  • Fragment-specific loading
  • Configurable loading types (skeleton, spinner, progress, none)
  • Smooth transitions
  • Optimistic UI integration (no loading for optimistic actions)
  • Per-component configuration

Basic Usage

<!-- Component with skeleton loading -->
<div
    data-live-component="product-list"
    data-loading-type="skeleton"
    data-loading-fragments="product-list"
>
    <div data-lc-fragment="product-list">
        <!-- Skeleton template will be shown during loading -->
        <div class="skeleton-item">
            <div class="skeleton-image"></div>
            <div class="skeleton-text"></div>
        </div>
    </div>
</div>

Server-Side Loading Configuration

use App\Framework\LiveComponents\Attributes\Action;
use App\Framework\LiveComponents\Attributes\Loading;

final class ProductList extends LiveComponent
{
    #[Action]
    #[Loading(type: 'skeleton', fragments: ['product-list'], showDelay: 150)]
    public function loadProducts(string $category): void
    {
        $this->products = $this->productService->getByCategory($category);
    }

    #[Action]
    #[Loading(type: 'spinner', showDelay: 0)] // Show immediately
    public function quickAction(): void
    {
        // Fast action with immediate spinner
    }
}

Loading Types

1. Skeleton Loading

<!-- Skeleton template in component -->
<div data-lc-fragment="content">
    <!-- Default content -->
    <div class="product-card">
        <img src="{product.image}" />
        <h3>{product.name}</h3>
    </div>

    <!-- Skeleton template (shown during loading) -->
    <template data-skeleton-template>
        <div class="product-card skeleton">
            <div class="skeleton-image"></div>
            <div class="skeleton-text"></div>
        </div>
    </template>
</div>

2. Spinner Loading

<!-- Automatic spinner overlay -->
<div
    data-live-component="component-id"
    data-loading-type="spinner"
>
    <!-- Spinner automatically appears during actions -->
</div>

3. Progress Loading

<!-- Progress bar for long-running actions -->
<div
    data-live-component="upload-component"
    data-loading-type="progress"
>
    <div class="progress-bar" data-progress="0"></div>
</div>

Custom Loading Configuration

// Configure loading behavior per component
const component = LiveComponentManager.getInstance().getComponent('component-id');

component.setLoadingConfig({
    type: 'skeleton',
    fragments: ['content', 'sidebar'],
    showDelay: 150,
    hideDelay: 100
});

Loading Events

// Listen for loading state changes
window.addEventListener('livecomponent:loading-started', (e) => {
    const { componentId, type, fragments } = e.detail;
    console.log(`Loading started: ${componentId} (${type})`);
});

window.addEventListener('livecomponent:loading-finished', (e) => {
    const { componentId, duration } = e.detail;
    console.log(`Loading finished: ${componentId} (${duration}ms)`);
});

UI Helper System

Overview

The UI Helper System provides a standardized way for LiveComponents to interact with common UI elements like dialogs, modals, and notifications.

Features:

  • Unified API for UI components
  • Integration with UIManager
  • Promise-based API
  • Automatic cleanup
  • Component-scoped UI elements

Dialog & Modal Helpers

use App\Framework\LiveComponents\Attributes\Action;

final class UserManagement extends LiveComponent
{
    #[Action]
    public function showDeleteConfirm(int $userId): void
    {
        // Show confirmation dialog via UI Helper
        $this->uiHelper->showDialog(
            title: 'Delete User',
            message: 'Are you sure you want to delete this user?',
            buttons: [
                ['label' => 'Cancel', 'action' => 'cancel'],
                ['label' => 'Delete', 'action' => 'confirm', 'variant' => 'danger']
            ]
        )->then(function($action) use ($userId) {
            if ($action === 'confirm') {
                $this->deleteUser($userId);
            }
        });
    }
}

JavaScript API

import { LiveComponentUIHelper } from './modules/livecomponent/LiveComponentUIHelper.js';

const uiHelper = new LiveComponentUIHelper(liveComponentManager);

// Show modal
const action = await uiHelper.showModal('component-id', {
    title: 'Confirm Action',
    content: '<p>Are you sure?</p>',
    size: 'medium',
    buttons: [
        { label: 'Cancel', action: 'cancel' },
        { label: 'Confirm', action: 'confirm', variant: 'primary' }
    ]
});

if (action === 'confirm') {
    // Handle confirmation
}

// Show alert
await uiHelper.showAlert('component-id', {
    title: 'Success',
    message: 'Operation completed successfully',
    type: 'success'
});

// Show confirm dialog
const confirmed = await uiHelper.showConfirm('component-id', {
    title: 'Delete Item',
    message: 'This action cannot be undone.',
    confirmText: 'Delete',
    cancelText: 'Cancel'
});

Notification Helpers

// Show notification
uiHelper.showNotification('component-id', 'Operation successful', 'success');

// Show error notification with retry
uiHelper.showErrorNotification(
    'component-id',
    'Upload failed. Please try again.',
    'error',
    true, // Can retry
    () => {
        // Retry logic
        retryUpload();
    }
);

// Hide notification
uiHelper.hideNotification('component-id');

Event-Based UI Integration

Overview

The Event-Based UI Integration system allows LiveComponents to trigger UI components (Toasts, Modals) by dispatching events from PHP actions. The JavaScript UIEventHandler automatically listens to these events and displays the appropriate UI components.

Features:

  • Automatic UI component display from PHP events
  • Type-safe event payloads
  • Toast queue management
  • Modal stack management
  • Zero JavaScript code required in components

Architecture

  • PHP Side: Components dispatch events via ComponentEventDispatcher
  • JavaScript Side: UIEventHandler listens to events and displays UI components
  • Integration: Events are automatically dispatched after action execution

Basic Usage with UIHelper

The UIHelper class provides convenient methods for dispatching UI events:

<?php

declare(strict_types=1);

namespace App\Application\LiveComponents\Product;

use App\Framework\LiveComponents\Attributes\Action;
use App\Framework\LiveComponents\Attributes\LiveComponent;
use App\Framework\LiveComponents\ComponentEventDispatcher;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
use App\Framework\LiveComponents\UI\UIHelper;
use App\Framework\LiveComponents\ValueObjects\ComponentId;

#[LiveComponent('product-form')]
final readonly class ProductFormComponent implements LiveComponentContract
{
    public function __construct(
        public ComponentId $id,
        public ProductFormState $state
    ) {
    }

    #[Action]
    public function save(?ComponentEventDispatcher $events = null): ProductFormState
    {
        // ... save logic ...

        // Show success toast
        (new UIHelper($events))->successToast('Product saved successfully!');

        return $this->state->withSaved();
    }

    #[Action]
    public function delete(int $productId, ?ComponentEventDispatcher $events = null): ProductFormState
    {
        // Show confirmation dialog with action
        (new UIHelper($events))->confirmDelete(
            $this->id,
            "product #{$productId}",
            'doDelete',
            ['id' => $productId]
        );

        return $this->state;
    }

    #[Action]
    public function doDelete(int $id): ProductFormState
    {
        // This action is automatically called when user confirms
        // ... delete logic ...
        return $this->state->withoutProduct($id);
    }
}

Toast Events

Show Toast

use App\Framework\LiveComponents\UI\UIHelper;

#[Action]
public function save(?ComponentEventDispatcher $events = null): State
{
    // Basic success toast
    (new UIHelper($events))->successToast('Saved successfully!');

    // Toast with custom duration
    (new UIHelper($events))->infoToast('File uploaded', 7000);

    // Using fluent interface for multiple toasts
    (new UIHelper($events))
        ->infoToast('Processing...')
        ->successToast('Done!');

    return $this->state;
}

Toast Types:

  • info - Blue, informational message
  • success - Green, success message
  • warning - Orange, warning message
  • error - Red, error message

Positions:

  • top-right (default)
  • top-left
  • bottom-right
  • bottom-left

Hide Toast

#[Action]
public function dismissNotification(?ComponentEventDispatcher $events = null): State
{
    $this->hideToast($events, 'global');
    return $this->state;
}

Modal Events

Show Modal

use App\Framework\LiveComponents\UI\UIHelper;
use App\Framework\LiveComponents\Events\UI\Options\ModalOptions;
use App\Framework\LiveComponents\Events\UI\Enums\ModalSize;

#[Action]
public function showEditForm(int $userId, ?ComponentEventDispatcher $events = null): State
{
    $formHtml = $this->renderEditForm($userId);

    (new UIHelper($events))->modal(
        $this->id,
        'Edit User',
        $formHtml,
        ModalOptions::create()
            ->withSize(ModalSize::Large)
            ->withButtons([
                ['text' => 'Save', 'class' => 'btn-primary', 'action' => 'save'],
                ['text' => 'Cancel', 'class' => 'btn-secondary', 'action' => 'close']
            ])
    );

    return $this->state;
}

Show Confirmation Dialog

use App\Framework\LiveComponents\UI\UIHelper;

#[Action]
public function requestDelete(int $id, ?ComponentEventDispatcher $events = null): State
{
    (new UIHelper($events))->confirmDelete(
        $this->id,
        "item #{$id}",
        'doDelete',
        ['id' => $id]
    );

    return $this->state;
}

Note: Confirmation dialogs return a result via the modal:confirm:result event. You can listen to this event in JavaScript to handle the confirmation:

document.addEventListener('modal:confirm:result', (e) => {
    const { componentId, confirmed } = e.detail;
    if (confirmed) {
        // User confirmed - execute action
        LiveComponentManager.getInstance()
            .executeAction(componentId, 'confirmDelete', { id: 123 });
    }
});

Show Alert Dialog

use App\Framework\LiveComponents\UI\UIHelper;

#[Action]
public function showError(?ComponentEventDispatcher $events = null): State
{
    (new UIHelper($events))->alertError(
        $this->id,
        'Error',
        'An error occurred while processing your request.'
    );

    return $this->state;
}

Close Modal

use App\Framework\LiveComponents\UI\UIHelper;

#[Action]
public function closeModal(?ComponentEventDispatcher $events = null): State
{
    (new UIHelper($events))->closeModal($this->id);
    return $this->state;
}

Manual Event Dispatching

If you prefer to dispatch events manually (without UIHelper):

use App\Framework\LiveComponents\ComponentEventDispatcher;
use App\Framework\LiveComponents\Events\UI\ToastShowEvent;

#[Action]
public function save(?ComponentEventDispatcher $events = null): State
{
    // ... save logic ...

    if ($events !== null) {
        // Option 1: Using Event classes (recommended)
        $events->dispatchEvent(ToastShowEvent::success('Saved successfully!'));
        
        // Option 2: Direct instantiation
        $events->dispatchEvent(new ToastShowEvent(
            message: 'Saved successfully!',
            type: 'success',
            duration: 5000
        ));
    }

    return $this->state;
}

JavaScript Event Handling

The UIEventHandler automatically listens to these events and displays UI components. You can also listen to events manually:

// Listen to toast events
document.addEventListener('toast:show', (e) => {
    const { message, type, duration, position } = e.detail;
    console.log(`Toast: ${message} (${type})`);
});

// Listen to modal events
document.addEventListener('modal:show', (e) => {
    const { componentId, title, content } = e.detail;
    console.log(`Modal shown: ${title}`);
});

// Listen to confirmation results
document.addEventListener('modal:confirm:result', (e) => {
    const { componentId, confirmed } = e.detail;
    if (confirmed) {
        // Handle confirmation
    }
});

Toast Queue Management

The ToastQueue automatically manages multiple toasts:

  • Maximum 5 toasts per position (configurable)
  • Automatic stacking with spacing
  • Oldest toast removed when limit reached
  • Auto-dismiss with queue management

Modal Stack Management

The ModalManager handles modal stacking using native <dialog> element:

  • Native <dialog> element: Uses showModal() for true modal behavior
  • Automatic backdrop: Native ::backdrop pseudo-element
  • Automatic focus management: Browser handles focus trapping
  • Automatic ESC handling: Native cancel event
  • Z-index management: Manual stacking for nested modals
  • Stack tracking: Manages multiple open modals

Native <dialog> Benefits

The <dialog> element provides:

  • True modal behavior: Blocks background interaction via showModal()
  • Native backdrop: Automatic overlay with ::backdrop pseudo-element
  • Automatic focus trapping: Browser handles focus management
  • Automatic ESC handling: Native cancel event
  • Better accessibility: Native ARIA attributes and semantics
  • Better performance: Native browser implementation
  • Wide browser support: Chrome 37+, Firefox 98+, Safari 15.4+

The system uses native <dialog> features for optimal modal behavior and accessibility.

Best Practices

  1. Use UIHelper: Prefer the UIHelper class over manual event dispatching for type safety and convenience
  2. Component IDs: Always provide component IDs for modals to enable proper management
  3. Toast Duration: Use appropriate durations (5s for success, longer for errors)
  4. Modal Sizes: Choose appropriate sizes (small for alerts, large for forms)
  5. Error Handling: Always show user-friendly error messages
// Good: Clear, user-friendly toast
(new UIHelper($events))->successToast('Product saved successfully!');

// Bad: Technical error message
(new UIHelper($events))->errorToast('SQL Error: INSERT failed');

Notification Component

Overview

The NotificationComponent is a full-featured LiveComponent for displaying toast notifications with support for different types, positions, durations, and action buttons.

Features:

  • Type-safe state management
  • Multiple notification types (info, success, warning, error)
  • Configurable positions (top-right, top-left, bottom-right, bottom-left)
  • Auto-dismiss with duration
  • Action buttons
  • Icon support

Basic Usage

use App\Application\LiveComponents\Notification\NotificationComponent;
use App\Framework\LiveComponents\ValueObjects\ComponentId;

final class ProductController
{
    public function create(): ViewResult
    {
        $notification = NotificationComponent::mount(
            ComponentId::generate('notification'),
            message: 'Product created successfully',
            type: 'success',
            duration: 5000
        );

        return new ViewResult('product/index', [
            'notification' => $notification
        ]);
    }
}

Template Integration

<!-- Include notification component in layout -->
{notification}

<!-- Or use in component template -->
<div data-live-component="{notification.id}">
    <!-- Notification will render here -->
</div>

Server-Side Actions

final class NotificationExample extends LiveComponent
{
    #[Action]
    public function showSuccess(): NotificationState
    {
        return NotificationState::empty()
            ->withMessage('Operation successful!', 'success')
            ->show();
    }

    #[Action]
    public function showError(string $message): NotificationState
    {
        return NotificationState::empty()
            ->withMessage($message, 'error')
            ->show();
    }

    #[Action]
    public function showWithAction(): NotificationState
    {
        return new NotificationState(
            message: 'File uploaded successfully',
            type: 'success',
            isVisible: true,
            actionText: 'View',
            actionUrl: '/files'
        );
    }
}

Client-Side API

// Show notification via LiveComponent action
liveComponentManager.executeAction('notification-id', 'showNotification', {
    message: 'Operation successful',
    type: 'success',
    duration: 5000
});

// Hide notification
liveComponentManager.executeAction('notification-id', 'hide');

Notification Types

// Info notification
$notification = NotificationState::empty()
    ->withMessage('New update available', 'info')
    ->show();

// Success notification
$notification = NotificationState::empty()
    ->withMessage('Changes saved', 'success')
    ->show();

// Warning notification
$notification = NotificationState::empty()
    ->withMessage('Low disk space', 'warning')
    ->show();

// Error notification
$notification = NotificationState::empty()
    ->withMessage('Upload failed', 'error')
    ->show();

Notification Positions

$notification = new NotificationState(
    message: 'Notification message',
    type: 'info',
    position: 'top-right', // or 'top-left', 'bottom-right', 'bottom-left'
    isVisible: true
);

Notification with Action Button

$notification = new NotificationState(
    message: 'File ready for download',
    type: 'success',
    isVisible: true,
    actionText: 'Download',
    actionUrl: '/download/file.pdf'
);

Dialog & Modal Integration

Overview

LiveComponents integrate seamlessly with the UIManager for dialogs and modals, providing a consistent API across the application.

Basic Modal Usage

use App\Framework\LiveComponents\Attributes\Action;

final class UserSettings extends LiveComponent
{
    #[Action]
    public function showEditModal(int $userId): void
    {
        // Modal will be shown via UI Helper
        $this->uiHelper->showModal(
            title: 'Edit User',
            content: $this->renderEditForm($userId),
            size: 'large',
            buttons: [
                ['label' => 'Save', 'action' => 'save', 'variant' => 'primary'],
                ['label' => 'Cancel', 'action' => 'cancel']
            ]
        );
    }
}

Modal with LiveComponent Content

use App\Framework\LiveComponents\UI\UIHelper;
use App\Framework\LiveComponents\Events\UI\Options\ModalOptions;
use App\Framework\LiveComponents\Events\UI\Enums\ModalSize;

#[Action]
public function showUserModal(int $userId, ?ComponentEventDispatcher $events = null): void
{
    $userComponent = UserDetailsComponent::mount(
        ComponentId::generate('user-details'),
        userId: $userId
    );

    (new UIHelper($events))->modal(
        $this->id,
        'User Details',
        $userComponent->render(),
        ModalOptions::create()->withSize(ModalSize::Medium)
    );
}

Modal Events

// Listen for modal events
window.addEventListener('livecomponent:modal-opened', (e) => {
    const { componentId, modalInstance } = e.detail;
    console.log(`Modal opened for component: ${componentId}`);
});

window.addEventListener('livecomponent:modal-closed', (e) => {
    const { componentId, action } = e.detail;
    console.log(`Modal closed with action: ${action}`);
});

Best Practices

1. Tooltip Usage

  • Do: Use tooltips for helpful context and validation errors
  • Don't: Overuse tooltips - they can be distracting
  • Accessibility: Always ensure tooltips are keyboard-accessible
<!-- Good: Helpful tooltip -->
<button data-tooltip="Save your changes (Ctrl+S)">
    Save
</button>

<!-- Bad: Obvious tooltip -->
<button data-tooltip="Click to save">
    Save
</button>

2. Loading States

  • Do: Use skeleton loading for content-heavy updates
  • Do: Use spinners for quick actions (< 500ms)
  • Don't: Show loading for optimistic UI updates
  • Do: Configure appropriate delays to prevent flickering
// Good: Appropriate loading type
#[Loading(type: 'skeleton', fragments: ['product-list'])]
public function loadProducts(): void { }

// Good: Quick action with spinner
#[Loading(type: 'spinner', showDelay: 0)]
public function toggleFavorite(): void { }

3. Notifications

  • Do: Use notifications for important feedback
  • Don't: Overuse notifications - they can be annoying
  • Do: Set appropriate durations (5s for success, longer for errors)
  • Do: Provide action buttons for actionable notifications
// Good: Clear, actionable notification
$notification = new NotificationState(
    message: 'File uploaded successfully',
    type: 'success',
    duration: 5000,
    actionText: 'View',
    actionUrl: '/files'
);

// Bad: Too many notifications
// Don't show a notification for every minor action

4. Modals & Dialogs

  • Do: Use modals for important confirmations
  • Don't: Overuse modals - they interrupt user flow
  • Do: Provide clear action buttons
  • Do: Support keyboard navigation (Escape to close)
// Good: Clear confirmation dialog
$this->uiHelper->showConfirm(
    title: 'Delete Item',
    message: 'This action cannot be undone.',
    confirmText: 'Delete',
    cancelText: 'Cancel'
);

5. Error Handling

  • Do: Use ErrorBoundary for automatic error handling
  • Do: Show user-friendly error messages
  • Do: Provide retry options for recoverable errors
  • Don't: Show technical error details to users
// Good: User-friendly error
throw LiveComponentError::validation(
    'Please enter a valid email address',
    ['field' => 'email'],
    $this->id->toString()
);

// Bad: Technical error
throw new \Exception('Invalid email format: ' . $email);

Configuration

Global Configuration

import { sharedConfig } from './modules/livecomponent/SharedConfig.js';

// Configure default values
sharedConfig.defaultDebounce = 300;
sharedConfig.defaultCacheTTL = 5000;
sharedConfig.defaultLoadingShowDelay = 150;
sharedConfig.defaultLoadingType = 'skeleton';
sharedConfig.defaultNotificationDuration = 5000;
sharedConfig.defaultNotificationPosition = 'top-right';
sharedConfig.defaultModalAnimation = 'fade';

Per-Component Configuration

<!-- Component-level configuration -->
<div
    data-live-component="component-id"
    data-loading-type="skeleton"
    data-loading-show-delay="200"
    data-notification-position="bottom-right"
>
    <!-- Component content -->
</div>

Troubleshooting

Tooltips Not Showing

  1. Check that data-tooltip attribute is present
  2. Verify TooltipManager is initialized
  3. Check browser console for errors
  4. Ensure element is visible and not hidden

Loading States Not Working

  1. Verify data-loading-type attribute is set
  2. Check that fragments match between HTML and PHP
  3. Ensure LoadingStateManager is initialized
  4. Check for JavaScript errors in console

Notifications Not Displaying

  1. Verify NotificationComponent is mounted
  2. Check that component ID matches
  3. Ensure state is properly serialized
  4. Check browser console for errors

Modals Not Opening

  1. Verify UIManager is initialized
  2. Check that modal content is valid HTML
  3. Ensure no JavaScript errors are blocking execution
  4. Check z-index conflicts

Next: API Reference