Organising Filament Form Sections

When building out resources for a Filament Admin panel the main resource classes can get rather long very quickly. When you have lots of data to model they can get unwieldily.

Initially, I set up my form() fields with a primary area and a sidebar area, using the \Filament\Forms\Components\Grid component. I split each grid column schema() array to use new methods within the same resource class. This helped keep the components organised a little better.

<?php // UserResource.php

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Grid::make(6)
                ->schema([
                    Grid::make()
                        ->schema(self::primary())
                        ->columnSpan(4),

                    Grid::make()
                        ->schema(self::sidebar())
                        ->columnSpan(2),
                ]),
        ]);
}

protected static function primary(): array
{
	return [
		// Primary components in here.
	];
}

protected static function sidebar(): array
{
	return [
		// Sidebar components in here.
	];
}

This helped separate the grid layout from the actual layout and form field components, but it still resulted in large resource classes. It also helped reduce the large indentation look of the arrays.

Separate Component Classes

To solve the length of resources classes, I moved each main section into separate classes. Each grid column (primary / sidebar) might have multiple layout components. Each of these could then contain multiple component configurations defined inside the schema() for that layout component.

I created a new component class for each of the layout components I wanted to create. I started with extending the \Filament\Forms\Components\Section, which generates a simple contained area. For each custom layout component, I added the appropriate form field components to the schema().

I defined some basic settings on how the layout component should render, such as how many columns() should be used. I also inferred the layout component heading() using the class name and some Laravel fluent string helpers.

<?php

namespace App\Filament\Components\User\Forms;

use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Str;

class PersonalDetails extends Section
{
    protected function setUp(): void
    {
        parent::setUp();

        $this
            ->heading(
	            // Renders "Personal Details" as the heading.
	            Str::of(static::class)->classBasename()->headline()
	        )
            ->columns(2)
            ->schema([
                TextInput::make('firstname')
                    ->label('First Name')
                    ->required(),

                TextInput::make('lastname')
                    ->label('Last Name')
                    ->required(),

                TextInput::make('email')
                    ->email()
                    ->required()
                    ->columnSpanFull(),
            ]);
    }
}

Common Components

As well as creating custom components for individual resources, generic custom components can be created. This allows their form field schemas can be reused across multiple resources. Many resources have created_at and modified_at timestamps and they might also have a status flag. These can be grouped into a component and then integrated with multiple resources easily.

<?php

namespace App\Filament\Components\Forms;

use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Toggle;
use Illuminate\Support\Str;

class Status extends Section
{
    protected function setUp(): void
    {
        parent::setUp();

        $this
            ->heading(
	            Str::of(static::class)->classBasename()->headline()
	        )
            ->schema([
	            DatePicker::make('created_at')
                    ->label('Created')
                    ->inlineLabel()
                    ->readOnly()
                    ->disabled(),
                    
	            DatePicker::make('modified_at')
                    ->label('Modified')
                    ->inlineLabel()
                    ->readOnly()
                    ->disabled(),
                    
                Toggle::make('active')
                    ->inlineLabel()
	                ->required(),
            ]);
    }
}

Updating the Resource

In the resource, I then replaced the primary() and sidebar() schema calls with an array of the new custom components. Instead of three form field components for item details, five for an address, an upload, two date fields and a toggle – a total of twelve (12) or more component configurations, there are just four custom components.

I find this more concise and easier to understand when looking at the overall resource.

Now the resource class form() method looks something like this.

<?php // UserResource.php

use App\Filament\Components\Forms\Status;
use App\Filament\Components\User\Forms;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Grid::make(6)
                ->schema([
                    Grid::make()
                        ->schema([
	                        Forms\PersonalDetails::make(),
	                        Forms\Address::make(),
                        ])
                        ->columnSpan(4),

                    Grid::make()
                        ->schema([
	                        Forms\Avatar::make(),
	                        Status::make(),
						])
                        ->columnSpan(2),
                ]),
        ]);
}