跳到主要内容
版本:3.x

高级表单

概述

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 方法中很有用。

注入当前表单操作

如果你为面板资源或者关联管理器编写表单,你想检测表单为 createedit 或者 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() 方法,并根据情况返回 truefalse。该函数可以将 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()
->afterStateUpdated(fn (Set $set, ?string $state) => $set('slug', Str::slug($state)))

TextInput::make('slug')

本例中,title 字段是 live() 实时更新的。这允许在 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()
->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'),
])

本例中,titledescriptionimage 字段从 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()