表單元件
內容來源
本頁內容源自 appfuse-web/lib/form/README.md,如有差異以 README 為準。
AppFuse Web 提供完整的表單元件庫,整合 react-hook-form 進行表單狀態管理和驗證。
設計理念
- Modifiers: 前處理修飾器(如 trim、轉型)
- Hook: react-hook-form Controller
- Hints: 提示訊息(helper text、error message)
- Component: UI 元件渲染
基本用法
建立表單
import { useForm } from 'react-hook-form';
import { Input, Select, Checkbox } from '@appfuse/appfuse-web/form';
import { schema, validatorResolver } from '@appfuse/appfuse-web/form/validator';
// 1. 定義 Schema(使用框架內建的輕量驗證器)
const userSchema = schema.object({
name: schema.string().required(),
email: schema.string().required(),
role: schema.string().required(),
active: schema.boolean(),
});
// 2. 建立表單
function UserForm() {
const { control, handleSubmit } = useForm({
resolver: validatorResolver(userSchema),
defaultValues: {
name: '',
email: '',
role: null,
active: true,
},
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input name="name" control={control} label="Name" />
<Input name="email" control={control} label="Email" type="email" />
<Select
name="role"
control={control}
label="Role"
options={[
{ value: 'admin', label: 'Admin' },
{ value: 'user', label: 'User' },
]}
/>
<Checkbox name="active" control={control} label="Active" />
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
);
}
驗證策略
AppFuse Web 採用「前端輕量、後端完整」的驗證策略。前端僅驗證類型和必填,格式驗證(Email、長度等)由後端處理。禁止使用 Zod,請使用框架內建的 schema 驗證器(~5KB vs Zod ~60KB)。
表單元件一覽
文字輸入
| 元件 | 用途 | 特性 |
|---|---|---|
Input | 單行文字 | 支援 text, number, date, email, password |
Textarea | 多行文字 | 自動調整高度、字數統計 |
TagInput | 標籤輸入 | 多值輸入、可創建新標籤 |
RichTextEditor | 富文本 | 基於 Slate.js,支援格式化 |
選擇輸入
| 元件 | 用途 | 特性 |
|---|---|---|
Select | 下拉選單 | 單選/多選、可搜尋、可創建 |
Checkbox | 核取方塊 | 布林/單選/多選模式 |
CheckboxGroup | 核取群組 | 宣告式多選 |
Radio | 單選按鈕 | 單一選擇 |
RadioGroup | 單選群組 | 宣告式單選 |
Switch | 開關 | 布林切換 |
檔案輸入
| 元件 | 用途 | 特性 |
|---|---|---|
FileInput | 檔案上傳 | 拖放、多檔、驗證 |
MediaInput | 媒體上傳 | 圖片/影片預覽、裁切、旋轉 |
元件屬性
共用屬性
所有表單元件都支援以下屬性:
interface CommonProps {
name: string; // 欄位名稱(必填)
control: Control; // react-hook-form control(必填)
label?: string; // 標籤(自動翻譯)
hint?: string; // 提示文字
readOnly?: boolean; // 唯讀模式
disabled?: boolean; // 停用
variant?: 'bordered' | 'ghost'; // 外觀變體
density?: 'compact' | 'comfortable'; // 密度
color?: 'default' | 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error';
}
Input 屬性
<Input
name="price"
control={control}
label="Price"
type="number" // text | number | date | email | password
min={0} // 最小值(number 類型)
max={9999} // 最大值(number 類型)
step={0.01} // 步進值(number 類型)
placeholder="Enter price"
/>
Select 屬性
<Select
name="category"
control={control}
label="Category"
options={categories}
isMulti={false} // 是否多選
isSearchable={true} // 是否可搜尋
isCreatable={false} // 是否可創建新選項
isClearable={true} // 是否可清除
dependsOn="parentField" // 依賴欄位(連動過濾)
filterBy="parentId" // 過濾欄位
/>
Checkbox 屬性
// 布林模式
<Checkbox
name="agree"
control={control}
label="I agree to terms"
/>
// 多選模式
<Checkbox
name="features"
control={control}
label="Features"
options={[
{ value: 'wifi', label: 'WiFi' },
{ value: 'parking', label: 'Parking' },
]}
/>
標籤自動翻譯
所有表單元件會自動翻譯以下 props,傳入 i18n key(英文)即可:
| Prop | 適用元件 | 說明 |
|---|---|---|
label | 所有表單元件 | 欄位標籤 |
aria-label | Input, Select, TagInput 等 | 無障礙標籤 |
placeholder | TagInput, Select(搜尋框) | 提示文字 |
// ✅ 正確:傳入 i18n key
<Input name="email" control={control} label="Email" />
<Select name="role" control={control} label="Role" options={roles} />
// ❌ 錯誤:不要先翻譯再傳入(會造成雙重翻譯)
<Input name="email" control={control} label={t('Email')} />
Input / Textarea 的 placeholder
Input 和 Textarea 為了實現浮動標籤效果,placeholder 被硬編碼為 " ",傳入的 placeholder prop 不會顯示。
表單值設計
空值處理
AppFuse Web 使用 null 表示空值(非空字串 ''):
// ✅ 正確的預設值
const defaultValues = {
name: '', // 字串欄位用空字串
category: null, // 選擇欄位用 null
price: null, // 數字欄位用 null
date: null, // 日期欄位用 null
};
// ❌ 錯誤
const defaultValues = {
category: '', // Select 不應使用空字串
price: 0, // 0 是有意義的值,不是空值
};
為什麼用 null 而非 ''?
- 與 API 一致:RESTful API 回傳
null表示缺失值,使用null可直接對應 - 語義清晰:
null= 未填寫,''= 刻意填寫空白 - 型別安全:
string | null明確表達可為空,string則語義不清 - 自動轉換:表單元件自動處理
null↔''的轉換,開發者無需關心
型別轉換
表單元件會自動處理型別轉換:
| 元件 | HTML 值 | 表單值 |
|---|---|---|
| Input (text) | '' | '' |
| Input (number) | '' | null |
| Input (number) | '123' | 123 |
| Input (date) | '' | null |
| Select | undefined | null |
驗證
使用框架內建驗證器
import { schema, validatorResolver } from '@appfuse/appfuse-web/form/validator';
const customerSchema = schema.object({
// 必填字串
name: schema.string().required(),
// 必填選擇
category: schema.string().required(),
// 必填數字
price: schema.number().required(),
// 選填
note: schema.string(),
// 選填數字(nullable)
discount: schema.number().nullable(),
});
// 搭配 react-hook-form 使用
const { control, handleSubmit } = useForm({
resolver: validatorResolver(customerSchema),
});
前端驗證範圍
前端只驗證類型和必填。格式驗證(Email 格式、字串長度等)由後端 @Email、@Length 等 Bean Validation 處理。
後端違例處理
後端回傳的驗證錯誤可自動映射到表單欄位:
import { mapViolationsToFormErrors, createServerErrorHandler } from '@appfuse/appfuse-web/form/validator';
const onSubmit = async (data) => {
try {
await api.post('/customers', data);
prompt.success('Customer created');
} catch (error) {
// 自動將後端 violations 映射到 react-hook-form 錯誤
const handler = createServerErrorHandler(setError);
handler(error);
}
};
錯誤訊息翻譯
錯誤訊息會自動翻譯,並替換 ${field} 為欄位的翻譯後標籤:
驗證器生成錯誤 → 訊息模板 + 參數 → Hook 翻譯 → 顯示本地化錯誤
在 src/nls/message/ 中定義錯誤訊息翻譯:
// src/nls/message/zh-TW.ts
export default {
'${field} is required': '${field}為必填',
'${field} must be ${type}': '${field}必須為${type}類型',
'${field} must be an array': '${field}必須為陣列',
'${field} must not be empty': '${field}不可為空',
}
效果:假設 label="Name" 且 i18n 定義 Name → 姓名,驗證失敗時顯示「姓名為必填」。
後端回傳的 violation 錯誤也使用相同格式,前端自動處理:
{
"violations": [{
"format": "${field} is already taken",
"params": { "field": "email" }
}]
}
進階用法
連動選單
// 縣市 → 區域連動
<Select
name="city"
control={control}
label="City"
options={cities}
/>
<Select
name="district"
control={control}
label="District"
options={districts}
dependsOn="city" // 依賴 city 欄位
filterBy="cityId" // 用 cityId 欄位過濾
/>
動態表單
import { useFieldArray } from 'react-hook-form';
function OrderForm() {
const { control } = useForm();
const { fields, append, remove } = useFieldArray({
control,
name: 'items',
});
return (
<div>
{fields.map((field, index) => (
<div key={field.id}>
<Input
name={`items.${index}.name`}
control={control}
label="Item Name"
/>
<Input
name={`items.${index}.quantity`}
control={control}
label="Quantity"
type="number"
/>
<button onClick={() => remove(index)}>Remove</button>
</div>
))}
<button onClick={() => append({ name: '', quantity: 1 })}>
Add Item
</button>
</div>
);
}
唯讀模式
// 單一欄位
<Input name="id" control={control} label="ID" readOnly />
// 整個表單
function ViewForm({ data }) {
const { control } = useForm({ defaultValues: data });
return (
<fieldset disabled>
<Input name="name" control={control} label="Name" readOnly />
<Select name="status" control={control} label="Status" readOnly />
</fieldset>
);
}
外觀變體
Variant(外框樣式)
// Bordered(預設)- 有邊框
<Input name="name" control={control} variant="bordered" />
// Ghost - 無邊框,僅底線
<Input name="name" control={control} variant="ghost" />
Density(密度)
// Comfortable(預設)- 標準間距
<Input name="name" control={control} density="comfortable" />
// Compact - 緊湊間距
<Input name="name" control={control} density="compact" />
Color(顏色)
<Input name="name" control={control} color="primary" />
<Input name="name" control={control} color="error" />
最佳實踐
- 使用框架內建驗證器 - 使用
schema+validatorResolver,禁止使用 Zod - 前端輕量驗證 - 只驗證類型和必填,格式驗證交給後端
- 正確的預設值 - 選擇和數字欄位使用
null,字串使用'' - 標籤自動翻譯 - label 傳入英文 key,框架自動翻譯
- 保持一致性 - 同一表單使用相同的 variant 和 density