Frontend Patterns
This guide covers the patterns and conventions used when building frontend Livewire components for the CMS.
Property Naming
Section titled “Property Naming”Maintain consistency between content keys and PHP properties:
| Content Key (snake_case) | PHP Property (camelCase) |
|---|---|
label | $label |
headline | $headline |
primary_cta_text | $primaryCtaText |
background_image | $backgroundImage |
members | $members |
// Content key: primary_cta_text// Property: $primaryCtaText// autoHydrate() handles the mapping automaticallySection Component Pattern
Section titled “Section Component Pattern”All section components extend SectionComponent and let autoHydrate() handle property mapping:
namespace App\Livewire;
use JFA\FilamentCMSLivewire\Livewire\SectionComponent;
class Hero extends SectionComponent{ public string $label = ''; public string $headline = ''; public string $primaryCtaText = ''; public string $primaryCtaUrl = '';
// No hydrateFromContent() needed! // No mount() override needed! // autoHydrate() maps everything automatically
public function render(): \Illuminate\Contracts\View\View { return view('livewire.components.hero'); }}Visual Editing Hook
Section titled “Visual Editing Hook”For visual editing, override initializeVisualEditing():
use JFA\VeFilamentCMSLivewire\Concerns\InteractsWithVisualEditing;
class Hero extends SectionComponent{ use InteractsWithVisualEditing;
public string $label = ''; public string $headline = ''; public array $cmsSourceMap = [];
protected function initializeVisualEditing(): void { $block = $this->block?->first(); if ($block !== null) { $this->cmsSourceMap = $block->getSourceMap(); } }
public function render(): \Illuminate\Contracts\View\View { return view('livewire.components.hero'); }}This hook is called automatically by SectionComponent::mount() after autoHydrate().
Repeater Handling
Section titled “Repeater Handling”Repeaters store arrays of associative arrays:
// autoHydrate() handles this automatically// $this->features is set from content key 'features'@foreach($features as $feature) <div> <h3>{{ $feature['title'] }}</h3> <p>{{ $feature['description'] }}</p> </div>@endforeachImage Handling
Section titled “Image Handling”From Spatie Media Library
Section titled “From Spatie Media Library”// In initializeVisualEditing()$this->imageUrl = $this->block?->images?->first()?->getUrl() ?? '';From FileUpload Block
Section titled “From FileUpload Block”// autoHydrate() maps 'background_image' to $backgroundImage automaticallypublic string $backgroundImage = '';<img src="{{ asset('storage/' . $backgroundImage) }}" alt="">Conditional Rendering
Section titled “Conditional Rendering”Always check for content existence:
@if($headline) <h1>{{ $headline }}</h1>@endif
@if(!empty($features)) <div class="grid"> @foreach($features as $feature) // ... @endforeach </div>@endifCSS Classes
Section titled “CSS Classes”Use Tailwind CSS utility classes:
<section class="py-20 bg-gray-900 text-white"> <div class="container mx-auto px-4"> <div class="max-w-3xl mx-auto text-center"> // ... </div> </div></section>For component-specific styles, add CSS files:
.hero-section { /* component-specific styles */}Import in resources/css/app.css:
@import 'tailwindcss';@import './hero.css';Component Layout
Section titled “Component Layout”Standard section structure:
<section class="py-20"> <div class="container mx-auto px-4"> {{-- Header --}} <div class="text-center mb-12"> <span class="text-sm uppercase">{{ $label }}</span> <h2 class="text-4xl font-bold">{{ $headline }}</h2> </div>
{{-- Content --}} <div class="grid grid-cols-1 md:grid-cols-3 gap-8"> // ... </div>
{{-- CTA --}} @if($primaryCtaText) <div class="text-center mt-12"> <a href="{{ $primaryCtaUrl }}" class="btn-primary"> {{ $primaryCtaText }} </a> </div> @endif </div></section>Visual Editing Integration
Section titled “Visual Editing Integration”To use renderField(), renderImageField(), and renderRepeaterContainer() in your component, add the InteractsWithVisualEditing trait:
use JFA\VeFilamentCMSLivewire\Concerns\InteractsWithVisualEditing;
class Hero extends SectionComponent{ use InteractsWithVisualEditing;
// No resolveFieldValue() needed — base class provides default}Then wrap editable fields in Blade:
{{-- Plain text --}}<h2>{!! $this->renderField('headline', 'text') !!}</h2>
{{-- Rich text (don't escape) --}}<div>{!! $this->renderField('body', 'rich_text', false) !!}</div>
{{-- Image --}}<img src="{{ $imageUrl }}" data-cms-source="{{ $this->renderImageField('image') }}">
{{-- Repeater container --}}<div data-cms-source="{{ $this->renderRepeaterContainer('items') }}"> @foreach($items as $item) // ... @endforeach</div>Note: Without this trait,
renderField()is unavailable. Non-visual-editing components don’t need any trait — simply use{{ $label }}directly.
Computed Properties
Section titled “Computed Properties”Use Laravel’s #[Computed] attribute for derived values:
use Illuminate\Support\Facades\Blade;
#[Computed]public function hasCta(): bool{ return !empty($this->primaryCtaText) && !empty($this->primaryCtaUrl);}@if($this->hasCta) <a href="{{ $primaryCtaUrl }}">{{ $primaryCtaText }}</a>@endifCustom Properties (Non-Native Types)
Section titled “Custom Properties (Non-Native Types)”For properties that aren’t native types, use initializeVisualEditing():
use Illuminate\Support\Collection;
class Services extends SectionComponent{ use InteractsWithVisualEditing;
public string $title = ''; public array $services = []; // autoHydrate handles this public Collection $serviceModels; // NOT auto-hydrated
protected function initializeVisualEditing(): void { $servicesData = $this->block?->first()?->get('services') ?? []; $ids = collect($servicesData)->pluck('service_id')->filter(); $this->serviceModels = \App\Models\Service::whereIn('id', $ids)->get();
$block = $this->block?->first(); if ($block !== null) { $this->cmsSourceMap = $block->getSourceMap(); } }}Events
Section titled “Events”Listen for content updates using the listener from InteractsWithVisualEditing:
protected $listeners = [ 'contentUpdated' => 'refreshFromCMS',];Or use the built-in listener from the trait (auto-registered when applied):
// Automatically listens for contentUpdated via InteractsWithVisualEditing// No additional setup neededBest Practices
Section titled “Best Practices”- Keep components focused on a single section
- Use semantic HTML (
<section>,<article>,<header>) - Add
aria-labelfor accessibility - Use responsive classes (
md:,lg:) - Don’t hardcode content — always pull from
$this->block - Use fallbacks (
?? '',?? []) for all fields - Test with empty content to ensure graceful degradation
- Use
renderField()for visual editing support
Common Mistakes
Section titled “Common Mistakes”| Mistake | Problem | Solution |
|---|---|---|
| Missing fallbacks | null displayed | Use ?? '' and ?? [] |
| Wrong content key | Empty field | Match snake_case in JSON |
| No source map | Visual editing broken | Implement initializeVisualEditing() |
| Escaped HTML | Tags shown as text | Use {!! !!} for rich text |
| Overriding mount() | Livewire serialization break | Don’t override mount() without parent::mount() |
| Non-native types in properties | TypeError | Use initializeVisualEditing() for Collection types |