Filament Forms Builder 组件详解:用 Block 构建可拖拽的页面内容编辑器
【免费下载链接】filamentA powerful open-source UI framework for Laravel • Build and ship apps & admin panels fast with Livewire项目地址: https://gitcode.com/GitHub_Trending/fi/filament
导读
Filament 是一套基于 Laravel + Livewire 的开源 UI 框架,其forms包提供了丰富的表单组件。其中Builder组件与 Repeater 类似,会输出一组可重复的表单组件 JSON 数组,但它的核心区别在于:Builder 允许你预先定义多种不同的 schema「块」(Block),让用户在任意顺序、任意次数下自由组合。这使得它成为构建营销网站内容、在线表单字段、富内容页面等「内容块驱动」编辑器的理想方案。读完本文,你将掌握 Builder 的完整 API:Block 定义、标签与图标、块预览、增删改排序、折叠、克隆、块选择器自定义、数量限制、跨字段取值以及内置校验规则,并了解其底层源码实现与测试验证。
一、Builder 与 Repeater 的本质区别
在 Filament 的表单体系中,Repeater 与 Builder 常常被放在一起讨论,但二者有明确的职责划分:
- Repeater:只定义一套表单 schema,在列表中重复渲染多次,适用于结构完全一致的重复数据(如订单明细行);
- Builder:定义多套schema 块(Block),每套块拥有独立的字段结构,用户可以在任意顺序下混合组合,适用于「同一字段内包含多种异构结构」的场景。
从源码上看,Builder继承自Filament\Forms\Components\Field(见 Builder.php),并将传入的blocks()直接委托给组件容器——blocks()方法内部实现就是$this->components($blocks),即每个 Block 本质上是一个子组件(Builder.php#L163-L168)。Block则继承自Filament\Schemas\Components\Component(见 Block.php)。
Builder 的典型应用场景:构建网页内容。例如为一个营销官网定义 heading(标题)、paragraph(段落)、image(图片)等块,前端拿到 JSON 后逐块渲染即可。官方文档给出的最简示例:
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder\Block; use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\Select; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; Builder::make('content') ->blocks([ Block::make('heading') ->schema([ TextInput::make('content') ->label('Heading') ->required(), Select::make('level') ->options([ 'h1' => 'Heading 1', 'h2' => 'Heading 2', 'h3' => 'Heading 3', 'h4' => 'Heading 4', 'h5' => 'Heading 5', 'h6' => 'Heading 6', ]) ->required(), ]) ->columns(2), Block::make('paragraph') ->schema([ Textarea::make('content') ->label('Paragraph') ->required(), ]), Block::make('image') ->schema([ FileUpload::make('url') ->label('Image') ->image() ->required(), TextInput::make('alt') ->label('Alt text') ->required(), ]), ])数据存储建议
官方明确建议:Builder 的数据应当存储在数据库的JSON类型列中;如果使用 Eloquent,请务必给该列加上arraycast,否则从模型读取时得到的是字符串而不是数组。生成的数据结构大致如下:
[ [ 'type' => 'heading', 'data' => [ 'content' => '欢迎访问本站', 'level' => 'h1', ], ], [ 'type' => 'paragraph', 'data' => [ 'content' => '这是一段正文……', ], ], ]每个条目包含type(块名)与data(该块的字段数据)两个键。前端即可据此遍历渲染。
Block 的定义与唯一性要求
Block 通过Block::make('name')创建,名称必须全局唯一,并提供一个组件 schema。从 Block.php#L26-L45 可以看出,make()会校验名称非空,否则抛出InvalidArgumentException:
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder\Block; use Filament\Forms\Components\TextInput; Builder::make('content') ->blocks([ Block::make('heading') ->schema([ TextInput::make('content')->required(), // ... ]), // ... ])二、设置块的标签(Label)
2.1 默认标签与label()覆盖
默认情况下,块标签会根据块名自动推导(源码 Block.php#L90-L96:块名经 kebab-case 转换、下划线替换为空格并首字母大写)。若要覆盖默认标签,使用label()方法——官方特别推荐结合 Laravel 的翻译字符串实现国际化:
use Filament\Forms\Components\Builder\Block; Block::make('heading') ->label(__('blocks.heading'))2.2 根据块内容动态生成条目标签
同一个label()方法还接受闭包,闭包接收该条目的数据($state变量):
- 当
$state为null时,应返回块选择器中展示的块标签; - 否则返回该条目的自定义标签(可基于字段内容拼接)。
use Filament\Forms\Components\Builder\Block; use Filament\Forms\Components\TextInput; Block::make('heading') ->schema([ TextInput::make('content') ->live(onBlur: true) ->required(), // ... ]) ->label(function (?array $state): string { if ($state === null) { return 'Heading'; } return $state['content'] ?? 'Untitled heading'; })要点:凡是希望在$state中使用的字段,都应标记为->live()(或至少live(onBlur: true)),否则标签不会随输入实时更新。该闭包除了$state外,还可以注入其他工具参数:
| 参数 | 类型 | 说明 |
|---|---|---|
$key | string | 当前块条目的键(UUID 或数字索引) |
$index | int | 当前块条目的零基序号 |
$state | array<string, mixed> | 当前块条目的原始未校验数据 |
对应地,源码中Block::getLabel()的求值会同时传入index、key、state、uuid四个变量(Block.php#L79-L101)。
2.3 关闭条目编号
默认每个条目标签旁会显示一个序号(1、2、3……),可通过blockNumbers(false)关闭:
use Filament\Forms\Components\Builder; Builder::make('content') ->blocks([ // ... ]) ->blockNumbers(false)blockNumbers()同样支持传闭包动态计算。底层上,序号在渲染时通过$itemIndex输出(见 Builder.php#L1596-L1598),而该开关对应属性$hasBlockNumbers,默认值为true(Builder.php#L61)。
三、设置块的图标(Icon)
3.1 为块添加图标
块可以设置图标,展示在块选择器的下拉列表中、标签旁边:
use Filament\Forms\Components\Builder\Block; use Filament\Support\Icons\Heroicon; Block::make('paragraph') ->icon(Heroicon::Bars3BottomLeft)这里使用的是 Filament 内置的Heroicon枚举(也可以在 图标文档 查看其他引入图标的方式)。icon()方法在源码中接受string | BackedEnum | Htmlable | Closure四种类型(Block.php#L52-L62),同样支持闭包动态计算。
3.2 在块头部显示图标
默认情况下,图标只出现在「添加块」的下拉选择器中,块头部并不显示图标。通过blockIcons()开启头部图标展示:
use Filament\Forms\Components\Builder; Builder::make('content') ->blocks([ // ... ]) ->blockIcons()也可以传入布尔值动态控制:
Builder::make('content') ->blocks([ // ... ]) ->blockIcons(FeatureFlag::active())对应源码属性$hasBlockIcons,默认值为false(Builder.php#L63),当为true且块配置了图标时,头部会渲染fi-fo-builder-item-header-icon(Builder.php#L1584-L1586)。
四、块预览(Block Previews)
4.1 用只读预览替代表单
当表单很长时,你可能希望在 Builder 中直接展示块内容的只读预览,而不是整块表单。使用blockPreviews()即可:开启后,每个块渲染的是preview()指定的 Blade 视图,而不是它的表单。块的原始数据会以与字段同名的变量传入该 Blade 视图:
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder\Block; use Filament\Forms\Components\TextInput; Builder::make('content') ->blockPreviews() ->blocks([ Block::make('heading') ->schema([ TextInput::make('text') ->placeholder('Default heading'), ]) ->preview('filament.content.block-previews.heading'), ])对应的 Blade 视图(如resources/views/filament/content/block-previews/heading.blade.php):
<h1> {{ $text ?? 'Default heading' }} </h1>blockPreviews()也可以传入布尔值动态控制显示与否:
Builder::make('content') ->blocks([ // ... ]) ->blockPreviews(FeatureFlag::active())底层实现中,preview()存储在块的$preview属性,renderPreview()通过view($previewView, $data)把块数据作为视图变量渲染(见 HasPreview.php);渲染逻辑位于 Builder.php#L1642-L1650——只要hasBlockPreviews()且块有预览,就渲染预览区而非表单。
4.2 交互式块预览
默认情况下,预览内容不可交互,点击预览区域会打开该块的「编辑」模态框来管理设置。如果你希望预览中的链接、按钮保持可点击(交互),为blockPreviews()传入命名参数areInteractive: true:
use Filament\Forms\Components\Builder; Builder::make('content') ->blockPreviews(areInteractive: true) ->blocks([ // ])areInteractive参数同样接受闭包。从源码看,交互式预览会为预览容器加上fi-interactive类,并隐藏编辑遮罩层(Builder.php#L1646-L1658)。
五、添加、删除、排序条目
5.1 添加条目
Builder 底部默认显示一个「添加」按钮,点击后弹出块选择器,选择块类型即可插入新条目。
- 自定义添加按钮文案:使用
addActionLabel()。
use Filament\Forms\Components\Builder; Builder::make('content') ->blocks([ // ... ]) ->addActionLabel('Add a new block')说明:
addActionLabel()也支持闭包动态计算。对应默认文案见源码getAddActionLabel()(Builder.php#L988-L993),默认会拼接字段标签如 "Add content"。
- 调整添加按钮对齐方式:默认居中,可用
addActionAlignment()配合Filament\Support\Enums\Alignment枚举改为左对齐(Alignment::Start)或右对齐(Alignment::End):
use Filament\Forms\Components\Builder; use Filament\Support\Enums\Alignment; Builder::make('content') ->schema([ // ... ]) ->addActionAlignment(Alignment::Start)源码中getAddActionAlignment()会把字符串值转换为Alignment枚举(Builder.php#L232-L241),并对齐到下拉浮层的 placement。
- 禁止添加:
addable(false)。
Builder::make('content') ->blocks([ // ... ]) ->addable(false)5.2 删除条目
每个条目头部默认显示删除按钮。
- 禁止删除:
deletable(false)。
Builder::make('content') ->blocks([ // ... ]) ->deletable(false)5.3 排序条目
默认每个条目支持拖拽排序。
- 禁止排序:
reorderable(false)。
Builder::make('content') ->blocks([ // ... ]) ->reorderable(false)- 改用上下按钮排序:
reorderableWithButtons(),也可传布尔值控制:
Builder::make('content') ->blocks([ // ... ]) ->reorderableWithButtons()Builder::make('content') ->blocks([ // ... ]) ->reorderableWithButtons(FeatureFlag::active())- 仅禁用拖拽(保留按钮排序):
reorderableWithDragAndDrop(false):
Builder::make('content') ->blocks([ // ... ]) ->reorderableWithDragAndDrop(false)源码佐证:以上开关对应$isReorderable(默认true)、$isReorderableWithDragAndDrop(默认true)、$isReorderableWithButtons(默认false)三个属性(Builder.php#L49-L53)。拖拽手柄与上下移动按钮的可见性分别由isReorderableWithDragAndDrop()、isReorderableWithButtons()决定,且都会与isReorderable()取与(Builder.php#L1009-L1017)。另外注意:组件处于禁用(disabled)状态时,添加、删除、排序功能会整体失效(见isAddable()、isDeletable()、isReorderable()中isDisabled()的短路判断,Builder.php#L1000-L1039)。
六、折叠与延迟加载
6.1 折叠条目
长表单中,可以让 Builder 条目可折叠,以隐藏冗长的字段:
Builder::make('content') ->blocks([ // ... ]) ->collapsible()还可以让所有条目默认折叠:
Builder::make('content') ->blocks([ // ... ]) ->collapsed()两者均可传入布尔值动态控制:
Builder::make('content') ->blocks([ // ... ]) ->collapsible(FeatureFlag::active()) ->collapsed(FeatureFlag::active())注意:从 Builder.php#L1487-L1505 可以看到,当条目数 ≥ 2 且可折叠时,Builder 顶部还会出现「全部折叠 / 全部展开」的快捷链接。
6.2 延迟加载块 schema(Deferred Loading)
如果某些块的 schema 渲染开销很大(例如包含富文本编辑器或文件上传),且条目又默认折叠,可以用延迟加载优化性能:把Schema对象传给schema(),并调用deferLoading()。每个块条目的 schema 会独立地在对应条目被展开、进入视口时才加载:
use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder\Block; use Filament\Forms\Components\TextInput; use Filament\Schemas\Schema; Builder::make('content') ->blocks([ Block::make('heading') ->schema( Schema::make() ->components([ TextInput::make('content') ->label('Heading') ->required(), ]) ->deferLoading(), ), // ... ]) ->collapsed()Builder 的块条目 schema 会自动从其条目状态路径获得唯一 key。更多关于延迟 schema 的内容可参考 Schema 文档。
七、克隆条目
如果希望用户能快速复制已有条目(包括其数据),使用cloneable():
Builder::make('content') ->blocks([ // ... ]) ->cloneable()克隆能力由CanBeClonedtrait 提供(见 CanBeCloned.php),默认$isCloneable = false。克隆动作在源码中会为目标条目生成新的 UUID key 并复制其完整数据(Builder.php#L338-L348);当条目数已达maxItems上限或组件被禁用时,克隆按钮不会渲染。
八、自定义块选择器(Block Picker)
添加条目时弹出的下拉即为「块选择器」,默认只有 1 列,可以通过以下两个方法自定义:
8.1 修改块选择器列数blockPickerColumns()
Builder::make() ->blockPickerColumns(2) ->blocks([ // ... ])该方法有两种用法:
- 传整数:如
blockPickerColumns(2),该整数表示lg断点及以上使用的列数;更小设备始终为 1 列; - 传数组:键为断点、值为列数。如
blockPickerColumns(['md' => 2, 'xl' => 4])表示中等屏幕 2 列、超大屏幕 4 列;更小设备默认 1 列,除非使用default键单独指定。
断点sm、md、lg、xl、2xl由 Tailwind 定义。源码中getBlockPickerColumns()的默认结构即为['default' => 1, 'sm' => null, 'md' => null, 'lg' => null, 'xl' => null, '2xl' => null](Builder.php#L1132-L1139),整数参数会自动归一化为['lg' => 整数](Builder.php#L1113-L1117)。
8.2 调整块选择器宽度blockPickerWidth()
增加列数后,下拉宽度会随列数按档位递增;如需手动精确控制最大宽度,使用blockPickerWidth()。可选值与 Tailwind 的 max-width 刻度对应:xs、sm、md、lg、xl、2xl、3xl、4xl、5xl、6xl、7xl:
Builder::make() ->blockPickerColumns(3) ->blockPickerWidth('2xl') ->blocks([ // ... ])有趣的是,源码会自动计算默认宽度:2 列 →md、3 列 →2xl、4 列 →4xl、5 列 →6xl、6 列 →7xl(Builder.php#L1200-L1207),手动设置即覆盖此默认。
九、限制块的重复使用次数
默认每个块可以在 Builder 中无限次使用。若要限制,可在Block 上调用maxItems():
use Filament\Forms\Components\Builder\Block; Block::make('heading') ->schema([ // ... ]) ->maxItems(1)注意这里的maxItems()是 Block 的方法(Block.php#L64-L74),用于限制同一块类型在 Builder 中出现的最大次数。当某块达到上限后,它会从块选择器中消失(getBlockPickerBlocks()会过滤掉已达上限的块,见 Builder.php#L1079-L1100)。该值也支持闭包动态计算。
十、跨字段取值:$get()/$set()的路径语义
所有表单组件都可以用$get()/$set()读取/写入其他字段的值(参见 Forms 概览),但在 Builder 的 schema 内部使用时需要注意作用域问题。
关键规则:$get()/$set()默认以当前 Builder 条目为作用域。也就是说,在某个块条目内部调用$get('foo'),实际查找的是「当前条目下的foo」,而不是 Builder 外部的foo。这一设计让你无需知道当前组件属于哪个条目,就能轻松读取同一条目内的其他字段。
其副作用是:你可能无法直接访问 Builder 外部的字段。解决办法是使用../语法上跳一级:$get('../parent_field_name')。
考虑如下数据结构:
[ 'client_id' => 1, 'builder' => [ 'item1' => [ 'service_id' => 2, ], ], ]假设你正处在builder.item1这个条目内部,想读取外部的client_id:
$get()相对于当前条目,因此$get('client_id')实际等价于查找builder.item1.client_id(不存在);- 使用
../向上跳一级:$get('../client_id')等价于查找builder.client_id;$get('../../client_id')等价于查找client_id(即顶层字段)。
特殊情形:$get()无参数、$get('')或$get('./'),始终返回当前 Builder 条目的完整数据数组。
十一、Builder 校验规则
除 Validation 文档 中列出的通用规则外,Builder 还有专属规则。
11.1 条目数量校验minItems()/maxItems()
use Filament\Forms\Components\Builder; Builder::make('content') ->blocks([ // ... ]) ->minItems(1) ->maxItems(5)minItems()/maxItems()在 Builder 上用于限制整个 Builder 的条目总数,二者均可传闭包。底层由CanLimitItemsLengthtrait 实现(CanLimitItemsLength.php):设置后会自动追加array规则(min:N/max:N)到校验规则集。另外,Builder 在脱水校验时还会强制要求每个条目的type字段必填({$statePath}.*.type => ['required'],见 Builder.php#L1238-L1243)。
区分两者:
Builder::maxItems()限制条目总数;Block::maxItems()限制同一块类型的出现次数。
测试用例可佐证 Builder 的校验与状态处理行为,例如 BuilderTest.php 中验证了fillForm()后assertSchemaStateSet()能完整还原['type' => ..., 'data' => [...]]结构,以及块内字段使用distinct()校验时,重复值会精确地报在builder.0.data.foo/builder.1.data.foo这类路径上。
十二、定制 Builder 条目操作(Actions)
Builder 内部的每个按钮都是 Filament 的 Action 对象,可以通过「操作注册方法」传入闭包进行定制。闭包接收$action对象,进而使用 Actions 文档 中的全部定制能力。
可定制的操作方法如下:
| 方法 | 作用 |
|---|---|
addAction() | 添加条目(底部按钮) |
addBetweenAction() | 在两条目之间插入 |
cloneAction() | 克隆条目 |
collapseAction() | 折叠单个条目 |
collapseAllAction() | 折叠全部 |
deleteAction() | 删除条目 |
expandAction() | 展开单个条目 |
expandAllAction() | 展开全部 |
moveDownAction() | 下移 |
moveUpAction() | 上移 |
reorderAction() | 拖拽排序 |
示例:修改「全部折叠」按钮文案:
use Filament\Actions\Action; use Filament\Forms\Components\Builder; Builder::make('content') ->blocks([ // ... ]) ->collapseAllAction( fn (Action $action) => $action->label('Collapse all content'), )12.1 用模态框确认操作
可以对支持网络请求的操作使用requiresConfirmation()弹出确认模态框,并可结合 Actions Modals 文档 中的任意模态定制方法:
use Filament\Actions\Action; use Filament\Forms\Components\Builder; Builder::make('content') ->blocks([ // ... ]) ->deleteAction( fn (Action $action) => $action->requiresConfirmation(), )限制说明:
addAction()、addBetweenAction()、collapseAction()、collapseAllAction()、expandAction()、expandAllAction()和reorderAction()不支持确认模态框——因为这些按钮的点击不会发出展示模态框所需的网络请求。
12.2 在条目头部添加自定义操作
extraItemActions()允许你向每个 Builder 条目的头部追加自定义 Action 按钮:
use Filament\Actions\Action; use Filament\Forms\Components\Builder; use Filament\Forms\Components\Builder\Block; use Filament\Forms\Components\TextInput; use Filament\Support\Icons\Heroicon; use Illuminate\Support\Facades\Mail; Builder::make('content') ->blocks([ Block::make('contactDetails') ->schema([ TextInput::make('email') ->label('Email address') ->email() ->required(), // ... ]), // ... ]) ->extraItemActions([ Action::make('sendEmail') ->icon(Heroicon::Square2Stack) ->action(function (array $arguments, Builder $component): void { $itemData = $component->getItemState($arguments['item']); Mail::to($itemData['email']) ->send( // ... ); }), ])上述示例中:
$arguments['item']是当前 Builder 条目的 ID;getItemState()返回该条目的已校验数据;若条目校验失败,动作会被取消,并在表单中为该条目显示错误信息;- 若想获取未校验的原始数据,改用
$component->getRawItemState($arguments['item'])。
如果要对整个 Builder 的原始数据进行增删改,可以先用$component->getState()取回全部数据,修改后用$component->state($state)写回:
use Illuminate\Support\Str; // 获取整个 Builder 的原始数据 $state = $component->getState(); // 新增一个条目,以随机 UUID 作为 key $state[Str::uuid()] = [ 'type' => 'contactDetails', 'data' => [ 'email' => auth()->user()->email, ], ]; // 写回 Builder $component->state($state);源码对应:getItemState()内部调用getChildSchema($key)->getState(shouldCallHooksBefore: false),getRawItemState()则调用getStateSnapshot()(Builder.php#L1213-L1224)。从registerActions()(Builder.php#L125-L138)可以看到 Builder 内置注册了 add、addBetween、clone、collapse、collapseAll、delete、edit、expand、expandAll、moveDown、moveUp、reorder 共 12 个动作。
十三、文档演示项目中的真实用法
本仓库的文档演示应用在 BuilderSchema.php 中几乎覆盖了本文讲到的全部特性:基础 Builder、基于内容动态标签(labelledBuilder)、块图标(builderIcons)、头部图标(builderBlockIcons)、添加按钮对齐(builderAddActionAlignment)、块预览(builderBlockPreviews)、按钮排序(builderReorderableWithButtons)、可折叠(collapsibleBuilder)、默认折叠(collapsedBuilder)、可克隆(cloneableBuilder)以及多列块选择器(builderBlockPickerColumns)。当你需要对照某一特性的完整可运行示例时,可以直接翻阅该文件。
总结
Filament Forms 的 Builder 组件以「多类型、可排序、可嵌套的表单块容器」为设计核心,是搭建内容块式编辑器(CMS 页面、营销落地页、动态表单模板)的高效基础设施。其关键能力可归纳为:
- Block 多 schema:
blocks()+Block::make()定义异构块结构,数据以JSON列 +arraycast 存储; - 展示控制:动态标签、图标、编号、只读/交互预览、折叠与延迟加载;
- 交互控制:添加(含条目间插入)、删除、拖拽/按钮排序、克隆,以及
addable()/deletable()/reorderable()等细粒度开关; - 选择器与约束:
blockPickerColumns()/blockPickerWidth()布局、Block::maxItems()单块次数限制、minItems()/maxItems()总量校验; - 作用域与扩展:
$get('../x')跨级取值的路径语义、基于 Action 的 11+ 种可定制内置操作与extraItemActions()自定义扩展。
配合 Forms 概览 与 Schemas 文档 阅读,可以进一步掌握它在大型表单、动态页面编辑器中的完整用法。
【免费下载链接】filamentA powerful open-source UI framework for Laravel • Build and ship apps & admin panels fast with Livewire项目地址: https://gitcode.com/GitHub_Trending/fi/filament
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考