Custom fields
Introduction¶
You can create a reusable field with a Blade view, React, Vue, Svelte, or framework-free JavaScript. Start by generating a field, then customize its view or renderer. Filament handles the field wrapper, validation, and state binding.
To generate a Blade-based field:
php artisan make:filament-form-field LocationPicker
Use the generated class in your form's schema:
use App\Filament\Forms\Components\LocationPicker;
LocationPicker::make('location')
Edit the generated Blade view to render your input, following custom field classes. For a JavaScript-rendered field, choose one of the options below instead.
Generating a JavaScript field¶
Install your application's JavaScript dependencies, including Vite, before running the generator, for example with npm install.
Add one renderer flag to the command:
php artisan make:filament-form-field LocationPicker --react
php artisan make:filament-form-field LocationPicker --vue
php artisan make:filament-form-field LocationPicker --svelte
php artisan make:filament-form-field LocationPicker --js
The command creates a PHP field class and a text-input renderer in resources/js/filament/forms/components/, without a Blade view. It installs missing dependencies, updates supported Vite configurations, and offers to compile your assets. Use the generated class in your schema as shown above, then replace the starter input with your own UI.
For TypeScript, add --typescript or its alias --ts:
php artisan make:filament-form-field LocationPicker --react --ts
The JavaScript renderer examples show how to build a location picker with each option.
Customizing generation¶
Use --pm=yarn to use Yarn instead of npm, --skip-install to install dependencies yourself, or --skip-build to skip the compilation prompt. Blade-based generation does not run a package manager.
The generated PHP class uses HasJsRenderer and loads its entry through Vite::asset(). The generated files depend on your chosen renderer:
- React:
location-picker.jsx, orlocation-picker.tsxwith--typescript. - Vue:
location-picker.jsandLocationPicker.vue. With--typescript, the entry uses.tsand the component uses<script setup lang="ts">. - Svelte:
location-picker.svelte.jsandLocationPicker.svelte. With--typescript, the entry uses.svelte.tsand the component uses<script lang="ts">. - Framework-free JavaScript:
location-picker.js, orlocation-picker.tswith--typescript.
The React component and Vue or Svelte import are named LocationPicker, matching the PHP class. Names that start with a lowercase letter or conflict with a starter's imports use the local name Component instead. Filenames and the default-exported mountLocationPicker() function still use the requested name.
The starters support React 18 or later, Vue 3.3 or later within Vue 3, and Svelte 5. Missing React installations default to React 19. Existing compatible dependencies are preserved; incompatible versions require a manual upgrade.
Nested field names, such as Maps/LocationPicker, place the JavaScript files in a maps/ subdirectory. TypeScript generation also configures the @filament/forms/js-field type alias.
If you configure an alternative component location, renderer directories follow the generated PHP namespace, with your application's root namespace removed and the remaining directories converted to kebab case. For example, App\Admin\Forms\Components\LocationPicker uses resources/js/admin/forms/components/location-picker.js. Namespaces outside your application's root are retained.
If your configuration cannot be updated automatically, the command prints the remaining manual steps. Follow the Vite module setup to complete them. For a plugin, use a published module instead of the application's Vite manifest.
Custom field classes¶
The Blade-based generator creates a field class that selects its view:
use Filament\Forms\Components\Field;
class LocationPicker extends Field
{
protected string $view = 'filament.forms.components.location-picker';
}
It will also create a view file at resources/views/filament/forms/components/location-picker.blade.php.
Binding a field to its state¶
A field's state is stored in a public property on its Livewire component. Use $getStatePath() in the field's view to bind to the correct property, including when the field is nested inside a repeater or another component. You can bind with wire:model or entangle an Alpine.js property:
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
<input wire:model="{{ $getStatePath() }}" />
<!-- Or -->
<div x-data="{ state: $wire.$entangle('{{ $getStatePath() }}') }">
<input x-model="state" />
</div>
</x-dynamic-component>
See obeying state binding modifiers to support options such as live() and debounce(). Preserve the field's accessibility associations when creating its control markup.
If your field uses third-party JavaScript libraries, you can load its Alpine.js component asynchronously so those assets are only loaded when needed.
Accessing the state of another component in the Blade view¶
Inside the Blade view, you may access the state of another component in the schema using the $get() function:
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
{{ $get('email') }}
</x-dynamic-component>
Accessing the Eloquent record in the Blade view¶
Inside the Blade view, you may access the current Eloquent record using the $record variable:
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
{{ $record->name }}
</x-dynamic-component>
Accessing the current operation in the Blade view¶
Inside the Blade view, you may access the current operation, usually create, edit or view, using the $operation variable:
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
@if ($operation === 'create')
This is a new conference.
@else
This is an existing conference.
@endif
</x-dynamic-component>
Accessing the current Livewire component instance in the Blade view¶
Inside the Blade view, you may access the current Livewire component instance using $this:
@php
use Filament\Resources\Users\RelationManagers\ConferencesRelationManager;
@endphp
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
@if ($this instanceof ConferencesRelationManager)
You are editing conferences the of a user.
@endif
</x-dynamic-component>
Accessing the current field instance in the Blade view¶
Inside the Blade view, you may access the current field instance using $field. You can call public methods on this object to access other information that may not be available in variables:
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
@if ($field->getState())
This is a new conference.
@endif
</x-dynamic-component>
Adding a configuration method to a custom field class¶
You may add a public method to the custom field class that accepts a configuration value, stores it in a protected property, and returns it again from another public method:
use Filament\Forms\Components\Field;
class LocationPicker extends Field
{
protected string $view = 'filament.forms.components.location-picker';
protected ?float $zoom = null;
public function zoom(?float $zoom): static
{
$this->zoom = $zoom;
return $this;
}
public function getZoom(): ?float
{
return $this->zoom;
}
}
Now, in the Blade view for the custom field, you may access the zoom using the $getZoom() function:
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
{{ $getZoom() }}
</x-dynamic-component>
Any public method that you define on the custom field class can be accessed in the Blade view as a variable function in this way.
To pass the configuration value to the custom field class, you may use the public method:
use App\Filament\Forms\Components\LocationPicker;
LocationPicker::make('location')
->zoom(0.5)
Allowing utility injection in a custom field configuration method¶
Utility injection is a powerful feature of Filament that allows users to configure a component using functions that can access various utilities. You can allow utility injection by ensuring that the parameter type and property type of the configuration allows the user to pass a Closure. In the getter method, you should pass the configuration value to the $this->evaluate() method, which will inject utilities into the user's function if they pass one, or return the value if it is static:
use Closure;
use Filament\Forms\Components\Field;
class LocationPicker extends Field
{
protected string $view = 'filament.forms.components.location-picker';
protected float | Closure | null $zoom = null;
public function zoom(float | Closure | null $zoom): static
{
$this->zoom = $zoom;
return $this;
}
public function getZoom(): ?float
{
return $this->evaluate($this->zoom);
}
}
Now, you can pass a static value or a function to the zoom() method, and inject any utility as a parameter:
use App\Filament\Forms\Components\LocationPicker;
LocationPicker::make('location')
->zoom(fn (Conference $record): float => $record->isGlobal() ? 1 : 0.5)
Obeying state binding modifiers¶
When you bind a field to a state path, you may use the defer modifier to ensure that the state is only sent to the server when the user submits the form, or whenever the next Livewire request is made. This is the default behavior.
However, you may use the live() on a field to ensure that the state is sent to the server immediately when the user interacts with the field. This allows for lots of advanced use cases as explained in the reactivity section of the documentation.
Filament provides a $applyStateBindingModifiers() function that you may use in your view to apply any state binding modifiers to a wire:model or $wire.$entangle() binding:
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
<input {{ $applyStateBindingModifiers('wire:model') }}="{{ $getStatePath() }}" />
<!-- Or -->
<div x-data="{ state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$getStatePath()}')") }} }">
<input x-model="state" />
</div>
</x-dynamic-component>
Rendering fields with JavaScript frameworks¶
After generating a JavaScript field, customize its renderer using one of the examples below. Your application owns the renderer and its dependencies; Filament does not bundle a framework.
You can also use JsField directly without generating a PHP class. Pass the URL of your renderer's ES module to renderer():
use Filament\Forms\Components\JsField;
use Illuminate\Support\Facades\Vite;
JsField::make('location')
->renderer(Vite::asset('resources/js/fields/location-picker.jsx'))
->default(['latitude' => 51.5, 'longitude' => -0.12])
Filament loads the module when the field initializes, without a global registry or a script tag. Each field gets its own mounted instance, even when the browser has already loaded the module for another field.
renderer() method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.
Writing a renderer¶
Each renderer exports a mount function that receives host, props, and optional schema utilities. Render inside host, then return update(props) to receive changes and destroy() to clean up. Filament supplies state, accessibility props, and callbacks to connect your input to the form.
Choose one example below. If you used the generator, adapt it in your generated files and keep their existing entry paths. If you create the files manually, follow the Vite module setup to compile the example's entry file.
Creating a React field¶
Create resources/js/fields/location-picker.jsx. The component edits a latitude and longitude, storing an empty input as null. Apply Filament's input props so labels, descriptions, validation, and disabled or read-only states work:
import React from 'react'
import { createRoot } from 'react-dom/client'
function LocationPicker(props) {
return ['latitude', 'longitude'].map((coordinate) => (
<label key={coordinate}>
{coordinate === 'latitude' ? 'Latitude' : 'Longitude'}
<input
id={coordinate === 'latitude' ? props.id : `${props.id}-longitude`}
type="number"
step="any"
value={props.value?.[coordinate] ?? ''}
disabled={props.isDisabled}
readOnly={props.isReadOnly}
required={props.isRequired}
aria-invalid={props.isInvalid}
aria-describedby={props.ariaDescribedBy}
onChange={(event) => props.onChange({
...props.value,
[coordinate]: event.target.value === '' ? null : Number(event.target.value),
})}
onBlur={props.onBlur}
/>
</label>
))
}
export default function mountLocationPicker({ host, props: initialProps }) {
const root = createRoot(host)
const update = (props) => root.render(<LocationPicker {...props} />)
update(initialProps)
return { update, destroy: () => root.unmount() }
}
Creating a Vue field¶
Create resources/js/fields/LocationPicker.vue:
<script setup>
defineOptions({ inheritAttrs: false })
const props = defineProps(['value', 'id', 'isDisabled', 'isReadOnly', 'isRequired', 'isInvalid', 'ariaDescribedBy', 'onChange', 'onBlur'])
function changeCoordinate(coordinate, event) {
props.onChange({
...props.value,
[coordinate]: event.target.value === '' ? null : Number(event.target.value),
})
}
</script>
<template>
<label v-for="coordinate in ['latitude', 'longitude']" :key="coordinate">
{{ coordinate === 'latitude' ? 'Latitude' : 'Longitude' }}
<input
:id="coordinate === 'latitude' ? props.id : `${props.id}-longitude`"
type="number"
step="any"
:value="props.value?.[coordinate] ?? ''"
:disabled="props.isDisabled"
:readonly="props.isReadOnly"
:required="props.isRequired"
:aria-invalid="props.isInvalid"
:aria-describedby="props.ariaDescribedBy"
@input="changeCoordinate(coordinate, $event)"
@blur="props.onBlur()"
/>
</label>
</template>
Create resources/js/fields/location-picker.js to mount it. Use this entry's URL in renderer():
import { createApp, h, shallowRef } from 'vue'
import LocationPicker from './LocationPicker.vue'
export default function mountLocationPicker({ host, props: initialProps }) {
const props = shallowRef(initialProps)
const application = createApp({
setup: () => () => h(LocationPicker, props.value),
})
application.mount(host)
return {
update(nextProps) {
props.value = nextProps
},
destroy: () => application.unmount(),
}
}
Creating a Svelte field¶
Create resources/js/fields/LocationPicker.svelte:
<script>
let { value, id, isDisabled, isReadOnly, isRequired, isInvalid, ariaDescribedBy, onChange, onBlur } = $props()
function changeCoordinate(coordinate, event) {
onChange({
...value,
[coordinate]: event.target.value === '' ? null : Number(event.target.value),
})
}
</script>
{#each ['latitude', 'longitude'] as coordinate (coordinate)}
<label>
{coordinate === 'latitude' ? 'Latitude' : 'Longitude'}
<input
id={coordinate === 'latitude' ? id : `${id}-longitude`}
type="number"
step="any"
value={value?.[coordinate] ?? ''}
disabled={isDisabled}
readonly={isReadOnly}
required={isRequired}
aria-invalid={isInvalid}
aria-describedby={ariaDescribedBy}
oninput={(event) => changeCoordinate(coordinate, event)}
onblur={onBlur}
/>
</label>
{/each}
Create resources/js/fields/location-picker.svelte.js to mount it. The .svelte.js extension allows the Svelte compiler to process $state. Use this entry's URL in renderer():
import { mount, unmount } from 'svelte'
import LocationPicker from './LocationPicker.svelte'
export default function mountLocationPicker({ host, props: initialProps }) {
const props = $state({ ...initialProps })
const component = mount(LocationPicker, { target: host, props })
return {
update: (nextProps) => Object.assign(props, nextProps),
destroy: () => unmount(component),
}
}
Creating a framework-free JavaScript field¶
You do not need a framework to build a JavaScript field. Create resources/js/fields/location-picker.js, add it to your Vite input, and use its built URL in renderer() as shown above. The following renderer creates the same latitude and longitude inputs using browser DOM APIs:
export default function mountLocationPicker({ host, props: initialProps }) {
const coordinates = ['latitude', 'longitude']
let currentProps = initialProps
const inputs = Object.fromEntries(coordinates.map((coordinate) => {
const label = document.createElement('label')
const labelText = document.createTextNode(
coordinate === 'latitude' ? 'Latitude' : 'Longitude',
)
const input = document.createElement('input')
input.type = 'number'
input.step = 'any'
label.append(labelText, input)
host.append(label)
return [coordinate, input]
}))
const onInput = (event) => {
const coordinate = coordinates.find(
(coordinate) => inputs[coordinate] === event.currentTarget,
)
initialProps.onChange({
...currentProps.value,
[coordinate]: event.currentTarget.value === ''
? null
: Number(event.currentTarget.value),
})
}
for (const input of Object.values(inputs)) {
input.addEventListener('input', onInput)
input.addEventListener('blur', initialProps.onBlur)
}
const update = (props) => {
currentProps = props
coordinates.forEach((coordinate) => {
const input = inputs[coordinate]
input.id = coordinate === 'latitude' ? currentProps.id : `${currentProps.id}-longitude`
input.value = currentProps.value?.[coordinate] ?? ''
input.disabled = currentProps.isDisabled
input.readOnly = currentProps.isReadOnly
input.required = currentProps.isRequired
input.setAttribute('aria-invalid', String(currentProps.isInvalid))
input.setAttribute('aria-describedby', currentProps.ariaDescribedBy)
})
}
update(initialProps)
return {
update,
destroy() {
for (const input of Object.values(inputs)) {
input.removeEventListener('input', onInput)
input.removeEventListener('blur', initialProps.onBlur)
}
host.replaceChildren()
},
}
}
The examples leave styling to your application. See registering CSS files to include a stylesheet with your field.
Receiving state and emitting changes¶
Use props.value to render the field's current state and call props.onChange(nextValue) after a user interaction. Always pass the complete value, rather than mutating the incoming object. Values must be JSON-serializable: null, scalars, arrays, or objects. Initialize a particular shape using default(), and handle cleared values in your component.
Filament supplies the complete props to both the mount function and update(props), including onChange() and onBlur(). These callbacks keep the same function references for the mounted field's lifetime, so you can replace props on each update or retain the callbacks for native event listeners. Do not call onChange() when receiving props or rendering the component, as this can create feedback loops.
By default, edits update local Livewire state and are sent on the next request. Use the usual state binding modifiers to change this behavior:
use Filament\Forms\Components\JsField;
use Illuminate\Support\Facades\Vite;
JsField::make('location')
->renderer(Vite::asset('resources/js/fields/location-picker.jsx'))
->live(onBlur: true)
live() sends changes immediately, live(onBlur: true) waits for props.onBlur(), and live(debounce: 500) waits after the latest edit. Other Livewire requests may send pending state earlier.
Applying accessibility and input state¶
Apply the supplied input props as shown in the examples. Use id on the primary input to associate Filament's label, and label additional inputs separately. ariaDescribedBy associates helper text, validation messages, and the required description; append additional description IDs instead of replacing it. Custom wrappers must preserve these associations.
Passing configuration from PHP¶
You can pass configuration to your renderer using rendererConfiguration():
use Filament\Forms\Components\JsField;
use Illuminate\Support\Facades\Vite;
JsField::make('location')
->renderer(Vite::asset('resources/js/fields/location-picker.jsx'))
->rendererConfiguration(['zoom' => 12])
Read it from props.configuration.zoom in your component. Configuration is separate from built-in props such as value and onChange, so it cannot override the state binding. Only pass JSON-serializable data intended for the browser, not secrets. Modifying configuration in JavaScript does not write back to PHP.
In the Vue example, add configuration to defineProps(). In the Svelte example, read configuration from $props() alongside value and the other props.
rendererConfiguration() method also accepts a function to dynamically calculate it. You can inject various utilities into the function as parameters.
Updating configuration from another field¶
For example, you can use the selected country to configure a location picker:
use Filament\Forms\Components\JsField;
use Filament\Schemas\Components\Utilities\Get;
use Illuminate\Support\Facades\Vite;
JsField::make('location')
->renderer(Vite::asset('resources/js/fields/location-picker.jsx'))
->rendererConfiguration(static fn (Get $get): array => [
'country' => $get('country'),
'zoom' => 12,
])
Filament reevaluates the closure when the field renders on the server, then updates the renderer without remounting it. Use live() on the country field to send edits immediately. Requests that skip rendering this field do not update its PHP-derived configuration.
Treat configuration as immutable. Pass null to rendererConfiguration() to clear it. For a reusable field, override getRendererConfiguration(): array instead; its default is [].
Accessing schema utilities¶
Accept utilities alongside host and props in your mount function to access schema utilities. For example, an interaction handler can read or change a sibling country field:
const country = utilities.$get('country')
await utilities.$set('country', 'GB')
$get(path, isAbsolute) and $set(path, value, isAbsolute, isLive = false) use paths relative to the field's container unless isAbsolute is true. $set() is deferred unless isLive is true. It does not apply the target field's read-only configuration or state binding modifiers, so use props.onChange() for ordinary edits to this field.
utilities.$statePath is this field's absolute state path, and utilities.$state reads its current value. Treat returned state as read-only. Keep utilities intact when passing it to your framework: spreading or destructuring its getters captures their current values. Reads do not subscribe to sibling-field changes; use PHP configuration for updates after a server render.
In Svelte components, local names beginning with $ are reserved. Keep property access such as utilities.$get(), or alias destructured functions: const { $get: getState, $set: setState } = utilities.
Calling PHP methods¶
You can expose methods on your custom field class and call them through utilities, such as utilities.$geocodeAddress({ address }). The same method exposure and authorization rules apply to Blade and JavaScript-rendered fields. See calling an exposed method from a framework for an example.
For methods on the owning Livewire component rather than the field class, use utilities.$wire. For example, if your Livewire component defines saveLocation():
await utilities.$wire.$call('saveLocation')
$wire is the original Livewire proxy. Direct writes bypass the field's state binding timing; prefer props.onChange() for field edits. Handle request failures, and stop using utilities when the renderer is destroyed.
Typing renderers¶
Use --typescript or --ts to generate typed starters. Filament includes the declarations in its Composer package, without a JavaScript runtime dependency. See type-checking ES modules for compiler configuration and framework-specific checks.
For an existing renderer, add a type alias to your application's tsconfig.json, relative to that configuration file (or its baseUrl, if configured):
{
"compilerOptions": {
"paths": {
"@filament/forms/js-field": ["./vendor/filament/forms/resources/js/types/js-field.d.ts"]
}
}
}
Use JsFieldRenderer to type your mount function's state and PHP configuration. The following shows where to apply the type to one of the implementations above:
import type { JsFieldRenderer } from '@filament/forms/js-field'
type Location = { latitude: number | null; longitude: number | null } | null
type Configuration = { zoom: number }
const mountLocationPicker: JsFieldRenderer<Location, Configuration> = ({ host, props, utilities }) => {
// Mount your component with `props` and `utilities`.
return {
update(nextProps) {
// Update it with `nextProps.value` and `nextProps.configuration`.
},
destroy() {
// Unmount it and remove subscriptions.
},
}
}
export default mountLocationPicker
You can also import JsFieldRendererContext, JsFieldProps, JsFieldUtilities, and JsFieldRendererInstance to type individual parts of your implementation. JsFieldProps includes callbacks and describes both mount and update props.
State and configuration are deeply read-only snapshots in TypeScript, not runtime-frozen objects. Unspecified types must be narrowed before use. Keep import type so your bundler does not try to load the declaration file, and keep PHP validation even when using TypeScript.
Typing exposed methods¶
For exposed PHP methods, supply a third type argument, such as JsFieldRenderer<Location, Configuration, Methods>. Declare the methods your field exposes; PHP signatures are not automatically translated into TypeScript:
interface Methods {
$geocodeAddress(argumentsObject: { address: string }): Promise<Location>
}
JsFieldLivewire declares $call(), $get(), and $set() on $wire. Use $call() for custom Livewire methods. Other Livewire APIs remain available at runtime; extend the type locally to match your installed Livewire version when using them. In a plugin repository, point the type alias at your development Composer installation.
Synchronizing and disposing renderers¶
Return update(props) and destroy() from your mount function, or return a promise for that object. Server changes update the renderer and cancel a pending debounce when the incoming value differs from the pending edit. Removal cancels pending commits and calls destroy(). A renderer that finishes initializing after removal is immediately destroyed.
Clean up subscriptions, event listeners, and framework roots in destroy(), including when the host is already detached. Stop DOM work synchronously even if cleanup returns a promise. Changes to the renderer URL or state binding configuration remount the renderer with current state; changes to rendererConfiguration() and input settings such as isInvalid, isRequired, isDisabled, and isReadOnly update it in place. Keep persistent data in the field value, not only in component-local state.
Understanding the boundary with Blade and Alpine¶
Render only inside host, whose contents are ignored by Livewire. Do not change its parent or siblings. PHP records, operations, closures, Blade slots, and rendered Filament child schemas are not automatically available inside it. Compute serializable data in rendererConfiguration() and keep server-rendered actions, hints, and child schemas outside the host. Filament and Livewire still own field state, validation, authorization, and HTTP responses.
Alpine directives and magic properties are not injected into React, Vue, or Svelte templates. Use the framework's event, reference, and lifecycle APIs, native DOM events, or the supplied $wire for Livewire-specific operations. There are no dedicated framework adapters for uploads, actions/modals, or sibling-state subscriptions; access to $wire does not automatically make those APIs reactive in your framework. Clean up listeners and subscriptions using their owning API's cleanup mechanism.
If you already load the renderer through another script, you may pass a trusted Filament\Support\RawJs function expression to renderer() instead of a URL.
Handling renderer errors¶
If importing, mounting, or update() fails, Filament shows an accessible error without changing the field value. Check the browser console, correct the module, then reload. Diagnostics include renderer URLs and original errors, so avoid including sensitive data in either. Cleanup failures are reported without blocking other fields' cleanup.
Clean up resources yourself if mounting fails before returning lifecycle methods. Handle errors in framework rendering, event handlers, and independent asynchronous work with your framework's error handling. Manage ordering and cancellation for asynchronous updates: Filament does not serialize them or await cleanup before remounting.
Customizing the Blade view¶
A field using HasJsRenderer does not need a Blade view, but you can set its $view property or call view() to render a custom view instead:
use App\Filament\Forms\Components\LocationPicker;
LocationPicker::make('location')
->view('filament.forms.components.location-picker')
The custom view replaces the default output; it does not automatically mount the JavaScript renderer. To keep the renderer inside your own Blade layout, render the field's toEmbeddedHtml() output in resources/views/filament/forms/components/location-picker.blade.php:
<div>
{!! $field->toEmbeddedHtml() !!}
</div>
This output already includes the field wrapper, so do not add another field wrapper around it. Keep any additional Blade content outside the renderer's host.
Building reusable plugin fields¶
Use HasJsRenderer on your own Field class to build a reusable field without a Blade view. Implement getRenderer() to return your module URL:
namespace Vendor\LocationPicker\Forms\Components;
use Filament\Forms\Components\Concerns\HasJsRenderer;
use Filament\Forms\Components\Field;
use Filament\Support\Facades\FilamentAsset;
class LocationPicker extends Field
{
use HasJsRenderer;
public function getRenderer(): string
{
return FilamentAsset::getScriptSrc('location-picker', 'vendor/location-picker');
}
}
Follow the plugin guide to set up your package and publish its prebuilt module under this asset ID and package name. Consumers can then use LocationPicker::make('location')->live() without a renderer URL, a Blade view, or changes to their Vite setup.
Use the same mount function as for JsField. To pass configuration, override getRendererConfiguration(): array and return your field's evaluated configuration values. You can expose those values through the usual fluent configuration methods.
Calling field methods from JavaScript¶
You can call methods on your custom field class from a Blade view or a JavaScript renderer. For example, a location picker might call a geocoding service in PHP. Expose the method using #[Exposed].
Exposing a method¶
Add #[Exposed] to a public instance method on your field. This Blade-based example also applies to a class using HasJsRenderer:
use Filament\Forms\Components\Field;
use Filament\Support\Components\Attributes\Exposed;
class LocationPicker extends Field
{
protected string $view = 'filament.forms.components.location-picker';
#[Exposed]
public function geocodeAddress(string $address): array
{
// Perform geocoding logic...
return [
'latitude' => $latitude,
'longitude' => $longitude,
];
}
}
Arguments must be a JSON-serializable object keyed by PHP parameter name. Filament binds each call to the current field, including nested repeater instances. The returned promise resolves to the method's return value, or null if the component or exposed method is missing. Calls make Livewire requests and can send pending deferred edits, regardless of live().
Use the PHP method's casing in JavaScript. Built-in utility and Alpine magic names are reserved; a method named get, for example, is available through $callSchemaComponentMethod() without replacing $get(). Do not register Alpine magics with the same names as your exposed methods.
Calling the method from JavaScript¶
In your Blade view, call the exposed method using its $-prefixed name. Pass an object keyed by PHP parameter name. The utility is bound to this field, so you do not need its key. You can also use $callSchemaComponentMethod('geocodeAddress', { address: this.address }), including when a method name collides with a built-in utility such as $get():
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
<div
x-data="{
address: '',
coordinates: null,
async geocode() {
this.coordinates = await this.$geocodeAddress(
{ address: this.address },
)
},
}"
>
<input type="text" x-model="address" />
<button type="button" x-on:click="geocode">Geocode</button>
<template x-if="coordinates">
<p x-text="`${coordinates.latitude}, ${coordinates.longitude}`"></p>
</template>
</div>
</x-dynamic-component>
Calling an exposed method from a framework¶
In a JavaScript renderer, the exposed methods are available on utilities. Call them from an interaction handler and pass the result to props.onChange():
const coordinates = await utilities.$geocodeAddress({ address: '10 Downing Street, London' })
if (coordinates !== null) {
props.onChange(coordinates)
}
You can also call utilities.$callSchemaComponentMethod('geocodeAddress', { address }). Handle request failures in your component, and avoid updating it if it has been destroyed while awaiting the response. See typing renderers to declare named methods in TypeScript.
Preventing re-renders¶
By default, calling an exposed method triggers a re-render of the Livewire component. If your method only returns data and does not change state or PHP props that need rendering, add Livewire's #[Renderless] attribute. Other pending updates or calls in the same request may still require rendering:
use Filament\Forms\Components\Field;
use Filament\Support\Components\Attributes\Exposed;
use Livewire\Attributes\Renderless;
class LocationPicker extends Field
{
protected string $view = 'filament.forms.components.location-picker';
#[Exposed]
#[Renderless]
public function geocodeAddress(string $address): array
{
// ...
}
}