《Day 04:表單欄位全解析,從文字輸入到日期選擇器》 結尾留下一句話,商品表單的七個欄位確實都選對了型別,但全部攤平排在同一個頁面上,開始顯得擁擠,這個版面問題不是欄位型別能解決的,需要另一層版面配置的設計。今天要處理的正是這件事。
先想像一下使用者實際打開這張商品表單的感受。名稱、分類、規格描述、價格、庫存數量、上架狀態、進貨日期,七個欄位由上到下一路排開,每個欄位單獨看都沒問題,TextInput 該接的文字接住了,Select 該擋的亂打也擋住了。
但把七個欄位攤平擺在一起,使用者要一路往下捲動才能填完整張表單,捲到一半還得回頭確認自己是不是漏填了什麼。
更麻煩的是欄位之間缺乏任何邏輯分組。商品名稱跟進貨日期挨在一起,中間夾著價格跟庫存數量,規格描述又插在分類跟上架狀態之間。這張表單其實同時在問兩件性質不同的事,這個商品是什麼,以及這個商品在倉庫裡的狀態如何,但畫面上完全看不出這個區隔,使用者得自己在腦中重新分類,才能理解現在填的這一格屬於哪個面向。
今天的任務範圍很明確,不會再調整任何一個欄位型別本身,名稱依然是 TextInput,分類依然是 Select,這些在 Day 4 已經定案,不會重新討論。
今天要做的是重新設計這七個欄位在畫面上的排列方式,讓表單讀起來有邏輯,填起來不吃力。這件事有點像整理一間店面,同樣的商品,堆在門口讓客人自己翻找,跟分區陳列讓客人一眼找到想要的貨架,體驗完全是兩回事。表單版面配置要做的,就是把商品表單從貨物堆在門口的倉庫,變成一間分區清楚的店。
在動手排版面之前,有一個詞需要先正式定下來,Schema。這個詞今天第一次出現,但接下來整個系列都會用它,指的是 Filament v5 統一表單欄位與表格欄位的底層定義方式,此系列一律使用此詞指稱這層設計。
這個定義聽起來有點抽象,換個角度理解會清楚一些。表單欄位跟表格欄位,一個是使用者填寫資料時看到的介面,一個是使用者瀏覽資料列表時看到的介面,畫面呈現天差地遠。但如果拆開來看兩者背後在做的事,其實都是在回答同一個問題,這個資料欄位該怎麼被安排與顯示。名稱這個欄位在表單裡要不要必填、要不要限制長度,跟名稱這個欄位在表格裡要不要顯示、要不要可以排序,本質上都是「描述一個欄位的行為」,只是套用的場景不同。
Filament v5 把這層共同的描述邏輯抽成同一套機制,這就是 Schema。實務上帶來的好處很直接,學會怎麼在表單裡用 Section 把欄位分組,之後要在表格那一側做類似的版面安排,用的是同一套語法邏輯,不需要重新學一套完全不同的 API。這也是為什麼 Resource 的表單方法簽名長這樣:
use Filament\Schemas\Schema;
public static function form(Schema $schema): Schema
{
return $schema
->components([
// 欄位與版面配置元件放在這裡
]);
}
方法接收的參數型別是 Schema,回傳的也是 Schema,這個命名對應並非巧合,而是 Filament v5 刻意把表單這一層的定義方式,直接掛到這套統一機制上。
今天只處理 Schema 用在表單版面配置的部分,商品表單要怎麼分組、要不要分頁、要不要依條件顯示欄位,這些都會在接下來的段落展開。Schema 用在表格欄位呈現的部分先按下不表,留給明天處理,這裡只需要先建立一個認知,表單跟表格背後是同一套設計,不是兩套要分開硬背的東西。
回到商品表單本身。七個欄位攤平擺放的問題,第一步要解決的是分組。分組的判斷依據要回頭看這些欄位在業務語意上彼此的關係,而非隨便把七個欄位平均切成幾堆。
商品名稱、分類、規格描述,這三個欄位回答的是「這個商品是什麼」,屬於商品的基本身份資訊。上架狀態同樣貼近這個面向,決定這個商品目前是否要對外呈現,也歸進這一組。價格、庫存數量、進貨日期則是另一組,回答的是「這個商品在進銷存流程裡的狀態如何」,賣多少錢、剩多少庫存、什麼時候進的貨,這三者彼此的關聯明顯比它們跟商品名稱的關聯更緊密。
把這個分組落實到程式碼裡,Section 元件正好適合用來建立這種帶標題的群組區塊:
use Filament\Schemas\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\DatePicker;
Section::make('基本資訊')
->description('商品的名稱、分類與規格說明')
->schema([
TextInput::make('name')
->label('商品名稱')
->required()
->maxLength(255),
Select::make('category')
->label('分類')
->options([
'food' => '寵物飼料',
'toy' => '寵物玩具',
'cleaning' => '清潔用品',
])
->required(),
Textarea::make('description')
->label('規格描述')
->rows(4)
->maxLength(1000),
Toggle::make('is_active')
->label('上架狀態')
->default(true),
])
->columns(2),
Section::make('進銷資訊')
->description('價格、庫存與進貨紀錄')
->schema([
TextInput::make('price')
->label('價格')
->numeric()
->minValue(0)
->prefix('NT$'),
TextInput::make('stock')
->label('庫存數量')
->numeric()
->minValue(0)
->integer(),
DatePicker::make('restocked_at')
->label('進貨日期')
->native(false)
->maxDate(now()),
])
->columns(2),
每個 Section 都可以帶一個標題跟一段說明文字,columns(2) 讓群組內的欄位以兩欄並排,不用每個欄位都獨佔一整列的寬度。畫面上會出現兩塊帶標題的卡片式區塊,一塊寫著基本資訊,一塊寫著進銷資訊,使用者打開表單,先看到的是兩個清楚的區塊標題,一眼就能判斷目前要處理的是商品身份還是進銷狀況。
這裡要再次強調分組的判斷依據,是欄位之間的業務語意關聯,不是把七個欄位隨手切成兩份湊數。基本資訊那組放的是描述商品本身是什麼的欄位,進銷資訊那組放的是跟金流、庫存、時間相關的欄位,這個切法背後有商品管理實際的業務邏輯支撐,版面配置設計跟隨意調整排版的差別正在這裡。
分組已經讓表單好讀很多,但分組不是版面配置唯一的手段。當欄位分組後組別數量依然偏多,或是某些欄位只在特定情況下才需要出現,還有另外兩種手法可以進一步運用。
第一種是分頁。目前商品表單只有兩組,一次全部顯示在同一個頁面還算合理。但如果之後商品模組擴充更多細節欄位,例如再加上供應商資訊、保固條款、多國語系說明,分組數量一多,頁面依然會被拉得很長。這時可以把不同群組拆到不同的分頁籤,使用者一次只聚焦在一個面向:
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
Tabs::make('商品資料')
->tabs([
Tab::make('基本資訊')
->schema([
// 名稱、分類、規格描述、上架狀態
]),
Tab::make('進銷資訊')
->schema([
// 價格、庫存數量、進貨日期
]),
]),
Tabs 底下用 Tab::make 定義每一個分頁籤,各自帶一組 schema。商品表單目前七個欄位分成兩組,用分頁其實還嫌大材小用,Section 並排呈現就足夠,但一旦組別數量繼續增加,分頁籤能把每次映入眼簾的欄位數量控制在一個可負荷的範圍,這是分頁真正派得上用場的時機。
第二種是條件顯示,處理的是「這個欄位不一定每次都需要出現」的情況。商品表單裡,上架狀態這個 Toggle 關掉時,其實隱含著一個後續動作,這個商品為什麼下架。如果每次填表單都固定顯示「下架原因」跟「預計補貨日期」這兩個欄位,對大多數上架中的商品來說,這兩格完全用不到,只是白白佔用畫面空間。這裡新增下架原因與預計補貨日期兩個欄位,用來示範條件顯示的實際效果:
use Filament\Schemas\Components\Utilities\Get;
Toggle::make('is_active')
->label('上架狀態')
->default(true)
->live(),
TextInput::make('inactive_reason')
->label('下架原因')
->visible(fn (Get $get): bool => ! $get('is_active')),
DatePicker::make('restock_expected_at')
->label('預計補貨日期')
->native(false)
->visible(fn (Get $get): bool => ! $get('is_active')),
Toggle 加上 live(),讓這個欄位的值一變動就即時觸發畫面重新運算。visible() 接收一個閉包,透過 Get 工具讀取上架狀態目前的值,只有當上架狀態是關閉的時候,下架原因跟預計補貨日期這兩格才會顯示出來。使用者撥動開關的瞬間,畫面上會即時多出這兩個欄位,撥回開啟狀態,這兩格又會收起來,不相關的欄位不會一直佔用畫面空間。
分組、分頁、條件顯示,這三種手法都建立在今天定案的 Schema 這套定義方式之上,是版面配置在分組之外可以延伸運用的選項。商品表單目前用分組跟一組條件顯示欄位就已經足夠清楚,分頁暫時用不到,但知道有這個工具存在,未來欄位數量真的膨脹時就不必從頭摸索。
今天結束之後,商品表單的樣貌已經跟 Day 4 完全不同。名稱、分類、規格描述、上架狀態收進基本資訊區塊,價格、庫存數量、進貨日期收進進銷資訊區塊,兩個帶標題的區塊並排呈現,使用者打開表單第一眼就能分辨目前在填哪個面向的資料。上架狀態關閉時,下架原因與預計補貨日期會即時浮現,開啟時則收起不佔畫面。表單不再是一長串攤平列表,讀起來有邏輯,填起來也不吃力。
這一切都建立在今天定案的 Schema 之上,Filament v5 統一表單欄位與表格欄位的底層定義方式。今天示範的分組、分頁、條件顯示,都是這套定義方式在表單這一側的具體運用。
而 Schema 這個詞本身的定義就已經點出,這套設計是表單與表格共用的,這套排版邏輯不是表單的專利。表格欄位的定義方式,其實是同一層 Schema 設計,只是套用在列表頁的呈現場景上。回頭看商品列表頁,目前顯示的表格欄位還是 Day 3 生成時最粗略的預設樣式,名稱怎麼顯示、價格要不要格式化成貨幣樣式、分類這種欄位怎麼讓人一眼辨識,這些都還沒處理。
把 Schema 這套邏輯延伸到表格那一側,處理格式化呈現與縮圖顯示,是明天要處理的內容。