Building a custom color palette field in Filament - Part 3

laravel
Table of Contents

Let's spruce up our field a little bit and add some extra methods of customisation.

There might be certain scenarios where you want to store the name instead of the actual color code in your applications. This could be useful if you are building a CMS and want to conditionally apply classes to an element based on the name of a color instead of the color code, avoiding the need for inline styles in your markup.

To achieve this, we'll add a new storeColorName() method to the field and adjust the field's functionality accordingly.

class ColorPalette extends Field
{
    // ...

    protected bool | Closure $shouldStoreColorName = false;

    // ...

    public function storeColorName(bool | Closure $condition = true): static
    {
        $this->shouldStoreColorName = $condition;

        return $this;
    }

    public function shouldStoreColorName(): bool
    {
        return (bool) $this->evaluate($this->shouldStoreColorName);
    }
}

And making some changes in the Blade view:

@php
    $shouldStoreColorName = $shouldStoreColorName();
@endphp

<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 x-data="{ state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }} }" class="flex items-center space-x-4">
        @foreach($getOptions() as $color => $label)
            @php($value = $shouldStoreColorName ? $label : $color)

            <button
                type="button"
                x-on:click="state = @js($value)"
                class="rounded-full w-8 h-8 border border-gray-300 relative inline-flex items-center justify-center"
                x-bind:class="{
                    'ring-2 ring-gray-300 ring-offset-2': state === @js($value),
                }"
                style="background: {{ $color }}" title="{{ $label }}"
            >
                <span class="sr-only">
                    {{ $label }}
                </span>

                <span x-show="state === @js($value)" x-cloak>
                    <x-heroicon-o-check class="w-4 h-4 text-gray-400" />
                </span>
            </button>
        @endforeach
    </div>
</x-forms::field-wrapper>

When selecting an option now it will use the color's name instead of the color code.

In some cases you might want your users to select their own color. Modern web browsers provide a color picker input type which we can use to do this.

Let's start by adding a method like before:

class ColorPalette extends Field
{
    // ...
    
    protected bool | Closure $canChooseCustomColors = false;

    // ...

    public function allowCustomColors(bool | Closure $condition = true): static
    {
        $this->canChooseCustomColors = $condition;

        return $this;
    }

    public function canChooseCustomColors(): bool
    {
        return (bool) $this->evaluate($this->canChooseCustomColors);
    }
}

And again, make some changes in the Blade view:

@php
    $shouldStoreColorName = $shouldStoreColorName();
@endphp

<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 x-data="{ state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }} }" class="flex space-x-4">
        @foreach($getOptions() as $color => $label)
            @php($value = $shouldStoreColorName ? $label : $color)

            <button
                type="button"
                x-on:click="state = @js($value)"
                class="rounded-full w-8 h-8 border border-gray-300 relative inline-flex items-center justify-center"
                x-bind:class="{
                    'ring-2 ring-gray-300 ring-offset-2': state === @js($value),
                }"
                style="background: {{ $color }}" title="{{ $label }}"
            >
                <span class="sr-only">
                    {{ $label }}
                </span>

                <span x-show="state === @js($value)" x-cloak>
                    <x-heroicon-o-check class="w-4 h-4 text-gray-400" />
                </span>
            </button>
        @endforeach

        @if($canChooseCustomColors() && ! $shouldStoreColorName)
            <div class="flex border bg-gray-50 rounded-lg">
                <input type="color" name="{{ $getStatePath() }}.custom" x-model.lazy="state" class="block h-full p-0 rounded-l-lg">
                <div class="text-xs font-medium px-2 inline-flex items-center">
                    <span>Select Color</span>
                </div>
            </div>
        @endif
    </div>
</x-forms::field-wrapper>

Alpine.js is handling the state changes on the color input and with some styling we can get it to look like a single input still. Here's the result:

Whilst we're here we should add a method that lets you change the custom color selector's label.

class ColorPalette extends Field
{
    // ...

    protected string | Closure $customColorLabel = 'Select Color';

    // ...

    public function customColorLabel(string | Closure $label): static
    {
        $this->customColorLabel = $label;

        return $this;
    }

    public function getCustomColorLabel(): string
    {
        return (string) $this->evaluate($this->customColorLabel);
    }
}

And updating the Blade view really simple:

@php
    $shouldStoreColorName = $shouldStoreColorName();
@endphp

<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 x-data="{ state: $wire.{{ $applyStateBindingModifiers('entangle(\'' . $getStatePath() . '\')') }} }" class="flex space-x-4">
        @foreach($getOptions() as $color => $label)
            @php($value = $shouldStoreColorName ? $label : $color)

            <button
                type="button"
                x-on:click="state = @js($value)"
                class="rounded-full w-8 h-8 border border-gray-300 relative inline-flex items-center justify-center"
                x-bind:class="{
                    'ring-2 ring-gray-300 ring-offset-2': state === @js($value),
                }"
                style="background: {{ $color }}" title="{{ $label }}"
            >
                <span class="sr-only">
                    {{ $label }}
                </span>

                <span x-show="state === @js($value)" x-cloak>
                    <x-heroicon-o-check class="w-4 h-4 text-gray-400" />
                </span>
            </button>
        @endforeach

        @if($canChooseCustomColors() && ! $shouldStoreColorName)
            <div class="flex border bg-gray-50 rounded-lg">
                <input type="color" name="{{ $getStatePath() }}.custom" x-model.lazy="state" class="block h-full p-0 rounded-l-lg">
                <div class="text-xs font-medium px-2 inline-flex items-center">
                    <span>{{ $getCustomColorLabel() }}</span>
                </div>
            </div>
        @endif
    </div>
</x-forms::field-wrapper>

And there we have it. A pretty powerful and useful ColorPalette field that lets you select a color from a fixed list of options and also choose your own custom colors.

Hopefully you found this mini-series helpful and learned a thing or two.

If you want to use this field in your own projects without rebuilding it, it's available for free on GitHub. You can find installation and usage instructions in the repository.

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