Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Add Docs
Laravel Nova Flexible Content
https://github.com/whitecube/nova-flexible-content
Admin
An easy and complete flexible field for Laravel Nova that enables management of repeatable and
...
Tokens:
9,286
Snippets:
91
Trust Score:
9.7
Update:
1 month ago
Context
Skills
Chat
Benchmark
76
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# 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 <?php namespace App\Nova; use Illuminate\Http\Request; use Laravel\Nova\Fields\Text; use Laravel\Nova\Fields\Markdown; use Laravel\Nova\Fields\Image; use Laravel\Nova\Resource; use Whitecube\NovaFlexibleContent\Flexible; class Page extends Resource { public static $model = \App\Models\Page::class; public function fields(Request $request) { return [ Text::make('Title'), Flexible::make('Content') ->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 <?php use Whitecube\NovaFlexibleContent\Flexible; use Laravel\Nova\Fields\Text; use Laravel\Nova\Fields\Textarea; Flexible::make('Page Sections') ->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 <?php use Whitecube\NovaFlexibleContent\Flexible; use Laravel\Nova\Fields\Text; use Laravel\Nova\Fields\Image; // Allow only 3 featured items maximum Flexible::make('Featured Items') ->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 <?php use Whitecube\NovaFlexibleContent\Flexible; use Laravel\Nova\Fields\Text; use Laravel\Nova\Fields\Textarea; // Default confirmation dialog Flexible::make('Content Blocks') ->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 <?php use Whitecube\NovaFlexibleContent\Flexible; use Laravel\Nova\Fields\Text; use Laravel\Nova\Fields\Markdown; Flexible::make('Page Content') ->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 <?php use Whitecube\NovaFlexibleContent\Flexible; use Laravel\Nova\Fields\Text; // Default dropdown menu Flexible::make('Content') ->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 <?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Whitecube\NovaFlexibleContent\Value\FlexibleCast; class Page extends Model { protected $casts = [ 'content' => 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 <?php // Generate with: php artisan flexible:cast PageContentCast namespace App\Casts; use Whitecube\NovaFlexibleContent\Value\FlexibleCast; class PageContentCast extends FlexibleCast { protected $layouts = [ 'wysiwyg' => \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 <?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Whitecube\NovaFlexibleContent\Concerns\HasFlexible; class Page extends Model { use HasFlexible; // Simple accessor without layout mapping public function getContentBlocksAttribute() { return $this->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 <?php // Generate with: php artisan flexible:layout WysiwygLayout wysiwyg namespace App\Nova\Flexible\Layouts; use Laravel\Nova\Fields\Text; use Laravel\Nova\Fields\Markdown; use Whitecube\NovaFlexibleContent\Layouts\Layout; class WysiwygLayout extends Layout { protected $name = 'wysiwyg'; protected $title = 'Content Section'; // Limit how many of this layout type can be added (optional) protected $limit = 5; public function fields() { return [ Text::make('Title'), Markdown::make('Content'), ]; } // Custom accessor for the layout public function getFormattedContentAttribute() { return strip_tags($this->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 <?php // Generate with: php artisan flexible:preset PageBuilderPreset namespace App\Nova\Flexible\Presets; use App\Nova\Flexible\Layouts\HeroLayout; use App\Nova\Flexible\Layouts\WysiwygLayout; use App\Nova\Flexible\Layouts\GalleryLayout; use App\Nova\Flexible\Layouts\CtaLayout; use Whitecube\NovaFlexibleContent\Flexible; use Whitecube\NovaFlexibleContent\Layouts\Preset; class PageBuilderPreset extends Preset { public function handle(Flexible $field) { $field->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 <?php // Generate with: php artisan flexible:resolver BlocksResolver namespace App\Nova\Flexible\Resolvers; use Whitecube\NovaFlexibleContent\Value\ResolverInterface; class BlocksResolver implements ResolverInterface { /** * Retrieve the field's value from the database */ public function get($resource, $attribute, $layouts) { $blocks = $resource->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 <?php use App\Models\Page; $page = Page::find(1); // Find first layout with specific name $heroSection = $page->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 <?php // Parent Model namespace App\Models; use Illuminate\Database\Eloquent\Model; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; use Whitecube\NovaFlexibleContent\Concerns\HasFlexible; class Page extends Model implements HasMedia { use HasFlexible; use InteractsWithMedia; } // Custom Layout with Media Support namespace App\Nova\Flexible\Layouts; use Spatie\MediaLibrary\HasMedia; use Whitecube\NovaFlexibleContent\Layouts\Layout; use Whitecube\NovaFlexibleContent\Concerns\HasMediaLibrary; use Ebess\AdvancedNovaMediaLibrary\Fields\Images; use Laravel\Nova\Fields\Text; class SliderLayout extends Layout implements HasMedia { use HasMediaLibrary; protected $name = 'slider'; protected $title = 'Image Slider'; public function fields() { return [ Text::make('Slider Title', 'title'), Images::make('Slides', 'slides') ->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 <?php use Whitecube\NovaFlexibleContent\Flexible; use Laravel\Nova\Fields\Text; use Laravel\Nova\Fields\Textarea; // Start with all layouts collapsed Flexible::make('Content') ->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') <h1>{{ $page->title }}</h1> @foreach($page->content as $layout) @if($layout->name() === 'wysiwyg') <section class="content-section"> <h2>{{ $layout->title }}</h2> <div class="prose"> {!! $layout->content !!} </div> </section> @elseif($layout->name() === 'video') <section class="video-section"> <h2>{{ $layout->title }}</h2> @if($layout->thumbnail) <img src="{{ $layout->thumbnail }}" alt="{{ $layout->caption }}"> @endif <iframe src="https://www.youtube.com/embed/{{ $layout->video }}" allowfullscreen> </iframe> <p>{{ $layout->caption }}</p> </section> @elseif($layout->name() === 'gallery') <section class="gallery-section"> <h2>{{ $layout->title }}</h2> <div class="gallery-grid"> @foreach(['image_1', 'image_2', 'image_3'] as $image) @if($layout->$image) <img src="{{ $layout->$image }}" alt=""> @endif @endforeach </div> </section> @endif @endforeach @endsection ``` ## Summary Laravel Nova Flexible Content is ideal for building page builders, dynamic content management systems, and any scenario requiring repeatable field groups. Common use cases include landing page builders with multiple section types, blog posts with mixed media content, product pages with flexible feature lists, and portfolio sites with customizable project layouts. The package excels when content editors need flexibility in structuring content without developer intervention. Integration follows standard Laravel patterns with full support for Nova's field ecosystem. The architecture supports extension through custom Layout classes for reusable field groups, Preset classes for complete field configurations, and custom Resolver classes for alternative storage backends. The package integrates seamlessly with popular packages like Spatie's Media Library, making it suitable for complex content management requirements while maintaining clean, maintainable code.