DEV3 Technologies home

Filament dynamic field repeater and runtime renderer

Let admins define custom form fields in a Filament 4 repeater with type-aware options, then render those definitions as real form inputs.

FilamentFormsfilamentrepeaterdynamic-formbuilder

Code

<?php

namespace App\Filament\Support;

use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Support\Str;

/** The editor: put FieldDefinitions::repeater() in a resource form. Cast the column to array. */
class FieldDefinitions
{
    public const TYPES = [
        'text' => 'Short text',
        'number' => 'Number',
        'select' => 'Dropdown',
        'checkbox' => 'Checkbox',
    ];

    public static function repeater(string $name = 'fields'): Repeater
    {
        return Repeater::make($name)
            ->columns(2)
            ->collapsible()
            ->cloneable()
            ->reorderableWithButtons()
            ->minItems(1)
            ->maxItems(30)
            ->addActionLabel('Add field')
            ->itemLabel(fn (array $state): ?string => $state['label'] ?? null)
            ->schema([
                TextInput::make('label')
                    ->required()
                    ->live(onBlur: true)
                    // Generate the storage key once, from the label.
                    ->afterStateUpdated(function (Get $get, Set $set, ?string $state) {
                        if (blank($get('key'))) {
                            $set('key', Str::snake((string) $state));
                        }
                    }),
                TextInput::make('key')->required()->alphaDash()->distinct(),
                Select::make('type')->options(self::TYPES)->default('text')->required()->live(),
                Toggle::make('required')->inline(false),
                // Options only make sense for dropdowns.
                TagsInput::make('options')
                    ->columnSpanFull()
                    ->visible(fn (Get $get): bool => $get('type') === 'select')
                    ->required(fn (Get $get): bool => $get('type') === 'select'),
                // Numeric bounds only for numbers.
                TextInput::make('min')->numeric()->visible(fn (Get $get): bool => $get('type') === 'number'),
                TextInput::make('max')->numeric()->visible(fn (Get $get): bool => $get('type') === 'number'),
            ]);
    }
}

Needs: filament/filament@^4.0

How it works

What it solves

Form, survey and intake builders let non-developers define which inputs a form has. That needs an editor for field definitions and a renderer that turns those definitions back into live inputs. This snippet shows both halves in Filament 4.

How it works

FieldDefinitions::repeater() returns a Repeater whose rows each describe one field: label, storage key, type, required flag and type-specific settings. The repeater is collapsible, reorderable with buttons and clonable, and itemLabel() shows the label as the row title.

Type awareness comes from live() on the type select plus visible() closures reading sibling state through Get. Choosing "Dropdown" reveals a required tags input for the choices, and "Number" reveals min and max. afterStateUpdated on the label fills the key with a snake-case version, but only while the key is blank, so manual edits stick. distinct() stops duplicate keys within the list.

DynamicFields::components() is the other half. It maps each saved definition to a real component with match, applies the label and required rule, and namespaces the state under answers.<key>.

Adapting it

Store the repeater in a JSON column cast to array. Add more types (date, textarea) to both TYPES and the match. For public rendering, reuse the same definitions to build Laravel validation rules.

Pitfalls

  • Keys are part of your data contract, renaming one orphans old answers.
  • Validate definitions again on the server; never trust a saved type string when generating rules.
  • live() makes a request per change, use live(onBlur: true) on text inputs.

How to use it

  1. Add a JSON column (for example fields) to your form model and cast it to array.
  2. Use FieldDefinitions::repeater() inside the resource form schema.
  3. To collect answers, call DynamicFields::components($form->fields) in a schema (a wizard step, a page form, or a public Livewire component).
  4. Store answers as JSON keyed by each field key.
  5. Extend the TYPES constant and the renderer match together.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets