Ant Design-Form.Item

很多好用的都藏在這。

常犯錯誤

  • 當 Form.Item 設置 name 以後就無法再對表單獨自設定 value
  • 當 Form.Item 被 re-render 時 onReset<Form> 的 props) 也會被觸發
  • rules 只能在 Form.item 有設置 name 時才有效。

validate

<Form.Item> 中有一個的 rule 的 props 可以傳,最常見的用法是這樣子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Form form={form} labelCol={{ span: 24 }} wrapperCol={{ span: 24 }}>
<Form.Item
name='phone'
label='Phone'
rules={[
{
required: true,
message: 'Phone number cannot be blank.'
}
]}
>
<Input type='tel' />
</Form.Item>
<Button htmlType='submit'>Submit</Button>
</Form>

沒什麼,就只是一個必填項目的驗證而已。

不過當你碰到比較複雜的邏輯時,可能會想寫一些「驗證函式」來做自定義驗證,這個就是 validate 的適用時機。

他的寫法會長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<Form form={form} labelCol={{ span: 24 }} wrapperCol={{ span: 24 }}>
<Form.Item
name='phone'
label='Phone'
rules={[
// 陣列值是一個 function
(form) => {
// 這個 function 中可以拿到 form instance
console.log('form instance', form)
// 回傳一個物件,值為 validator(驗證函式)
return {
validator: (rule, value) => {
console.log('rule', rule)
console.log('value', value)
// 回傳 Promise.resolve() / Promise.reject() 表示通過或未通過
return Promise.resolve()
}
}
}
]}
>
<Input type='tel' />
</Form.Item>

<Button htmlType='submit'>Submit</Button>
</Form>

附註:雖然是回傳 Promise 但並不是「非同步」執行哦,你試著在裡面用 while 去跑「同步」延遲的話就能看到 blocking 的現象了。

寫法會比剛剛複雜一點,但主要是為了給你一些用來取得資訊的參數,所以才會看起來會寫很多層。

不過照著上面的註解來看應該就能理解了,例如我現在輸入 1 的話,就會印出這樣的結果:

validate-log

所以現在如果我想驗證「電話號碼格式」的話就可以這樣子寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<Form
form={form}
labelCol={{ span: 24 }}
wrapperCol={{ span: 24 }}
onFinish={(values) => {
console.log('values', values)
}}
>
<Form.Item
name='phone'
label='Phone'
rules={[
(form) => {
return {
validator: (rule, value) => {
const pattern = /^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/
if (pattern.test(value)) {
return Promise.resolve()
} else {
return Promise.reject()
}
},
message: 'Does not match the phone format.'
}
}
]}
>
<Input type='tel' />
</Form.Item>

<Button htmlType='submit'>Submit</Button>
</Form>

此處範例:CodeSandbox

validate

dependencies

簡單來說這個是拿來設定「當某個欄位更新時,我想重新驗證」的用途。

舉例來說,一般如果要做「確認密碼」的欄位時,你可能會這樣寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function App() {
return (
<div className='wrapper'>
<Form>
<Form.Item name='password' label='Password'>
<Input.Password />
</Form.Item>
<Form.Item
name='confirmPassword'
label='Confirm Password'
rules={[
(form) => {
return {
// onChange 時會驗證是否跟原密碼相同
validator: (_, value) => {
if (form.getFieldValue('password') === value) {
return Promise.resolve()
} else {
return Promise.reject('does not match the password.')
}
}
}
}
]}
>
<Input.Password />
</Form.Item>
</Form>
</div>
)
}

export default App

這樣子確實會在不相等時顯示錯誤訊息,不過如果是下面這種情形呢?

missing-validate

預設的行為是 Confirm Password 改變時會做驗證,可是在那之後如果又改變 Password 的話就不會觸發驗證了。

如果希望在這個時候也做驗證的話,可以對 Confirm Password 加上 dependencies 來處理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function App() {
return (
<div className='wrapper'>
<Form>
<Form.Item name='password' label='Password'>
<Input.Password />
</Form.Item>
<Form.Item
name='confirmPassword'
label='Confirm Password'
dependencies={['password']} // 加上 dependencies
rules={[
(form) => {
return {
validator: (_, value) => {
console.log('validate')
if (form.getFieldValue('password') === value) {
return Promise.resolve()
} else {
return Promise.reject('does not match the password.')
}
}
}
}
]}
>
<Input.Password />
</Form.Item>
</Form>
</div>
)
}

export default App

這樣就可以達成預期的效果了:

use-dependencies

shouldUpdate

它比較主要的用途有兩個:

  1. 優化效能(不需要利用 state 來重新渲染整個元件)
  2. 某個區塊是在特定情境下才會秀出來的

這邊來舉一個例子,假設我希望做出這樣的效果:

feature

簡單來說就是表單能根據我目前選的產品來秀出不同項目給使用者選取,先附上這段的原始碼及 CodeSandbox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
function App() {
const [form] = Form.useForm()
const menu = [
{ label: '奶茶', value: '奶茶' },
{ label: '牛肉麵', value: '牛肉麵' },
{ label: '雞排', value: '雞排' }
]

const initValues = {
甜度: '無糖',
份量: '小碗',
酸菜: '不要',
辣度: '小辣',
切不切: '不要'
}

return (
<div className='wrapper'>
<Form
form={form}
initialValues={initValues}
onFinish={(values) => {
console.log(values)
}}
>
<Form.Item name='產品' label='產品'>
<Select options={menu} onChange={() => form.setFieldsValue({ ...initValues })} />
</Form.Item>
{/* 選項區塊 */}
<Form.Item shouldUpdate>
{/* 自動接收到 form instance 參數 */}
{(form) => {
const selectedValue = form.getFieldValue('產品')
return selectedValue === '奶茶' ? (
<Form.Item label='甜度' name='甜度'>
<Radio.Group>
<Radio value='無糖'>無糖</Radio>
<Radio value='少糖'>少糖</Radio>
<Radio value='半糖'>半糖</Radio>
<Radio value='多糖'>多糖</Radio>
<Radio value='全糖'>全糖</Radio>
</Radio.Group>
</Form.Item>
) : selectedValue === '牛肉麵' ? (
<>
<Form.Item label='份量' name='份量'>
<Radio.Group>
<Radio value='小碗'>小碗</Radio>
<Radio value='大碗'>大碗</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label='酸菜' name='酸菜'>
<Radio.Group>
<Radio value='不要'>不要</Radio>
<Radio value='要'></Radio>
</Radio.Group>
</Form.Item>
</>
) : selectedValue === '雞排' ? (
<>
<Form.Item label='辣度' name='辣度'>
<Radio.Group>
<Radio value='不辣'>不辣</Radio>
<Radio value='小辣'>小辣</Radio>
<Radio value='大辣'>大辣</Radio>
<Radio value='中辣'>中辣</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label='切不切' name='切不切'>
<Radio.Group>
<Radio value='不要'>不要</Radio>
<Radio value='要'></Radio>
</Radio.Group>
</Form.Item>
</>
) : null
}}
</Form.Item>
<Button htmlType='submit'>送出</Button>
</Form>
</div>
)
}

export default App

這樣子做的好處是不會把整個元件重新渲染,Antd 會自動幫你重新渲染「這個區塊」,想驗證的話可以加上 useEffect 跑跑看:

1
2
3
4
5
6
7
8
9
useEffect(() => {
console.log('render')
const timer = setInterval(() => {
setCounter(counter + 1)
}, 1000)
return () => {
clearInterval(timer)
}
})

render-test

(只有更新 counter 時才會讓元件重新渲染)

再來點優化

雖然 shouldUpdate 只會重新渲染指定區塊,但有另外一個問題是「每一次 form 的任何資料改變時都會重新渲染」,因為這是預設行為。

假設現在加上一個購買人的欄位,然後在 shouldUpdate 加上 log,就會發現每次我們每次輸入時都會觸發 shouldUpdate 的重新渲染(或是任何欄位值改變):

update-each-time

以這個範例來說我們應該只需要在「產品」改變時去重新渲染「選項」的區塊就好,不需要每一次都做更新,所以可以改寫成這種形式:

1
2
3
4
5
6
7
<Form.Item
shouldUpdate={(prevValues, currentValues) => {
return prevValues['產品'] !== currentValues['產品']
}}
>
{/* ... */}
</Form.Item>

shouldUpdate 可以傳入一個 function 並接收到「舊 / 新」的值,接著利用這個來判斷「產品」有沒有被改變即可,當產品一樣時會回傳 false,不會觸發更新,而當產品不一樣時則回傳 true,所以觸發更新。

這樣子就能確保只有在產品改變時才去更新選項的欄位了:

update-centain-time

搭配 CheckBox 使用

當搭配 <Form.Item> 來使用 checkbox 時,你可能沒辦法在 onFinish 時取出正確的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const App = () => {
return (
<Form
onFinish={(values) => {
console.log(values) //
}}
>
<Form.Item name='isJunior'>
<Checkbox>Is Junior?</Checkbox>
</Form.Item>
<Button htmlType='submit'>Submit</Button>
</Form>
)
}

不管我有沒有勾選,最後的結果都會是 { isJunior: undefined }

解決方式是在 Form.Item 上新增 valuePropName='checked'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const App = () => {
return (
<Form
onFinish={(values) => {
console.log(values)
}}
>
<Form.Item name='isJunior' valuePropName='checked'>
<Checkbox>Is Junior?</Checkbox>
</Form.Item>
<Button htmlType='submit'>Submit</Button>
</Form>
)
}

這樣就可以正確的取出 { isJunior: true }{ isJunior: false }

背後的原理

其實是用 getValuePropsgetValueFromEvent 來實作的,不懂的話建議先看滑去下面理解一下這兩個的用法再拉回來看。

簡單來說就是用 getValueProps 設定表單元件的 value 屬性,接著再用 getValueFromEvent來處理當值改變時該「如何對值做轉換」,直接來看範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<Form onFinish={(values) => console.log(values)} initialValues={{ isJunior: true }}>
<Form.Item
name='isJunior'
getValueFromEvent={(event) => {
// onChange 時把 checked 的值取出並回傳
// 這裡回傳的東西就會變成 getValueProps 中的 value
return event.target.checked
}}
getValueProps={(value) => {
// 回傳 "checked" 與 "value" 的物件
return {
checked: value,
value
}
}}
>
<Checkbox>Is Junior?</Checkbox>
</Form.Item>
<Button htmlType='submit'>Submit</Button>
</Form>

附註:CodeSandbox

這就跟你自己在寫「Controlled Component」的時候有點類似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function App() {
const [isJunior, setIsJunior] = useState(false)

return (
<label>
isJunior
<input
type='checkbox'
checked={isJunior}
onChange={(event) => setIsJunior(event.target.checked)}
/>
</label>
)
}

只不過因為這邊的 <Chekbox> 是放在 <Form.Item> 裡,所以依照 Antd 的規則來說 <Form.Item> 中的元件必須用 value 來控制,而不是 checked

不過既使是透過 value 來控制,你一樣得傳 checked 屬性給 <Checkbox>,因為他本身還是得有這個屬性,否則可能會看到這段 warning:

check-warning

總之這背後就是對值做轉換而已,知道原理以後你甚至能這樣子玩:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const App = () => {
return (
<Form
onFinish={(values) => {
// {isJunior: "Y"} or {isJunior: "N"}
console.log(values)
}}
initialValues={{
// 用自定義的值來表示
isJunior: 'Y'
}}
>
<Form.Item
name='isJunior'
getValueFromEvent={(event) => {
// 把值從 true / false 轉換成自定義的值
return event.target.checked ? 'Y' : 'N'
}}
getValueProps={(value) => {
// 把自定義的值來轉換成元件看得懂的值(true /false)
return {
checked: value === 'Y',
value: value === 'Y'
}
}}
>
<Checkbox>Is Junior?</Checkbox>
</Form.Item>
<Button htmlType='submit'>Submit</Button>
</Form>
)
}

附註:CodeSandbox

這樣子的好處是你可以在按下「Submit」時就直接拿到你想要的值 Y / N,不用再自己對 true / false 來做轉換,因為你在 <Form.Item> 那一層就先做好轉換的動作了。

getValueProps & getValueFromEvent

getValueProps

簡單來說 getValueProps 是用來讓你重新設定 value 屬性的值(也許可以設定別的屬性,但我想次數真的不多)。

實際可以用在 <Form.Item> 底下的 <DatePicker>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const App = () => {
return (
<Form initialValues={{ date: '08/08/2022' }}>
<Form.Item
label='date'
name='date'
getValueProps={(value) => {
// 08/08/2022
console.log('value', value)
// 根據初始值設定為空字串 or 新的 moment 物件
return {
value: moment(value, 'DD/MM/YYYY', true).isValid() ? moment(value, 'DD/MM/YYYY') : ''
}
}}
>
<DatePicker />
</Form.Item>
<Button htmlType='submit'>Submit</Button>
</Form>
)
}

會這樣設定是因為 <Form.Item> 預設會自動幫底下的表單元件設定 value 值,官方文件有提到這點:

被设置了 name 属性的 Form.Item 包装的控件,表单控件会自动添加 value(或 valuePropName 指定的其他属性) onChange(或 trigger 指定的其他属性)

可以想成它會自動幫 <DatePicker> 加上 valueonChange 事件,像這樣子:<DatePicker value="08/08/2022"/>

但這樣子會有問題,因為 <DatePicker> 是透過 moment 格式的物件來運作的,代表 value 必須是一個 moment 物件,否則的話會直接壞掉(可能會看到 date.clone is not a function 的錯誤)

總而言之透過 getValueProps 我們就能在這一層先對 value 做處理,看是要把它轉換成空字串(這是合法的值) 還是 moment 物件,接著傳給 <DatePicker>,就能避免格式上的問題。

getValueFromEvent

剛剛的範例其實會有另外一個問題,就是當我「重新選了一個日期」時,拿到的 value 會變成 moment 物件而不是原本的 08/08/2022

get-value-props-problem

這邊會有兩個問題:

1. 基於我們一開始寫的判斷可能會產生非預期結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Form.Item
label="date"
name="date"
getValueProps={(value) => {
// 如果拿到 moment 物件
console.log('value', value)
// 等於又把 moment 丟進去 moment() 當作值
return {
value: moment(value, "DD/MM/YYYY", true).isValid()
? moment(value, "DD/MM/YYYY")
: ""
};
}}
>

2. 表單 Submit 後拿到的是 moment 物件而不是 08/08/2022

要解決這個問題的辦法就是 getValueFromEvent

人如其名,它的用途就是「在 onChange 時把 event 丟給你,讓你重新設定 value 的值應該要長怎樣?」。也許表達的不是很正確,但我覺得這樣子比較好理解就是了。

附註:對於 DatePicker 來說 getValueFromEvent 會拿到的是 moment,但一般的 <Input> 或是 <Checkbox> 這類的表單元件則會拿到 event 物件。

所以上面的範例可以改寫成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Form.Item
label='date'
name='date'
getValueFromEvent={(moment) => {
// 這裡回傳什麼,下面的 value 就會拿到什麼
// 所以不需要寫成 `{ value: ... }` 的形式
return moment.format('DD/MM/YYYY')
}}
getValueProps={(value) => {
return {
value: moment(value, 'DD/MM/YYYY', true).isValid() ? moment(value, 'DD/MM/YYYY') : ''
}
}}
>
<DatePicker />
</Form.Item>

get-value-from-event

附註:CodeSandBox 連結

所以這兩個東西搭配使用的話就可以解決 <DatePicker> 的格式問題囉。

Moment.js 筆記 Ant Design-使用 Button 時可能會碰到的地雷
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×