Components and Their Usage
Components in Flute CMS include:
- Yoyo Components (
FluteComponent) for interactive logic without JS - Blade UI Components for consistent markup of forms and blocks (e.g.,
<x-card>,<x-forms.field>,<x-input>)
ModuleServiceProvider automatically registers Yoyo components from Components/ (kebab-case, without Component suffix). Thematic Blade components are registered in Template with cache.
Component Architecture
Base Class FluteComponent
<?php
namespace Flute\Core\Support;
use Clickfwd\Yoyo\Component;
use Flute\Core\Contracts\FluteComponentInterface;
abstract class FluteComponent extends Component implements FluteComponentInterface
{
public function validate(array $rules, ?array $data = null, array $messages = [])
{
// Component data validation
}
public function getValidatorErrors()
{
// Get validation errors
}
}Interface FluteComponentInterface
<?php
namespace Flute\Core\Contracts;
interface FluteComponentInterface
{
// Base contract for Flute Yoyo components
}Creating a Component
Component Structure
- YourComponent.php
Simple Component
<?php
namespace Flute\Modules\Blog\Components;
use Flute\Core\Support\FluteComponent;
class SimpleCounterComponent extends FluteComponent
{
// Public properties of the component
public int $count = 0;
// Private properties
protected string $title = 'Counter';
/**
* Increment counter
*/
public function increment()
{
$this->count++;
// Send event to browser
$this->emitEvent('counter-incremented', [
'newCount' => $this->count
]);
}
/**
* Reset counter
*/
public function reset()
{
$oldCount = $this->count;
$this->count = 0;
$this->emitEvent('counter-reset', [
'oldCount' => $oldCount
]);
}
/**
* Render component (use $this->view)
*/
public function render()
{
return $this->view('blog::components.simple-counter', [
'title' => $this->title,
'count' => $this->count
]);
}
}Component Template
{{-- resources/views/components/simple-counter.blade.php --}}
<div class="counter-component">
<h3>{{ $title }}</h3>
<div class="counter-display">
<span class="count">{{ $count }}</span>
</div>
<div class="counter-controls">
<button class="btn btn-primary"
yoyo:post="increment()">
<x-icon path="plus" />
Increment
</button>
<button class="btn btn-secondary"
yoyo:post="reset()">
<x-icon path="undo" />
Reset
</button>
</div>
</div>UI Components (Blade)
Cards
<x-card class="my-3">
<h3 class="mb-2">{{ __('def.title') }}</h3>
<p>{{ __('def.description') }}</p>
</x-card>Form Fields
<form method="POST" action="{{ route('example.store') }}">
@csrf
<x-forms.field class="mb-3">
<x-forms.label for="name" required>@t('def.name'):</x-forms.label>
<x-input name="name" id="name" :value="old('name')" />
@error('name')<div class="text-danger">{{ $message }}</div>@enderror
</x-forms.field>
<x-forms.field class="mb-3">
<x-forms.label for="category">@t('def.category'):</x-forms.label>
<select id="category" name="category" class="form-select">
@foreach($categories as $c)
<option value="{{ $c->id }}">{{ $c->name }}</option>
@endforeach
</select>
</x-forms.field>
<x-forms.field class="mb-3">
<x-forms.label for="content" required>@t('def.content'):</x-forms.label>
<textarea id="content" name="content" class="form-control" rows="6"></textarea>
</x-forms.field>
<button class="btn btn-primary" type="submit">@t('def.save')</button>
</form>Notes:
<x-input>— universal input (supportstype,mask,yoyo,multiple,filePond)<x-forms.field>— field container with label/hint/error- Use these components in admin panel and themes for UI consistency
<x-icon>— built-in icon component
Component Properties
Public and Private Properties
<?php
class ProductFormComponent extends FluteComponent
{
// Public properties (available in template and can be updated)
public string $productName = '';
public float $price = 0.0;
public array $categories = [];
public ?int $selectedCategory = null;
public bool $isActive = true;
// Private properties (only for internal logic)
private ProductService $productService;
private array $validationRules = [];
public function mount()
{
$this->productService = app(ProductService::class);
// Load categories
$this->categories = $this->productService->getCategories();
// Set default values
if (!$this->selectedCategory && count($this->categories) > 0) {
$this->selectedCategory = $this->categories[0]['id'];
}
}
}FluteComponent automatically populates public properties from the request; $props does not need to be set. Exceptions can be described in $excludesVariables.
Dynamic Properties
<?php
class DynamicFormComponent extends FluteComponent
{
public array $formFields = [];
/**
* Additional properties that can be created on the fly
*/
protected function getDynamicProperties(): array
{
return [
'customField1',
'customField2',
'dynamicValue'
];
}
public function addField()
{
$fieldId = 'field_' . count($this->formFields);
$this->formFields[] = $fieldId;
// Create dynamic property
$this->{$fieldId} = '';
}
public function removeField($fieldId)
{
if (($key = array_search($fieldId, $this->formFields)) !== false) {
unset($this->formFields[$key]);
unset($this->{$fieldId});
}
}
}Use dynamic properties carefully: they do not get autocomplete and complicate maintenance. Prefer explicit public properties.
Component Methods
Lifecycle Methods
<?php
class LifecycleComponent extends FluteComponent
{
public function mount()
{
// Called when component initializes
// Load data, set initial values here
$this->loadInitialData();
$this->setupValidationRules();
}
public function boot(array $variables, array $attributes)
{
// Called before mount
// Process incoming data here
parent::boot($variables, $attributes);
// Additional processing
$this->processInputData();
}
public function render()
{
// Called when rendering component
// Returns component view
// Check access rights
if (!$this->userCanView()) {
return $this->view('errors.access-denied');
}
return $this->view('components.lifecycle-example', [
'data' => $this->prepareRenderData()
]);
}
}Event Handlers
<?php
class EventHandlerComponent extends FluteComponent
{
public string $status = 'idle';
public array $messages = [];
/**
* Handle status change
*/
public function updatedStatus($newStatus)
{
// Called when status property changes
$this->messages[] = "Status changed to: {$newStatus}";
// Validate new value
if (!in_array($newStatus, ['idle', 'processing', 'completed', 'error'])) {
$this->status = 'idle';
$this->messages[] = "Invalid status";
}
// Emit event
$this->emitEvent('status-changed', [
'oldStatus' => $this->status,
'newStatus' => $newStatus
]);
}
/**
* Handle form submission
*/
public function submitForm()
{
$this->status = 'processing';
try {
// Validate data
$this->validateForm();
// Process data
$result = $this->processFormData();
$this->status = 'completed';
$this->messages[] = 'Form processed successfully';
// Redirect
$this->redirectTo('/success');
} catch (\Exception $e) {
$this->status = 'error';
$this->messages[] = $e->getMessage();
}
}
/**
* Handle button click
*/
public function handleButtonClick($buttonId)
{
$this->messages[] = "Button clicked: {$buttonId}";
// Perform action based on button
switch ($buttonId) {
case 'save':
$this->saveData();
break;
case 'delete':
$this->deleteData();
break;
case 'refresh':
$this->refreshData();
break;
}
}
}Validation in Components
Simple Validation
<?php
class ValidationComponent extends FluteComponent
{
public string $email = '';
public string $password = '';
public string $confirmPassword = '';
public function submitForm()
{
// Validate data
$isValid = $this->validate([
'email' => 'required|email',
'password' => 'required|min:8',
'confirmPassword' => 'required|same:password'
]);
if (!$isValid) {
// Get validation errors
$errors = $this->getValidatorErrors();
foreach ($errors->all() as $error) {
$this->flashMessage($error, 'error');
}
return;
}
// Process valid data
$this->processValidData();
}
}Advanced Validation
<?php
class AdvancedValidationComponent extends FluteComponent
{
public string $username = '';
public string $email = '';
public array $tags = [];
public function submitForm()
{
// Validation rules
$rules = [
'username' => 'required|string|min-str-len:3|max-str-len:20|unique:users,username',
'email' => 'required|email|unique:users,email',
'tags' => 'array|max:5',
'tags.*' => 'string|max-str-len:50|exists:tags,name'
];
// Error messages
$messages = [
'username.required' => 'Username is required',
'username.unique' => 'This username is already taken',
'email.unique' => 'This email is already in use',
'tags.max' => 'Maximum 5 tags',
'tags.*.exists' => 'One of the tags does not exist'
];
// Validation
$isValid = $this->validate($rules, null, $messages);
if (!$isValid) {
return;
}
// Process valid data
$this->createUser();
}
/**
* Custom validation
*/
public function validateUsername()
{
if (str_contains($this->username, 'admin')) {
$this->inputError('username', 'Username cannot contain the word "admin"');
return false;
}
return true;
}
/**
* Validation with additional rules
*/
public function createUser()
{
// Custom validation
if (!$this->validateUsername()) {
return;
}
// Create user
$user = User::create([
'username' => $this->username,
'email' => $this->email,
'tags' => $this->tags
]);
$this->flashMessage('User created successfully', 'success');
$this->redirectTo('/users');
}
}Working with Events
Dispatching Events
<?php
class EventEmitterComponent extends FluteComponent
{
public array $notifications = [];
/**
* Create notification
*/
public function createNotification($type, $message)
{
$notification = [
'id' => uniqid(),
'type' => $type,
'message' => $message,
'timestamp' => now()->toISOString()
];
$this->notifications[] = $notification;
// Emit event to browser
$this->emitEvent('notification-created', $notification);
}
/**
* Refresh data
*/
public function refreshData()
{
// Emit event about loading start
$this->emitEvent('data-loading-start');
try {
// Load data
$data = $this->loadData();
// Emit event about successful loading
$this->emitEvent('data-loaded', [
'data' => $data,
'count' => count($data)
]);
} catch (\Exception $e) {
// Emit event about error
$this->emitEvent('data-loading-error', [
'error' => $e->getMessage()
]);
}
}
/**
* Bulk emit events
*/
public function bulkOperations()
{
$operations = ['create', 'update', 'delete'];
foreach ($operations as $operation) {
$this->emitEvent('operation-start', [
'operation' => $operation
]);
// Perform operation
$result = $this->{$operation . 'Operation'}();
$this->emitEvent('operation-complete', [
'operation' => $operation,
'result' => $result
]);
}
$this->emitEvent('all-operations-complete');
}
}Listening to Events in JavaScript
{{-- resources/views/components/event-emitter.blade.php --}}
<div class="event-emitter-component">
<div class="notifications">
@foreach($notifications as $notification)
<div class="notification notification--{{ $notification['type'] }}">
{{ $notification['message'] }}
</div>
@endforeach
</div>
<button yoyo:post="refreshData()">Refresh Data</button>
<button yoyo:post="bulkOperations()">Bulk Operations</button>
</div>
@push('scripts')
<script>
// Listen to component events
document.addEventListener('notification-created', function(event) {
const notification = event.detail;
// Add notification to DOM
const notificationElement = createNotificationElement(notification);
document.querySelector('.notifications').appendChild(notificationElement);
// Auto remove after 5 seconds
setTimeout(() => {
notificationElement.remove();
}, 5000);
});
document.addEventListener('data-loading-start', function() {
showLoadingSpinner();
});
document.addEventListener('data-loaded', function(event) {
hideLoadingSpinner();
updateDataDisplay(event.detail.data);
showSuccessMessage(`Loaded ${event.detail.count} items`);
});
document.addEventListener('data-loading-error', function(event) {
hideLoadingSpinner();
showErrorMessage(event.detail.error);
});
document.addEventListener('operation-start', function(event) {
console.log(`Operation started: ${event.detail.operation}`);
});
document.addEventListener('operation-complete', function(event) {
console.log(`Operation completed: ${event.detail.operation}`);
});
document.addEventListener('all-operations-complete', function() {
showSuccessMessage('All operations completed');
});
function createNotificationElement(notification) {
const div = document.createElement('div');
div.className = `notification notification--${notification.type}`;
div.innerHTML = notification.message;
return div;
}
function showLoadingSpinner() {
// Show loading spinner
}
function hideLoadingSpinner() {
// Hide loading spinner
}
function updateDataDisplay(data) {
// Update data display
}
function showSuccessMessage(message) {
// Show success message
}
function showErrorMessage(message) {
// Show error message
}
</script>
@endpushConfirmations and Modal Windows
Confirmation System
<?php
class ConfirmationComponent extends FluteComponent
{
public array $items = [];
public string $selectedItemId = '';
/**
* Delete item with confirmation
*/
public function deleteItem($itemId)
{
// Find item
$item = $this->findItemById($itemId);
if (!$item) {
$this->flashMessage('Item not found', 'error');
return;
}
// Create confirmation
$this->withConfirmation(
"delete_item_{$itemId}",
'error',
"Are you sure you want to delete item '{$item['name']}'?",
function () use ($item) {
// Action on confirmation
$this->performDeletion($item);
$this->flashMessage('Item successfully deleted', 'success');
},
'Delete Item',
'Yes, delete',
'Cancel'
);
}
/**
* Batch processing with confirmation
*/
public function batchDelete()
{
$selectedItems = $this->getSelectedItems();
if (empty($selectedItems)) {
$this->flashMessage('No items selected for deletion', 'warning');
return;
}
$count = count($selectedItems);
$this->withConfirmation(
'batch_delete',
'warning',
"Are you sure you want to delete {$count} items?",
function () use ($selectedItems) {
foreach ($selectedItems as $item) {
$this->performDeletion($item);
}
$this->flashMessage("Deleted {$count} items", 'success');
},
'Bulk Delete',
'Yes, delete all',
'Cancel'
);
}
/**
* Critical action with additional confirmation
*/
public function criticalAction()
{
$this->withConfirmation(
'critical_action',
'error',
'This action cannot be undone. All data will be lost.',
function () {
// First show additional warning
$this->confirm(
'critical_action_confirm',
'error',
'CONFIRM: Do you understand this action is irreversible?',
'Critical Action',
'Yes, I understand consequences',
'Cancel'
);
},
'Critical Action',
'Continue',
'Cancel'
);
}
/**
* Handle critical action confirmation
*/
public function confirmCriticalAction()
{
// Perform critical action
$this->performCriticalAction();
$this->flashMessage('Critical action performed', 'success');
}
}Template with Confirmations
{{-- resources/views/components/confirmation-example.blade.php --}}
<div class="confirmation-component">
<div class="items-list">
@foreach($items as $item)
<div class="item">
<span>{{ $item['name'] }}</span>
<button class="btn btn-danger btn-sm"
yoyo:post="deleteItem({{ $item['id'] }})">
Delete
</button>
</div>
@endforeach
</div>
@if(count($selectedItems ?? []) > 0)
<div class="batch-actions">
<button class="btn btn-warning"
yoyo:post="batchDelete()">
Delete Selected ({{ count($selectedItems) }})
</button>
</div>
@endif
<button class="btn btn-danger"
yoyo:post="criticalAction()">
Critical Action
</button>
</div>Modal Windows
<?php
class ModalComponent extends FluteComponent
{
public string $modalTitle = '';
public string $modalContent = '';
/**
* Open modal window
*/
public function openModal($title, $content)
{
$this->modalTitle = $title;
$this->modalContent = $content;
$this->modalOpen('custom-modal');
}
/**
* Open edit form
*/
public function openEditForm($itemId)
{
$item = $this->findItemById($itemId);
if (!$item) {
$this->flashMessage('Item not found', 'error');
return;
}
$this->modalTitle = 'Edit Item';
$this->modalContent = $this->renderEditForm($item);
$this->modalOpen('edit-modal');
}
/**
* Save changes
*/
public function saveChanges()
{
// Validate and save
$this->validateAndSave();
// Close modal
$this->modalClose('edit-modal');
// Refresh list
$this->refreshItemsList();
}
/**
* Open confirmation
*/
public function openConfirmation($action, $itemId)
{
$this->modalTitle = 'Confirm Action';
$this->modalContent = $this->renderConfirmationDialog($action, $itemId);
$this->modalOpen('confirmation-modal');
}
/**
* Render modal window
*/
public function render()
{
return $this->view('components.modal-example');
}
}Real World Examples
ProductPurchaseComponent (from Shop module)
This component demonstrates all main features:
- State Management — properties for product, server, time
- Validation — checking data before purchase
- Confirmations — purchase confirmation dialogs
- Events — emitting events to browser
- AJAX Interaction — updating data without reload
- Modal Windows — integration with modal window system
<?php
// Adapted example from ProductQuickViewComponent
class ProductPurchaseComponent extends FluteComponent
{
public int $productId;
public ?int $time = null;
public ?int $serverId = null;
public function mount(int $productId)
{
$this->productId = $productId;
$this->loadProduct();
$this->initializeDefaults();
}
public function updatedServerId($serverId)
{
$this->serverId = (int) $serverId;
$this->emitEvent('server-changed', [
'serverId' => $this->serverId
]);
}
public function buyProduct()
{
// Validation
if (!$this->validatePurchase()) {
return;
}
// Confirmation
$this->withConfirmation(
'purchase_confirm',
'primary',
'Confirm product purchase?',
function () {
$this->processPurchase();
}
);
}
public function render()
{
return $this->view('shop::components.product-purchase', [
'product' => $this->getProduct(),
'servers' => $this->getAvailableServers(),
'prices' => $this->getProductPrices()
]);
}
}Best Practices
Component Organization
Single Responsibility
Each component should solve one problem.
Reusability
Components should be maximally reusable.
Clear API
Public methods should have clear names.
Documentation
All public methods should be documented.
Performance
- Data Caching — avoid extra DB queries
- Lazy Loading — load data only when needed
- Event Optimization — do not send unnecessary events
- Asset Minification — compress CSS and JavaScript
Security
- Data Validation — always check input data
- Authorization — check access rights to actions
- CSRF Protection — use built-in CSRF protection
- Sanitization — clean user input
Maintainability
- Testing — write tests for components
- Logging — log important actions
- Error Handling — handle exceptions correctly
- Documentation — maintain documentation for components
Summary
Flute CMS components provide a powerful tool for creating interactive web applications. They combine ease of use with high functionality, allowing you to create complex interfaces without needing page reloads.
Key Component Benefits:
- Reactivity — automatic interface updates
- AJAX Interaction — seamless server communication
- Event Model — flexible event system
- Validation — built-in data validation system
- Security — protection against CSRF and other attacks
- Reusability — ability to reuse components