iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Modern Web

Laravel Filament 從入門到實戰系列 第 5

Day 04:表單欄位全解析,從文字輸入到日期選擇器

  • 分享至 

  • xImage
  •  

Day 03:建立第一個 Resource,五分鐘做出一套 CRUD 介面》結尾留下一句話,今天生成的表單欄位還是最基本的預設樣式,分類要用什麼方式選擇,上架狀態要用什麼元件呈現,規格描述這種長文字內容又該怎麼處理,這些欄位背後都牽涉到不同的欄位型別選擇。今天要處理的正是這件事。

能動不代表好用

先看看昨天做出來的東西長什麼樣子。商品 Resource 的建立頁跟編輯頁確實能用,點下新增按鈕,表單會跳出來,填完送出,資料也真的存進資料庫。但打開表單仔細看會發現一個問題,不管欄位性質是什麼,畫面上呈現的方式幾乎都長得一樣。上架狀態這種其實是一道是非題,卻要使用者在文字輸入框裡打字回答,打「是」還是「Y」還是「已上架」,完全沒有規範。進貨日期更明顯,得靠使用者自己手動輸入日期格式,今天打 2026/09/15,明天可能打成 2026-9-15,介面完全不會攔下這種落差。

這不只是介面好不好看的問題。同一批商品資料,如果分類欄位讓十個人手動輸入十次,很可能就會出現「寵物玩具」「玩具」「寵物用玩具」這類看起來意思相同、實際上是三種不同字串的髒資料,後續要依分類篩選或統計,這些不一致的寫法會直接讓查詢結果失真。表單欄位型別選得對不對,牽涉的不只是操作體驗,更直接影響資料本身的正確性,以及未來要維護、查詢這批資料時要付出的成本。

今天的任務範圍很明確,把商品表單裡的每個欄位重新檢視一次,依照資料性質決定最合適的欄位型別。今天不會動到版面配置,所有欄位依然攤平排列,也不會碰驗證規則的細節,必填、長度限制這類基本能力會在對應欄位提到,但不會展開完整的驗證邏輯討論。專心做好一件事,讓每個欄位長得像它該有的樣子。

文字與數字類欄位,不是所有文字都該用同一種輸入框

商品名稱是最單純的例子。這是一段簡短、單行、使用者一次就能打完的文字,適合用 TextInput 處理:

use Filament\Forms\Components\TextInput;

TextInput::make('name')
    ->label('商品名稱')
    ->required()
    ->maxLength(255)

required() 確保這個欄位不能留空,maxLength() 限制輸入長度不超過資料庫欄位能承受的範圍,這兩個方法屬於欄位型別本身附帶的基本能力,簡單設定就完成,不需要額外的驗證邏輯堆疊。

規格描述就是另一回事了。商品規格往往需要寫上材質、尺寸、注意事項這類篇幅較長、可能得換行分段的內容,如果還是塞進單行的 TextInput,使用者打字時只能看到一條擠成一團的文字,可讀性很差。這種情況該換成 Textarea,用多行文字區域承接篇幅較長的內容:

use Filament\Forms\Components\Textarea;

Textarea::make('description')
    ->label('規格描述')
    ->rows(4)
    ->maxLength(1000)

判斷該用 TextInput 還是 Textarea 的依據很直接,內容篇幅短、單行輸入就能完整表達的用前者,內容篇幅長、需要多行呈現才好閱讀的用後者。商品名稱跟規格描述放在一起對照,這個差異會很明顯。

價格跟庫存數量則是另一種性質,這兩者本質上都是數字,而不是文字。如果用一般的文字輸入框接受商品價格,使用者手滑打進一個中文字或符號,介面完全不會阻擋,等到後續要做金額計算或依價格排序,才會踩到這顆地雷。TextInput 搭配 numeric() 可以直接把輸入限制在數字範圍:

TextInput::make('price')
    ->label('價格')
    ->numeric()
    ->minValue(0)
    ->prefix('NT$')

TextInput::make('stock')
    ->label('庫存數量')
    ->numeric()
    ->minValue(0)
    ->integer()

minValue() 貼近業務規則,價格與庫存都不該是負數,integer() 則進一步限制庫存數量只能是整數,不會出現半件商品這種不合理的數值。這些限制同樣是欄位型別本身附帶的基本能力,不需要另外寫驗證規則檔案。

選擇類欄位,讓使用者從有限選項裡挑,而不是自己亂打

商品分類是一個典型的例子,可能值就那幾種:寵物飼料、寵物玩具、清潔用品,是一個有限且明確的集合。這種情況適合用下拉選單,而不是讓使用者自己打字,Select 元件正好對應這個需求:

use Filament\Forms\Components\Select;

Select::make('category')
    ->label('分類')
    ->options([
        'food' => '寵物飼料',
        'toy' => '寵物玩具',
        'cleaning' => '清潔用品',
    ])
    ->required()

換成下拉選單之後,使用者只能從預先定義好的選項裡挑,不會再出現「寵物玩具」跟「玩具」並存的情況,這正是開場提到的是非題比喻的延伸,當一個欄位的答案範圍是有限的,讓使用者從清單裡挑,遠比讓使用者自己憑印象打字來得可靠。

上架狀態則更極端,這個欄位本質上只有兩種答案,上架或下架,是一道不折不扣的是非題。與其用下拉選單模擬一個二選一,不如直接用 Toggle,讓操作介面直接對應資料的布林值本質:

use Filament\Forms\Components\Toggle;

Toggle::make('is_active')
    ->label('上架狀態')
    ->default(true)

畫面上會呈現一顆可以左右撥動的開關,撥到開代表上架,撥到關代表下架,一眼就能判斷目前狀態,也不會出現使用者在文字輸入框裡打「上架中」跟「已上架」這類意思相同、寫法不同的情況。

這兩個欄位型別背後對應的資料庫欄位型別,讀者應該已經很熟悉,分類通常對應列舉或字串搭配固定選項,上架狀態對應布林值,這是 Eloquent Model 早就處理過的概念,這裡不重新解釋資料庫層的型別設計,只是讓 Filament 的欄位型別跟資料庫欄位型別的本質對上號。

日期類欄位,讓格式錯誤在輸入當下就不會發生

進貨日期是今天最後一個要處理的欄位,也是本篇標題點名的主角。如果沿用最基本的文字輸入框,讓使用者手動打日期,格式不一致幾乎是必然發生的事,有人打 2026/09/15,有人打 2026-9-15,甚至有人打成中文年月日。這類看起來只是格式差異的髒資料,一旦要依日期排序、篩選某段時間內的進貨紀錄,或是拿去做報表統計,就會變成需要額外清洗資料的麻煩。

DatePicker 元件直接解決這個問題,提供一個圖形化的日曆介面,讓使用者用點選取代手動輸入:

use Filament\Forms\Components\DatePicker;

DatePicker::make('restocked_at')
    ->label('進貨日期')
    ->native(false)
    ->maxDate(now())

native(false) 讓 Filament 使用自己實作的日曆選擇介面,而不是瀏覽器原生的日期輸入框,介面樣式會更貼近整個後台的視覺風格。使用者點開欄位,看到的是一個可以直接點選日期的日曆,不需要在腦中組合斜線或連字號,格式從輸入當下就保證一致,不會再出現前面描述的那種落差。

maxDate(now()) 則示範了欄位型別不只是外觀上的元件,也能承擔一部分業務規則。進貨日期理論上不該選到未來的日期,設定這個限制之後,日曆介面會直接讓未來的日期變成無法點選的狀態,使用者連誤選的機會都沒有,這比事後跳出一則錯誤訊息更直接,也更貼近今天想建立的核心手感,讓格式錯誤在輸入當下就不會發生,而不是等到送出表單才被攔下來。

欄位型別選對了,但表單開始變擁擠

回顧今天走過的四類欄位,文字、數字、選擇、日期,其實不是在背一份欄位型別對照表,而是在練習一種判斷邏輯,看到一個欄位,先問這個欄位承載的資料本質是什麼,短文字、長文字、數字、有限選項、是非題,還是日期,再回頭挑選對應的欄位型別。這個順序不能顛倒,先想資料性質,欄位型別自然就會浮現,而不是先挑一個順眼的元件,再硬把資料塞進去。

商品表單目前的樣貌也該誠實檢視一次。名稱用 TextInput,規格描述用 Textarea,價格跟庫存數量用帶數字限制的 TextInput,分類用 Select,上架狀態用 Toggle,進貨日期用 DatePicker,每個欄位確實都選對了型別,功能上已經齊全。

但商品的欄位不少,這七個欄位全部攤平排在同一個頁面上,開始顯得擁擠。這個版面問題不是欄位型別能解決的,需要另一層版面配置的設計,是明天要處理的內容。


上一篇
Day 03:建立第一個 Resource,五分鐘做出一套 CRUD 介面
下一篇
Day 05:表單版面配置,用 Schema 排出一份不擁擠的表單
系列文
Laravel Filament 從入門到實戰8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言