Skip to Content
ModulesUI Components

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 (supports type, 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> @endpush

Confirmations 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>
<?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:

  1. State Management — properties for product, server, time
  2. Validation — checking data before purchase
  3. Confirmations — purchase confirmation dialogs
  4. Events — emitting events to browser
  5. AJAX Interaction — updating data without reload
  6. 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

  1. Data Caching — avoid extra DB queries
  2. Lazy Loading — load data only when needed
  3. Event Optimization — do not send unnecessary events
  4. Asset Minification — compress CSS and JavaScript

Security

  1. Data Validation — always check input data
  2. Authorization — check access rights to actions
  3. CSRF Protection — use built-in CSRF protection
  4. Sanitization — clean user input

Maintainability

  1. Testing — write tests for components
  2. Logging — log important actions
  3. Error Handling — handle exceptions correctly
  4. 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