Skip to main content
Tutorial Free

Filament: Repeater with Key-Value Unique Pairs

April 04, 2024
5 min read

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

Enjoyed This Tutorial?

Get access to all premium tutorials, video and text courses, and exclusive Laravel resources. Join our community of 10,000+ developers.

Comments & Discussion

No comments yet…

We'd Love Your Feedback

Tell us what you like or what we can improve

Feel free to share anything you like or dislike about this page or the platform in general.