# Laravel Nova Flexible Content Laravel Nova Flexible Content is a powerful field package for Laravel Nova that enables management of repeatable and orderable groups of fields. Unlike other solutions, it supports all Laravel Nova field types without constraints, including community-made fields, making it perfect for building dynamic content sections similar to WordPress's Advanced Custom Fields (ACF) plugin. The package provides a complete system for creating flexible content areas in Nova resources. It includes layouts (field groups that can be repeated), presets (reusable field configurations), custom resolvers (for storing data in alternative locations), and seamless integration with Spatie's Media Library. Values are stored as JSON and can be easily cast and accessed in your application through the provided `FlexibleCast` class or `HasFlexible` trait. ## Flexible Field Creation Create a flexible content field by instantiating the `Flexible` class and adding layouts that define repeatable field groups. Each layout has a title (displayed in Nova), a name (identifier stored in the database), and an array of Nova fields. ```php addLayout('Simple content section', 'wysiwyg', [ Text::make('Title'), Markdown::make('Content') ]) ->addLayout('Video section', 'video', [ Text::make('Title'), Image::make('Video Thumbnail', 'thumbnail'), Text::make('Video ID (YouTube)', 'video'), Text::make('Video Caption', 'caption') ]) ->addLayout('Image Gallery', 'gallery', [ Text::make('Gallery Title', 'title'), Image::make('Image 1', 'image_1'), Image::make('Image 2', 'image_2'), Image::make('Image 3', 'image_3'), ]) ]; } } ``` ## Customizing the Add Button Customize the "Add layout" button text using the `button()` method to provide context-specific labels for users. ```php button('Add new section') ->addLayout('Hero Section', 'hero', [ Text::make('Headline'), Textarea::make('Subheadline'), ]); // Output in Nova: Button displays "Add new section" instead of default "Add layout" ``` ## Limiting Layout Count Use the `limit()` method to restrict how many layout groups can be added. This is useful for sections that should only appear a limited number of times. ```php limit(3) ->addLayout('Featured Item', 'featured', [ Text::make('Title'), Image::make('Image'), Text::make('Link URL', 'url'), ]); // Allow only 1 hero section Flexible::make('Hero') ->limit(1) ->addLayout('Hero Banner', 'hero', [ Text::make('Headline'), Text::make('CTA Button Text', 'cta_text'), Text::make('CTA Button URL', 'cta_url'), ]); ``` ## Layout Removal Confirmation Enable a confirmation dialog before users can delete a layout using `confirmRemove()`. Customize the message and button labels as needed. ```php confirmRemove() ->addLayout('Text Block', 'text', [ Text::make('Title'), Textarea::make('Body'), ]); // Custom confirmation with specific message and buttons Flexible::make('Important Sections') ->confirmRemove( 'Are you sure you want to remove this section? This action cannot be undone.', 'Yes, Remove', 'Keep It' ) ->addLayout('Section', 'section', [ Text::make('Title'), ]); ``` ## Full Width Display Make the flexible field span the full width of the form with `fullWidth()`, placing labels above the field content. ```php fullWidth() ->addLayout('Content Block', 'content', [ Text::make('Section Title'), Markdown::make('Content'), ]); ``` ## Layout Selection Menu Customize how users select layouts using `menu()`. Choose between the default dropdown or a searchable select field. ```php menu('flexible-drop-menu') ->addLayout('Text', 'text', [Text::make('Content')]); // Searchable select menu Flexible::make('Content') ->menu('flexible-search-menu') ->addLayout('Text', 'text', [Text::make('Content')]) ->addLayout('Image', 'image', [Text::make('URL')]) ->addLayout('Video', 'video', [Text::make('Video ID')]); // Customized searchable menu Flexible::make('Content') ->menu('flexible-search-menu', [ 'selectLabel' => 'Press enter to select', 'label' => 'title', 'openDirection' => 'bottom', // 'top', 'bottom', or 'auto' ]) ->addLayout('Hero Section', 'hero', [Text::make('Title')]) ->addLayout('Feature Grid', 'features', [Text::make('Title')]); ``` ## FlexibleCast for Model Attribute Casting Use `FlexibleCast` to automatically parse flexible content JSON into a collection of Layout instances when accessing model attributes. ```php FlexibleCast::class, ]; } // Usage in controller or view $page = Page::find(1); // Access the flexible content as a collection foreach ($page->content as $layout) { echo $layout->name(); // 'wysiwyg', 'video', etc. echo $layout->title(); // 'Simple content section', etc. echo $layout->key(); // Unique identifier echo $layout->title; // Access attributes directly echo $layout->content; // Access any field value } // Find a specific layout by name $videoSection = $page->content->find('video'); if ($videoSection) { echo $videoSection->video; // YouTube video ID } ``` ## Custom FlexibleCast with Layout Mapping Create custom cast classes to map layout names to custom Layout classes for enhanced functionality like accessors, mutators, and methods. ```php \App\Nova\Flexible\Layouts\WysiwygLayout::class, 'video' => \App\Nova\Flexible\Layouts\VideoLayout::class, 'gallery' => \App\Nova\Flexible\Layouts\GalleryLayout::class, ]; } // Model usage namespace App\Models; use Illuminate\Database\Eloquent\Model; use App\Casts\PageContentCast; class Page extends Model { protected $casts = [ 'content' => PageContentCast::class, ]; } // Now layouts will be instances of your custom classes $page = Page::find(1); foreach ($page->content as $layout) { // Each layout is now the appropriate custom class // with access to custom methods and accessors } ``` ## HasFlexible Trait Use the `HasFlexible` trait on models for manual flexible content parsing with optional layout mapping. ```php flexible('content'); } // Accessor with custom layout mapping public function getMappedContentAttribute() { return $this->flexible('content', [ 'wysiwyg' => \App\Nova\Flexible\Layouts\WysiwygLayout::class, 'video' => \App\Nova\Flexible\Layouts\VideoLayout::class, ]); } } // Usage $page = Page::find(1); // Using simple accessor foreach ($page->content_blocks as $block) { echo $block->title; } // Using mapped accessor foreach ($page->mapped_content as $block) { // Blocks are now custom Layout instances if ($block->name() === 'video') { echo $block->getEmbedUrl(); // Custom method on VideoLayout } } ``` ## Custom Layout Classes Extract layout definitions into reusable classes for cleaner code and shared layouts across multiple resources. ```php content); } // Custom method public function getExcerpt($length = 100) { return substr($this->formatted_content, 0, $length) . '...'; } } // Usage in Nova Resource use App\Nova\Flexible\Layouts\WysiwygLayout; use App\Nova\Flexible\Layouts\VideoLayout; Flexible::make('Content') ->addLayout(WysiwygLayout::class) ->addLayout(VideoLayout::class); ``` ## Preset Classes Create reusable Preset classes to configure entire Flexible fields with multiple layouts, resolvers, and options. ```php button('Add Page Block'); $field->confirmRemove('Remove this block?', 'Remove', 'Cancel'); $field->menu('flexible-search-menu'); $field->fullWidth(); $field->addLayout(HeroLayout::class); $field->addLayout(WysiwygLayout::class); $field->addLayout(GalleryLayout::class); $field->addLayout(CtaLayout::class); } } // Usage in Nova Resource use App\Nova\Flexible\Presets\PageBuilderPreset; Flexible::make('Content') ->preset(PageBuilderPreset::class); ``` ## Custom Resolver Classes Create custom resolvers to store flexible content in alternative locations like separate database tables instead of JSON columns. ```php blocks()->orderBy('order')->get(); return $blocks->map(function ($block) use ($layouts) { $layout = $layouts->find($block->name); if (!$layout) { return null; } return $layout->duplicateAndHydrate($block->id, [ 'title' => $block->title, 'content' => $block->content, ]); })->filter(); } /** * Save the field's value to the database */ public function set($model, $attribute, $groups) { $class = get_class($model); $class::saved(function ($model) use ($groups) { $blocks = $groups->map(function ($group, $index) { return [ 'name' => $group->name(), 'title' => $group->title, 'content' => $group->content, 'order' => $index, ]; }); // Sync blocks with the database $model->blocks()->delete(); $model->blocks()->createMany($blocks->toArray()); }); } } // Usage in Nova Resource use App\Nova\Flexible\Resolvers\BlocksResolver; Flexible::make('Content') ->resolver(BlocksResolver::class) ->addLayout('Block', 'block', [ Text::make('Title'), Markdown::make('Content'), ]); ``` ## Layouts Collection Methods The Layouts Collection extends Laravel's Collection with a `find()` method to locate layouts by name. ```php content->find('hero'); // Use standard Laravel collection methods $textBlocks = $page->content->filter(function ($layout) { return $layout->name() === 'wysiwyg'; }); // Count specific layout types $videoCount = $page->content->where('layout', 'video')->count(); // Map layouts to arrays $titles = $page->content->map(function ($layout) { return [ 'type' => $layout->name(), 'title' => $layout->title ?? 'Untitled', ]; }); // Get first layout $firstBlock = $page->content->first(); echo $firstBlock->name(); // 'hero' echo $firstBlock->title(); // 'Hero Section' echo $firstBlock->key(); // 'c1a2b3c4d5e6f7g8' ``` ## Media Library Integration Integrate with Spatie's Media Library and ebess/advanced-nova-media-library for image uploads within flexible layouts. ```php conversionOnIndexView('thumb') ->rules('required'), ]; } } // Usage - accessing media in views $page = Page::find(1); foreach ($page->content as $layout) { if ($layout->name() === 'slider') { $slides = $layout->getMedia('slides'); foreach ($slides as $slide) { echo $slide->getUrl('thumb'); } } } ``` ## Collapsed State Control whether layouts are collapsed by default using the `collapsed()` method. ```php collapsed(true) ->addLayout('Section', 'section', [ Text::make('Title'), Textarea::make('Content'), ]); // Start with layouts expanded (default) Flexible::make('Content') ->collapsed(false) ->addLayout('Section', 'section', [ Text::make('Title'), ]); ``` ## Rendering Flexible Content in Blade Views Iterate through flexible content layouts in Blade templates to render dynamic page sections. ```php {{-- resources/views/page.blade.php --}} @extends('layouts.app') @section('content')
{{ $layout->caption }}