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
- Tooltip System
- Loading States & Skeleton Loading
- UI Helper System
- Event-Based UI Integration
- Notification Component
- Dialog & Modal Integration
- 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-tooltipattributes - 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:
UIEventHandlerlistens 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 messagesuccess- Green, success messagewarning- Orange, warning messageerror- Red, error message
Positions:
top-right(default)top-leftbottom-rightbottom-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: UsesshowModal()for true modal behavior - Automatic backdrop: Native
::backdroppseudo-element - Automatic focus management: Browser handles focus trapping
- Automatic ESC handling: Native
cancelevent - 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
::backdroppseudo-element - Automatic focus trapping: Browser handles focus management
- Automatic ESC handling: Native
cancelevent - 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
- Use UIHelper: Prefer the UIHelper class over manual event dispatching for type safety and convenience
- Component IDs: Always provide component IDs for modals to enable proper management
- Toast Duration: Use appropriate durations (5s for success, longer for errors)
- Modal Sizes: Choose appropriate sizes (small for alerts, large for forms)
- 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
- Check that
data-tooltipattribute is present - Verify TooltipManager is initialized
- Check browser console for errors
- Ensure element is visible and not hidden
Loading States Not Working
- Verify
data-loading-typeattribute is set - Check that fragments match between HTML and PHP
- Ensure LoadingStateManager is initialized
- Check for JavaScript errors in console
Notifications Not Displaying
- Verify NotificationComponent is mounted
- Check that component ID matches
- Ensure state is properly serialized
- Check browser console for errors
Modals Not Opening
- Verify UIManager is initialized
- Check that modal content is valid HTML
- Ensure no JavaScript errors are blocking execution
- Check z-index conflicts
Next: API Reference →