表单构造器
高级表单
概述
Filament 表单构造器的设计具有灵活性且可自定义.许多现有的表单构造器允许用户定义表单 Schema,但没有为字段间交互或自定义逻辑提供良好的接口。由于所有 Filament 表单都建立在 Livewire 之上,表单可以动态地适配用户输入,即使在初始化渲染之后也是如此。开发者通过参数注入实时访问许多 utilities,并且基于用户输入创建动态表单。字段的生命周期使用钩子函数开放给扩展,用来定义每个字段的自定义功能。这就允许开发者轻松创建复杂表单。
字段动态响应基础
Livewire 是一个工具,它允许使用 Blade 渲染的 HTML 在不要求整页重载的情况下,动态地重新渲染。Filament 表单是建立在 Livewire 基础上的,因此它们可以动态地重新渲染,以在初始化渲染之后适配它们的布局。
默认情况下,用户使用字段时,表单将不再重新渲染。由于渲染需要往返于服务器,因此这是一种性能优化。但是,如果希望在用户与字段交互后重新渲染表单,可以使用 live()
方法:
use Filament\Forms\Components\Select; Select::make('status') ->options([ 'draft' => 'Draft', 'reviewing' => 'Reviewing', 'published' => 'Published', ]) ->live()
本例中,用户修改 status
字段的值后,表单会重新渲染。这就允许你基于 status
字段的新值,在表单中对字段进行修改。同时,你也可以深入字段的生命周期,在字段更新时执行自定义逻辑。
动态响应字段失焦
默认情况下,如果字段设为 live()
,表单将在每次字段交互时重新渲染。不过,对于某些字段,比如文本输入,这可能不合适,因为在用户输入区间发送网络请求会导致性能不佳。你可以让表单只有在用户完成字段输入后再重新渲染,也就是表单失去焦点后。使用 live(onBlur: true)
方法可以实现该功能:
use Filament\Forms\Components\TextInput; TextInput::make('username') ->live(onBlur: true)
动态响应字段防抖
使用 "debouncing",可以在 live()
和 livewire(onBlur: true)
之间找到一个中间方案。Debouncing(防抖)将会阻止网络请求,直至用户输入停止一段特定时间。你可以使用 live(debounce: 500)
方法来实现该功能:
use Filament\Forms\Components\TextInput; TextInput::make('username') ->live(debounce: 500) // Wait 500ms before re-rendering the form.
本例中,500
是发送请求前等待的毫秒数。你可以自定义该数字,甚至使用 '1s'
这样的字符串。
表单组件 utility 注入
用于配置字段和布局组件的绝大多数方法都接受函数作为参数,替代硬编码值:
use App\Models\User;use Filament\Forms\Components\DatePicker;use Filament\Forms\Components\Select;use Filament\Forms\Components\TextInput; DatePicker::make('date_of_birth') ->displayFormat(function () { if (auth()->user()->country_id === 'us') { return 'm/d/Y'; } return 'd/m/Y'; }) Select::make('user_id') ->options(function (): array { return User::all()->pluck('name', 'id')->all(); }) TextInput::make('middle_name') ->required(fn (): bool => auth()->user()->hasMiddleName())
这就解锁了许多自定义的可能。
本包也可以将许多 utilities 作为参数注入到这些函数中。所有接收函数作为参数的自定义方法都可以注入 utilities。
这些注入的 utilities 要求使用特定的参数名。否则 Filament 不会知道注入的是什么。
注入字段的当前状态
如果你想访问字段的当前状态(值),请定义 $state
参数:
function ($state) { // ...}
注入当前表单实例
如果你想访问当前组件实例,请定义 $component
参数:
use Filament\Forms\Components\Component; function (Component $component) { // ...}
注入当前 Livewire 组件实例
如果你想访问当前 Livewire 组件实例,请定义 $livewire
参数:
use Livewire\Component as Livewire; function (Livewire $livewire) { // ...}
注入当前表单记录
如果表单与 Eloquent 模型实例相关联,请定义 $record
参数:
use Illuminate\Database\Eloquent\Model; function (?Model $record) { // ...}
注入另一个字段的状态
使用 $get
参数,你也可以在回调函数中检索零一个字段的状态(值):
use Filament\Forms\Get; function (Get $get) { $email = $get('email'); // Store the value of the `email` field in the `$email` variable. //...}
注入函数设置另一个字段的状态
与 $get
类似的方式,你可以使用 $set
参数,在回调中设置另一个字段的值:
use Filament\Forms\Set; function (Set $set) { $set('title', 'Blog Post'); // Set the `title` field to `Blog Post`. //...}
当该函数运行时,title
字段的状态将会更新,表单将会使用新 title 重新渲染。这在 afterStateUpdated
方法中很有用。
注入当前表单操作
如果你为面板资源或者关联管理器编写表单,你想检测表单为 create
、edit
或者 view
时,请使用 $operation
参数:
function (string $operation) { // ...}
在面板之外,在表单定义中使用
operation()
方法,你可以设置表单的操作。
注入多个 utilities
参数使用反射动态注入,因此你可以以任何顺序联合使用多个参数:
use Filament\Forms\Get;use Filament\Forms\Set;use Livewire\Component as Livewire; function (Livewire $livewire, Get $get, Set $set) { // ...}
注入 Laravel 容器的依赖
你也可以注入来自 Laravel 容器的任何东西:
use Filament\Forms\Set;use Illuminate\Http\Request; function (Request $request, Set $set) { // ...}
字段生命周期
表单的每个字段都有生命周期,即表单加载后,与用户交互以及表单提交后的处理过程。使用各运行阶段的函数,你可以自定义生命周期每个阶段要做的事情。
字段 hydration
Hydration 是使用数据填充字段的处理过程。它在表单调用 fill()
方法运行。使用 afterStateHydrated()
方法,你可以自定义字段填充后的操作。
本例中,name
字段总是使用正确的大写名称进行填充:
use Closure;use Filament\Forms\Components\TextInput; TextInput::make('name') ->required() ->afterStateHydrated(function (TextInput $component, string $state) { $component->state(ucwords($state)); })
你也可以使用 formatStateUsing()
方法作为填充数据时格式化字段状态的快捷方式:
use Closure;use Filament\Forms\Components\TextInput; TextInput::make('name') ->formatStateUsing(fn (string $state): string => ucwords($state))
字段更新
你也可以使用 afterStateUpdated
方法,自定义用户更新字段后的操作。只有由用户在前端修改才会触发该函数,使用 $set()
或者其他 PHP 函数不会手动更改状态。
在此函数中,你也可以使用 $old
参数注入字段更新前的旧值:
use Filament\Forms\Components\TextInput; TextInput::make('name') ->afterStateUpdated(function (?string $state, ?string $old) { // ... })
如何使用该函数的示例,请查询使用标题自动生成 slug。
字段 dehydration
Dehydration 是在表单中从字段上获取数据,进行转换的处理过程。它在你调用表单的 getState()
方法时运行。
使用 dehydrateStateUsing()
函数,你可以自定义状态值如何转换。本例中,name
字段总是使用正确的大写名进行 dehydrated:
use Filament\Forms\Components\TextInput; TextInput::make('name') ->required() ->dehydrateStateUsing(fn (string $state): string => ucwords($state))
阻止字段被 dehydrated
使用 dehydrated(false)
方法,你可以阻止字段被一起 dehydrated。本例中,该字段不会显示在从 getState()
返回的数组中:
use Filament\Forms\Components\TextInput; TextInput::make('password_confirmation') ->password() ->dehydrated(false)
如果你的表单将数据自动保存到数据库,比如资源和表格 Action,这可以避免将纯粹用于展示的数据保存到数据库。
动态响应表单手册
这一部分包含创建高级表单时,你可能需要执行的常见任务用法集合。
条件性隐藏字段
要条件性地隐藏或者展示一个字段,你可以传递一个参数到 hidden()
方法,并根据情况返回 true
或 false
。该函数可以将 utilities 作为参数注入,因此你可以像检测另一个字段的值那样操作:
use Filament\Forms\Get;use Filament\Forms\Components\Checkbox;use Filament\Forms\Components\TextInput; Checkbox::make('is_company') ->live() TextInput::make('company_name') ->hidden(fn (Get $get): bool => ! $get('is_company'))
本例中,is_company
复选框是实时更新的live()
。这允许表单在 is_company
字段值改变时重新渲染。你可以在 hidden()
函数中使用 $get()
访问该字段的值。该字段的值使用 !
反转,因此,在 is_company
字段为 false
时,company_name
字段将隐藏起来。
此外,你也可以使用 visible()
方法条件性地显示字段。它与 hidden()
正好相反:
use Filament\Forms\Get;use Filament\Forms\Components\Checkbox;use Filament\Forms\Components\TextInput; Checkbox::make('is_company') ->live() TextInput::make('company_name') ->visible(fn (Get $get): bool => $get('is_company'))
条件性地使字段成为必需字段
要条件性地使字段成为必需,你可以传入一个函数到 required()
方法,并根据条件返回 true
或者 false
。函数可以将 utilities 作为参数注入,这样你就可以像检测其他字段值那样进行操作。
use Filament\Forms\Get;use Filament\Forms\Components\TextInput; TextInput::make('company_name') ->live(onBlur: true) TextInput::make('vat_number') ->required(fn (Get $get): bool => filled($get('company_name')))
本例中,company_name
字段是实时更新的 live(onBlur: true)
。这允许表单在 company_name
字段值修改及用户点击离开后重新渲染。你可以在 required()
函数中使用 $get()
来访问字段值。字段值使用 filled()
检测,这样当 company_name
字段不为 null
或者空字符串时,vat_number
字段是必需的。其结果是:只有在 company_name
字段填充时,vat_number
字段才是必需的。
使用函数能够以相同的方式让其他验证规则动态化。
使用标题生成 slug
要在用户输入标题时生成 slug,你可以在标题字段中使用 afterStateUpdated()
方法,以设置 $set()
slug 字段的值:
use Filament\Forms\Components\TextInput;use Filament\Forms\Set;use Illuminate\Support\Str; TextInput::make('title') ->live(onBlur: true) ->afterStateUpdated(fn (Set $set, ?string $state) => $set('slug', Str::slug($state))) TextInput::make('slug')
本例中,title
字段是 [live(onBlur: true)](#动态响应字段失焦) 实时更新的。这允许在
title字段值修改时,重新渲染表单。
afterStateUpdated()方法用于在
title 字段状态更新后运行函数。该函数注入 [
$set()](#注入函数设置另一个字段的状态)和
title 字段的新状态。
Str::slug()方法是 Laravel 的一部分,它用于从字符串中生成 slug。然后使用
$set()函数更新
slug` 字段。
有一件需要注意的事情是,用户可能会手动自定义 slug,我们不想在标题修改时重写它们的更改。要阻止此,我们可以使用标题生成的老版本 slug 来计算用户是否对其做了修改。要访问老版本,你可以注入 $old
并获取修改前 slug 的当前值,我们可以使用 $get()
:
use Filament\Forms\Components\TextInput;use Filament\Forms\Get;use Filament\Forms\Set;use Illuminate\Support\Str; TextInput::make('title') ->live(onBlur: true) ->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) { if (($get('slug') ?? '') !== Str::slug($old)) { return; } $set('slug', Str::slug($state)); }) TextInput::make('slug')
依赖 Select 选项
要基于另一个字段的值动态更新 Select 字段选项,你可以传递一个函数到 Select 字段的 options()
方法中。该函数可以将 utilities 作为参数注入,这样你就可以使用 $get()
做像检测另一个字段值这样的事情:
use Filament\Forms\Get;use Filament\Forms\Components\Select; Select::make('category') ->options([ 'web' => 'Web development', 'mobile' => 'Mobile development', 'design' => 'Design', ]) ->live() Select::make('sub_category') ->options(fn (Get $get): array => match ($get('category')) { 'web' => [ 'frontend_web' => 'Frontend development', 'backend_web' => 'Backend development', ], 'mobile' => [ 'ios_mobile' => 'iOS development', 'android_mobile' => 'Android development', ], 'design' => [ 'app_design' => 'Panel design', 'marketing_website_design' => 'Marketing website design', ], default => [], })
本例中,category
字段是 live()
实时更新的。这允许表单在 category
字段值修改时重新渲染。你可以在 options()
函数中使用 $get()
访问那个字段的值。字段的值用于确定哪些选项在 sub_category
字段中可用。PHP 中的 match()
语句用于基于 category
字段值返回选项数组。其结果是:sub_category
字段只会展示选中的 category
字段相关的值。
你可以修改这个例子,使之在函数中查询,从 Eloquent 模型或者其他数据源中加载选项选项:
use Filament\Forms\Get;use Filament\Forms\Components\Select;use Illuminate\Support\Collection; Select::make('category') ->options(Category::query()->pluck('name', 'id')) ->live() Select::make('sub_category') ->options(fn (Get $get): Collection => SubCategory::query() ->where('category', $get('category')) ->pluck('name', 'id'))
基于 Select 选项的动态字段
你可能希望基于字段值渲染不同的字段集,比如 Select。为此,你可传入一个函数到任何布局组件的 schema()
方法中,该函数检查字段值并基于该值返回不同的 Schema。同时,首次加载时,你需要在动态 Schema 中初始化这一新字段。
use Filament\Forms\Components\FileUpload;use Filament\Forms\Components\Grid;use Filament\Forms\Components\Select;use Filament\Forms\Components\TextInput;use Filament\Forms\Get; Select::make('type') ->options([ 'employee' => 'Employee', 'freelancer' => 'Freelancer', ]) ->live() ->afterStateUpdated(fn (Select $component) => $component ->getContainer() ->getComponent('dynamicTypeFields') ->getChildComponentContainer() ->fill()) Grid::make(2) ->schema(fn (Get $get): array => match ($get('type')) { 'employee' => [ TextInput::make('employee_number') ->required(), FileUpload::make('badge') ->image() ->required(), ], 'freelancer' => [ TextInput::make('hourly_rate') ->numeric() ->required() ->prefix('€'), FileUpload::make('contract') ->required(), ], default => [], }) ->key('dynamicTypeFields')
本例中,type
字段是 live()
实时更新的。这允许表单在 type
字段值修改时重新渲染。afterStateUpdated()
方法用于在 type
字段状态更新后运行函数。本例中,我们注入了当前 Select 字段实例,然后我们可以用它来获取持有 Select 组件和 Grid 组件的表单"容器"实例。使用该容器,我们可以使用唯一的键(dynamicTypeFields
)锁定 Grid 组件。使用该 Grid 组件实例,我们可以调用 fill()
,就像我们在普通表单中初始化一样。Grid 组件的 schema()
方法用于基于不同的 type
字段值返回不同的 schema。这是通过使用 $get()
并动态返回不同的 schema 数组。
自动哈希密码字段
有一个密码字段:
use Filament\Forms\Components\TextInput; TextInput::make('password') ->password()
你可以使用 dehydration 函数, 在表单提交时哈希密码:
use Filament\Forms\Components\TextInput;use Illuminate\Support\Facades\Hash; TextInput::make('password') ->password() ->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
不过如果表单用于修改密码,而你不希望字段为空时,修改现有密码。如果字段是 null 或空字符串(使用 filled()
判断)时,你可以阻止字段 dehydrated:
use Filament\Forms\Components\TextInput;use Illuminate\Support\Facades\Hash; TextInput::make('password') ->password() ->dehydrateStateUsing(fn (string $state): string => Hash::make($state)) ->dehydrated(fn (?string $state): bool => filled($state))
不过,如果你想创建用户时,要求密码必填,请注入 operation
参数,然后条件性地让字段必需:
use Filament\Forms\Components\TextInput;use Illuminate\Support\Facades\Hash; TextInput::make('password') ->password() ->dehydrateStateUsing(fn (string $state): string => Hash::make($state)) ->dehydrated(fn (?string $state): bool => filled($state)) ->required(fn (string $operation): bool => $operation === 'create')
保存数据到关联中
如果你在 Livewire 组件中创建表单,请确保你设置好了表单模型。否则,Filament 不知道关联要使用哪个模型进行检索。
除了能够为字段提供结构外,布局组件还能够将其嵌套字段"传送(teleport)"到关联中。Filament 将从 "HasOne"、"BelongsTo" 或 "MorphOne" Eloquent 关联中处理加载数据,然后将数据保存回关联中。要设置此行为,可以在任何布局组件上使用 "relationship()" 方法:
use Filament\Forms\Components\Fieldset;use Filament\Forms\Components\FileUpload;use Filament\Forms\Components\Textarea;use Filament\Forms\Components\TextInput; Fieldset::make('Metadata') ->relationship('metadata') ->schema([ TextInput::make('title'), Textarea::make('description'), FileUpload::make('image'), ])
本例中,title
、description
和 image
字段从 metadata
关联中自动加载并在字段提交后在此保存。如果 metadata
记录不存在,它将自动创建。
该函功能不仅限于 fieldset - 你可以使用任何的布局组件。比如,你可以使用没有样式与之关联的 Group
组件:
use Filament\Forms\Components\Group;use Filament\Forms\Components\TextInput; Group::make() ->relationship('customer') ->schema([ TextInput::make('name') ->label('Customer') ->required(), TextInput::make('email') ->label('Email address') ->email() ->required(), ])
保存数据到 BelongsTo
关联
请注意,如果你要将数据保存到 BelongsTo
关联,那么数据库中的外键字段必须为 nullable()
类型。这是因为 Filament 在保存关联之前先保存表单。由于先保存表单,外键 ID 还不存在,因此它必须是 nullable 的。表单保存后,Filament 立即保存关联,然后再填入外键 ID 并再次保存。
值得注意的是,如果表单模型上有一个观察者,那么可能需要对其进行调整,以确保它不依赖于创建时存在的关联。例如,如果有一个观察者在创建表单时向相关记录发送电子邮件,那么你可能需要切换到使用附加关联后运行的其他挂钩,如 updated()
。
条件性地将数据保存到关联
有时,保存关联记录可能是可选的。如果用户填写客户字段,则将创建/更新客户。否则,将不会创建该客户,或者如果该客户已经存在,则会将其删除。为此,你可以将 condition
函数作为参数传递给 relationship()
,该函数可以使用相关表单的 $state
来确定是否应保存关联:
use Filament\Forms\Components\Group;use Filament\Forms\Components\TextInput; Group::make() ->relationship( 'customer', condition: fn (?array $state): bool => filled($state['name']), ) ->schema([ TextInput::make('name') ->label('Customer'), TextInput::make('email') ->label('Email address') ->email() ->requiredWith('name'), ])
本例中,客户名不是必填项(required()
),同时邮箱地址只有在 name
填充后才是必填的。condition
函数用于检测 name
字段是否填充,如果是将会创建/更新客户。否则,客户不会被创建,或者如果已存在则删除。
将 Livewire 组件插入到表单
你可能会将 Livewire 组件直接插入到表单中:
use Filament\Forms\Components\Livewire;use App\Livewire\Foo; Livewire::make(Foo::class)
如果你渲染了多个相同的 Livewire 组件,请确保将一个唯一的 key()
传给各个组件。
use Filament\Forms\Components\Livewire;use App\Livewire\Foo; Livewire::make(Foo::class) ->key('foo-first') Livewire::make(Foo::class) ->key('foo-second') Livewire::make(Foo::class) ->key('foo-third')
在表单中插入 Livewire 组件
你可以将 Livewire 组件中直接插入到表单中:
use Filament\Forms\Components\Livewire;use App\Livewire\Foo; Livewire::make(Foo::class)
传递参数到 Livewire 组件中
你可以传递参数数组到 Livewire 组件:
use Filament\Forms\Components\Livewire;use App\Livewire\Foo; Livewire::make(Foo::class, ['bar' => 'baz'])
现在,这些参数可以传递给 Livewire 组件的 mount()
方法:
class Foo extends Component{ public function mount(string $bar): void { // ... }}
此外,这些参数将作为 Livewire 的 public 属性提供:
class Foo extends Component{ public string $bar;}
在 Livewire 组件中访问当前记录
在 mount()
方法中使用 $record
参数,或者使用 $record
属性,你可以在 Livewire 组件中访问当前记录:
use Illuminate\Database\Eloquent\Model; class Foo extends Component{ public function mount(?Model $record = null): void { // ... } // or public ?Model $record = null;}
请注意,当记录尚未创建时,它将为 null
。如果你想在记录为 null
时隐藏 Livewire 组件,可以使用 hidden()
方法:
use Filament\Forms\Components\Livewire;use Illuminate\Database\Eloquent\Model; Livewire::make(Foo::class) ->hidden(fn (?Model $record): bool => $record === null)
延迟加载 Livewire 组件
使用 lazy()
方法,你允许可以组件延迟加载:
use Filament\Forms\Components\Livewire;use App\Livewire\Foo; Livewire::make(Foo::class)->lazy()
Edit on GitHubStill need help? Join our Discord community or open a GitHub discussion