It's pretty typical to create key-value pairs for extra information about a product or a customer. You may also define those keys upfront and show them as a dropdown. Let me show you how to do it in Filament with Repeater!
In this example, we will add such key value pairs to the Customer form.
Laravel Models and Relationships
First, the DB setup with the relationship.
We will have three DB tables:
- customers
- fields (with field name values like "name", "address", "phone", etc.)
- customer_field (pivot table with
value
extra column)
Now, the Models.
app/Models/Customer.php:
class Customer extends Model{ use HasFactory; protected $fillable = [ 'name', 'email', 'phone', ]; public function fields(): BelongsToMany { return $this->belongsToMany(Field::class)->withPivot('value'); } // Filament uses this to fill the Repeater public function customerFields(): HasMany { return $this->hasMany(CustomerField::class); }}
Next, just a simple Model for Field:
app/Models/Field.php:
class Field extends Model{ protected $fillable = [ 'name', ];}
Finally, we need a Pivot model to make it work with Filament:
app/Models/CustomerField.php:
class CustomerField extends Pivot{ public function customer(): BelongsTo { return $this->belongsTo(Customer::class); } public function field(): BelongsTo { return $this->belongsTo(Field::class); }}
Filament Form with Repeater
You can read about how Repeater field works in general, but here we're adding some "advanced" behavior, read the comments in the code below.
app/Filament/Resources/CustomerResource.php:
public static function form(Form $form): Form{ return $form ->schema([ // We made a Section with our customer fields, nothing special Forms\Components\Section::make('Customer Details') ->schema([ Forms\Components\TextInput::make('name') ->required() ->maxLength(255), Forms\Components\TextInput::make('email') ->email() ->required() ->maxLength(255), Forms\Components\TextInput::make('phone') ->required() ->maxLength(255), ]) ->columns(), // Here's the Repeater with our custom fields Forms\Components\Repeater::make('fields') ->label('Additional Information') // We are using the customerFields() relationship to fill the Repeater ->relationship('customerFields') ->schema([ Forms\Components\Select::make('field_id') ->label('Field Type') // Options are the Field names from the database ->options(Field::pluck('name', 'id')->toArray()) // We are disabling the option if it's already selected in another Repeater row ->disableOptionWhen(function ($value, $state, Get $get) { return collect($get('../*.field_id')) ->reject(fn($id) => $id == $state) ->filter() ->contains($value); }) ->required() // Field has to be live to prevent duplicates. // If it's not live, the disabling won't update to all rows in real-time. ->live(), // Simple value field that's written to pivot table Forms\Components\TextInput::make('value') ->required() ]) // Custom action label for the "Add Another Field" button ->addAction(function (Action $action) { return $action->label('Add Another Field'); }) ->columns(), ]) ->columns(1);}
And, that's it! Here's the result again, visually:
Since Filament version 3.1 you can achieve the same with only one live of code.
app/Filament/Resources/CustomerResource.php:
public static function form(Form $form): Form{ return $form ->schema([ // We made a Section with our customer fields, nothing special Forms\Components\Section::make('Customer Details') ->schema([ Forms\Components\TextInput::make('name') ->required() ->maxLength(255), Forms\Components\TextInput::make('email') ->email() ->required() ->maxLength(255), Forms\Components\TextInput::make('phone') ->required() ->maxLength(255), ]) ->columns(), // Here's the Repeater with our custom fields Forms\Components\Repeater::make('fields') ->label('Additional Information') // We are using the customerFields() relationship to fill the Repeater ->relationship('customerFields') ->schema([ Forms\Components\Select::make('field_id') ->label('Field Type') // Options are the Field names from the database ->options(Field::pluck('name', 'id')->toArray()) // We are disabling the option if it's already selected in another Repeater row ->disableOptionsWhenSelectedInSiblingRepeaterItems() ->disableOptionWhen(function ($value, $state, Get $get) { return collect($get('../*.field_id')) ->reject(fn($id) => $id == $state) ->filter() ->contains($value); }) ->required() // Field has to be live to prevent duplicates. // If it's not live, the disabling won't update to all rows in real-time. ->live(), // Simple value field that's written to pivot table Forms\Components\TextInput::make('value') ->required() ]) // Custom action label for the "Add Another Field" button ->addAction(function (Action $action) { return $action->label('Add Another Field'); }) ->columns(), ]) ->columns(1);}
This method automatically adds the discint()
and live()
methods on the field.
This code comes from one of our FilamentExamples projects: Form with Custom Fields
No comments or questions yet...