whitecube/nova-flexible-content
Installation:
composer require whitecube/nova-flexible-content
php artisan vendor:publish --provider="Whitecube\\NovaFlexibleContent\\NovaFlexibleContentServiceProvider" --tag="nova-flexible-content-config"
php artisan nova:publish
First Use Case: Define a flexible content field in your Nova resource:
use Whitecube\\NovaFlexibleContent\\FlexibleContent;
public static $fields = [
FlexibleContent::make('Content Blocks')
->items([
// Define your repeatable field groups here
FlexibleContent\Items\Item::make('Hero Section', 'hero')
->fields([
Text::make('Title'),
Textarea::make('Subtitle'),
]),
FlexibleContent\Items\Item::make('Features', 'features')
->fields([
Text::make('Feature Title'),
Textarea::make('Description'),
]),
]),
];
Where to Look First:
resources/js/tools/FlexibleContent (for custom JS/CSS overrides)config/nova-flexible-content.php (for configuration options)Defining Flexible Content:
FlexibleContent::make() to create a container for repeatable content blocks.Item::make() for each unique content type (e.g., "Hero", "Testimonial").Item.Repeater Fields:
FlexibleContent::make('FAQs')
->items([
FlexibleContent\Items\Item::make('FAQ Item', 'faq')
->fields([
Text::make('Question'),
Textarea::make('Answer'),
Boolean::make('Is Featured'),
]),
])
->minItems(1) // Optional: enforce minimum items
->maxItems(10) // Optional: enforce maximum items
Dynamic Field Logic:
when() to conditionally show/hide items based on other fields:
FlexibleContent::make('Product Options')
->items([
Item::make('Color Options', 'colors')
->fields([...])
->when(fn ($resource) => $resource->isPhysicalProduct),
])
Integration with Nova Tools:
Item::make('Image Block', 'image')
->fields([
BelongsTo::make('Image', 'image', Media::class),
Text::make('Caption'),
])
Resource-Specific Logic:
resolveFlexibleContentItems() method in your resource to dynamically fetch or transform data:
public function resolveFlexibleContentItems($request, $model, $attribute, $requestAttribute)
{
return $model->{$attribute}->map(function ($item) {
return collect($item)->merge(['custom_key' => 'dynamic_value']);
});
}
Field Validation:
morphTo relationship for the flexible content:
public function flexibleContent()
{
return $this->morphMany(FlexibleContentItem::class, 'flexible_contentable');
}
public static $rules = [
'content_blocks.*.title' => 'required|max:255',
'content_blocks.*.hero.title' => 'sometimes|required_if:content_blocks.*.type,hero',
];
Performance:
public function scopeWithFlexibleContent($query)
{
$query->with(['flexibleContent' => function ($query) {
$query->with(['items']);
}]);
}
protected static $cache = true;
Ordering Issues:
order (default) or override:
FlexibleContent::make('Blocks')->orderColumn('custom_order_column');
sortable() on the Item level for drag-and-drop:
Item::make('Section')->sortable();
CSRF Token Conflicts:
// resources/js/tools/FlexibleContent/Tool.js
this.handleFormEvents = () => {
this.container.on('submit', 'form', (e) => {
e.preventDefault();
const form = e.target;
form.querySelector('input[name="_token"]').value = this.token;
form.submit();
});
};
Custom Item Templates:
// resources/js/tools/FlexibleContent/ItemTemplate.js
export default class extends ItemTemplate {
template() {
return `
<div class="flexible-content-item">
<h3>{{ this.item.title }}</h3>
<div class="fields">
{{ this.fields }}
</div>
<button class="delete-item">Delete</button>
</div>
`;
}
}
Dynamic Item Types:
public static function flexibleContentItemTypes()
{
return \App\Models\ContentType::all()->pluck('name', 'slug');
}
Localization:
Item::make('Hero', 'hero')
->title(__('nova-flexible-content::items.hero.title'))
->fields([
Text::make(__('nova-flexible-content::fields.title'), 'title'),
]);
php artisan vendor:publish --tag="nova-flexible-content-lang"
Testing:
NovaTestCase to test flexible content:
public function testFlexibleContent()
{
$resource = new YourResource();
$this->actingAs($this->admin)
->call('POST', '/nova/v1/resources/your-resource', [
'flexible_content' => [
[
'type' => 'hero',
'title' => 'Test Hero',
],
],
]);
}
Extending Core Functionality:
Item::make('Gallery', 'gallery')
->actions([
new PublishAction(),
]);
class CustomField extends Field
{
public function field()
{
return FlexibleContent::make('Custom Content')
->items([...]);
}
}
Debugging:
'debug' => env('NOVA_FLEXIBLE_CONTENT_DEBUG', false),
tail -f storage/logs/nova.log | grep "FlexibleContent"
data attribute on flexible content containers for raw payloads.Migration Quirks:
Schema::table('flexible_content_items', function (Blueprint $table) {
$table->json('data')->nullable()->change();
$table->string('type')->nullable()->change();
});
```markdown
## Maintenance and Contribution
- **Open Issues**: Prioritize bugs related to:
- Drag-and-drop reordering
- Nested form validation
- PHP 8.2+ compatibility
- **Testing**: Add tests for edge cases like:
- Empty flexible content arrays
- Circular references in nested fields
- Concurrent saves
- **Documentation**: Update the [official docs](https://whitecube.github.io/nova-flexible-content) with:
- Real-world examples (e.g., CMS page builder)
- Performance benchmarks
- Integration guides (e.g., with Nova Filament, Nova Media Library)
How can I help you explore Laravel packages today?