Ryan Chandler

Building a custom Filament view component

1 min read

This post was published 2 years ago. Some of the information might be outdated!

Out of the box, Filament provides a tonne of components that you can use in your application. Sometimes you might need something a little extra. Let's look at how I built a DescriptionList component for one of my own projects.

We start off by creating a new Component class. I'm going to do this manually inside of app/Forms/Components.

<?php

namespace App\Forms\Components;

use Filament\Forms\Components\Component;

class DescriptionList extends Component
{
    public static function make(): static
    {
        return new static();
    }
}

The first thing we need to do is specify the view that your component uses. This is done via a $view property on the class.

class DescriptionList extends Component
{
    protected string $view = 'filament.forms.components.description-list';
}

I tend to structure my views the same way the classes are structured, with the exception of placing it inside of an extra top-level filament folder for separation from other folders in my application.

The DescriptionList components is going to loop over an array of key value pairs where the key is used as the term <dt> and the value is used as the description <dd>. Let's add a method to the component that accepts that array.

class DescriptionList extends Component
{
    protected string $view = 'filament.forms.components.description-list';

    protected array $items = [];

    public function items(array $items): static
    {
        $this->items = $items;
        
        return $this;
    }
}

Now in your form, we can use the component:

protected function getFormSchema(): array
{
    return [
        DescriptionList::make('overview')
            ->items([
                'Name' => $this->user->name,
                'Email' => $this->user->email,
            ]),
    ];
}

Inside of our view we can start to output some values, but how do we get those values?

Fields and components in Filament are actually all Blade components under the hood, which means all public properties and methods get passed to the view as variables. We can add a new getItems() method to the component class and invoke that inside of our Blade view.

class DescriptionList extends Component
{
    protected string $view = 'filament.forms.components.description-list';

    protected array $items = [];

    // ...

    public function getItems(): array
    {
        return $this->items;
    }
}
<dl>
    @foreach($getItems() as $term => $description)
        <dt>{{ $term }}</dt>
        <dd>{{ $description }}</dd>
    @endforeach
</dl>

And we have some data being rendered now, cool! Let's take this a step further and look at using Closure objects to allow more dynamic lists of data.

The first step is changing what values we accept on our items() method.

class DescriptionList extends Component
{
    protected string $view = 'filament.forms.components.description-list';

    protected array |Closure $items = [];

    public function items(array |Closure $items): static
    {
        $this->items = $items;
        
        return $this;
    }
}

If we use a union type to also accept a Closure, users will be able to use closure customisation to generate dynamic sets of data based on other fields or the current record.

protected function getFormSchema(): array
{
    return [
        DescriptionList::make()
            ->items(function (Model $record, Closure $get) {
                $items = [
                    'Name' => $record->name,
                    'Email' => $record->email,
                ];

                if (!! $get('show_dangerous_things')) {
                    $items['Password'] = magical_function_to_reverse_sha256_hash($record->password);
                }

                return $items;
            }),
    ];
}

The getItems() method also needs to be updated to handle the new Closure object. Filament provides a handy evaluate() method which will take in a value and if it happens to be a Closure, it will run it through Laravel's service container and pass through an array of handy argument such as $record, $get, etc.

If the value provided to evaluate() isn't invokable, it will just return it as is meaning our array types will still work as expected.

class DescriptionList extends Component
{
    public function getItems(): array
    {
        return $this->evaluate($this->items);
    }
}