Advanced Filament: Custom Field Plugins, Custom Columns, and Render Hooks
#filament #laravel #php #filament-plugins

Advanced Filament: Custom Field Plugins, Custom Columns, and Render Hooks

3 min read Mohamed Said Mohamed Said

Why Extend Filament at the Component Level

Filament ships with a rich set of fields and columns, but production panels inevitably need components that don't exist yet: a colour-swatch column, a money-input field with currency selection, a sidebar widget injected via a render hook. Reaching for a raw Blade view every time creates drift. Building a proper plugin — even an internal one — gives you auto-discovery, configuration, and testability.


Building a Custom Field Plugin

A Filament field is a PHP class that extends Filament\Forms\Components\Field and pairs with a Blade view. The plugin wrapper registers it cleanly.

// src/Fields/MoneyInput.php
namespace Acme\FilamentMoney\Fields;

use Filament\Forms\Components\Field;

class MoneyInput extends Field
{
    protected string $view = 'filament-money::fields.money-input';

    protected string $currency = 'USD';

    public function currency(string $currency): static
    {
        $this->currency = $currency;
        return $this;
    }

    public function getCurrency(): string
    {
        return $this->currency;
    }
}
{{-- resources/views/fields/money-input.blade.php --}}
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
    <div class="flex items-center gap-2">
        <span class="text-sm font-medium text-gray-500">{{ $getRecord()?->currency ?? $getCurrency() }}</span>
        <input
            x-bind:value="$wire.entangle('{{ $getStatePath() }}')"
            type="number"
            step="0.01"
            {{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}"
            class="block w-full rounded-md border-gray-300 shadow-sm"
        />
    </div>
</x-dynamic-component>

The $applyStateBindingModifiers() helper respects lazy, debounce, and live modifiers set by the form author — always use it instead of hardcoding wire:model.

Service Provider and Auto-Discovery

namespace Acme\FilamentMoney;

use Filament\Support\Assets\Css;
use Filament\Support\Facades\FilamentAsset;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;

class FilamentMoneyServiceProvider extends PackageServiceProvider
{
    public function configurePackage(Package $package): void
    {
        $package->name('filament-money')->hasViews('filament-money');
    }

    public function packageBooted(): void
    {
        FilamentAsset::register([
            Css::make('filament-money', __DIR__.'/../dist/filament-money.css'),
        ], 'acme/filament-money');
    }
}

Filament's FilamentAsset facade handles asset versioning and panel-scoped injection — no manual Vite config needed in the consuming app.


Custom Table Columns

Custom columns extend Filament\Tables\Columns\Column. The key is overriding getState() when you need derived data, and providing a typed view.

namespace Acme\FilamentMoney\Columns;

use Filament\Tables\Columns\Column;

class MoneyColumn extends Column
{
    protected string $view = 'filament-money::columns.money-column';

    protected string $currency = 'USD';

    public function currency(string $currency): static
    {
        $this->currency = $currency;
        return $this;
    }

    public function getFormattedState(): string
    {
        $amount = $this->getState();
        if ($amount === null) return '—';

        return number_format((float) $amount / 100, 2) . ' ' . $this->currency;
    }
}
{{-- resources/views/columns/money-column.blade.php --}}
<div class="px-4 py-2 text-right tabular-nums">
    {{ $getFormattedState() }}
</div>

Keep column views stateless — no wire:model, no Alpine state. Columns render inside a Livewire table loop; any reactive state belongs in a custom action or a slide-over, not the cell itself.


Render Hooks: Injecting Into Panel Layouts

Render hooks let you inject Blade content at named positions in any Filament panel without overriding core views.

// In a PanelProvider or plugin's register() method
use Filament\Support\Facades\FilamentView;
use Filament\View\PanelsRenderHook;

FilamentView::registerRenderHook(
    PanelsRenderHook::SIDEBAR_NAV_END,
    fn (): string => view('filament-money::hooks.upgrade-banner')->render(),
);

Scope hooks to specific panels to avoid bleed across multi-panel setups:

FilamentView::registerRenderHook(
    PanelsRenderHook::PAGE_HEADER_ACTIONS_BEFORE,
    fn (): string => view('partials.environment-badge')->render(),
    scopes: \App\Filament\AdminPanelProvider::class,
);

The scopes parameter accepts a single class string or an array — essential when you run separate admin and app panels in the same installation.


Key Takeaways

  • Extend Field and Column directly; pair each with a dedicated Blade view that uses Filament's helper methods ($getStatePath(), $applyStateBindingModifiers()).
  • Register assets via FilamentAsset — it handles versioning and panel scoping automatically.
  • Keep column views stateless; reactive behaviour belongs in actions or slide-overs.
  • Use PanelsRenderHook constants (not raw strings) and always scope hooks to the target panel in multi-panel apps.
  • Even internal plugins deserve a ServiceProvider — it makes testing, versioning, and team onboarding dramatically easier.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can I use Livewire `wire:model` directly inside a custom column view?
No. Table columns render inside a Livewire loop and are not individually reactive. Use `wire:model` only in Field views. For interactive column behaviour, open an inline action or a slide-over panel instead.
Q02 How do I scope a render hook to only one panel when running multiple Filament panels?
Pass the `scopes` argument to `FilamentView::registerRenderHook()` with the fully-qualified class name of your target `PanelProvider`. This prevents the hook from rendering in every panel on the same installation.
Q03 Do I need to publish Filament's views to create a custom field view?
No. Custom field and column views live in your own package or app namespace. You register the view path via `loadViewsFrom()` in your service provider and reference it with a namespaced string like `filament-money::fields.money-input`.

Continue reading

More Articles

View all