Building a custom color palette field in Filament - Part 1
To accept an array of options on the field, we need to declare a new method on the class. This method will accept an array of $options
and store it in a property on the object also called $options
.
class ColorPalette extends Field
{
// ...
protected array $options = [];
public function options(array $options): static
{
$this->options = $options;
return $this;
}
}
We can now pass an array of options to the field in our form:
ColorPalette::make('color')
->options([
'#ffffff' => 'White',
'#000000' => 'Black',
]),
Retrieving the options
All fields and form components in Filament are actually just Blade components under the hood. This means we can define public methods on our ColorPalette
class and they will be sent through to the Blade view as invokable variables.
Let's add a getOptions()
method to the field that returns the array of options given to the field.
class ColorPalette extends Field
{
// ...
public function getOptions(): array
{
return $this->options;
}
}
Now inside of our Blade view, we can call this method and loop over the returned array
.
<x-forms::field-wrapper
:id="$getId()"
:label="$getLabel()"
:label-sr-only="$isLabelHidden()"
:helper-text="$getHelperText()"
:hint="$getHint()"
:hint-icon="$getHintIcon()"
:required="$isRequired()"
:state-path="$getStatePath()"
>
<div class="flex items-center space-x-4">
@foreach($getOptions() as $color => $label)
<button type="button" class="rounded-full w-8 h-8 border border-gray-500" style="background: {{ $color }}" title="{{ $label }}">
<span class="sr-only">
{{ $label }}
</span>
</button>
@endforeach
</div>
</x-forms::field-wrapper>
Using inline styles, we can change the background color of the button to the option's color. For accessibility reasons, we'll also output the $label
provided but only for screenreaders.
And just like that, we're rendering the color options on the page.
Adding support for computed options
In its current state, the ColorPalette
field can only accept a static array of color options. This causes problems when you want to change the color options based on the value of another field or the record you're working on.
This is where Filament's incredibly powerful Closure customisation system comes into play. By widening the type of the $options
argument to also accept Closure
values, we can allow developers to provide a computed list of options instead of a static list.
There are a few steps to make this work. The first being widening the types on the class itself.
class ColorPalette extends Field
{
// ...
protected array |Closure $options = [];
public function options(array |Closure $options): static
{
$this->options = $options;
return $this;
}
// ...
}
The ColorPalette::options()
method now accepts an array
or Closure
through a union type. This allows us to do this:
Select::make('theme')
->reactive()
->options([
'minimal' => 'Minimal',
'abstract' => 'Abstract',
]),
ColorPalette::make('color')
->options(function (callable $get) {
if ($get('theme') === 'abstract') {
return [
'#ff69b4' => 'Hot Pink',
'#32cd32' => 'Lime Green',
];
}
return [
'#ffffff' => 'White',
'#000000' => 'Black',
];
})
Now the ColorPalette
field's options will be updated and computed based on the option selected in the Select
field above it. Or will they?
There's one more thing we need to do. We need to actually invoke the Closure
. Thankfully, Filament has a smooth evaluate()
API which will invoke a Closure
and provide some standardised arguments depending on the context you're evaluating it in.
Instead of returning $this->options
inside of getOptions()
, we can return send the value through $this->evaluate()
and return the result of that instead.
class ColorPalette extends Field
{
// ...
public function getOptions(): array
{
return $this->evaluate($this->options);
}
}
In the next part, we'll hook up our buttons and start persisting state to the form / Livewire component.