Filament: Repeater with Key-Value Unique Pairs

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...

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 59 courses (1057 lessons, total 42 h 44 min)
  • 79 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent Premium Tutorials