Building a custom color palette field in Filament - Part 1

laravel
Table of Contents

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',
    ]),

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.

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.

Enjoyed this post or found it useful? Please consider sharing it on Twitter.