很多好用的都藏在這。
常犯錯誤
- 當 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
的話,就會印出這樣的結果:
所以現在如果我想驗證「電話號碼格式」的話就可以這樣子寫:
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
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
|
這樣子確實會在不相等時顯示錯誤訊息,不過如果是下面這種情形呢?
預設的行為是 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
|
這樣就可以達成預期的效果了:
shouldUpdate
它比較主要的用途有兩個:
- 優化效能(不需要利用 state 來重新渲染整個元件)
- 某個區塊是在特定情境下才會秀出來的
這邊來舉一個例子,假設我希望做出這樣的效果:
簡單來說就是表單能根據我目前選的產品來秀出不同項目給使用者選取,先附上這段的原始碼及 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) } })
|
(只有更新 counter
時才會讓元件重新渲染)
再來點優化
雖然 shouldUpdate
只會重新渲染指定區塊,但有另外一個問題是「每一次 form 的任何資料改變時都會重新渲染」,因為這是預設行為。
假設現在加上一個購買人的欄位,然後在 shouldUpdate
加上 log,就會發現每次我們每次輸入時都會觸發 shouldUpdate
的重新渲染(或是任何欄位值改變):
以這個範例來說我們應該只需要在「產品」改變時去重新渲染「選項」的區塊就好,不需要每一次都做更新,所以可以改寫成這種形式:
1 2 3 4 5 6 7
| <Form.Item shouldUpdate={(prevValues, currentValues) => { return prevValues['產品'] !== currentValues['產品'] }} > {} </Form.Item>
|
shouldUpdate
可以傳入一個 function 並接收到「舊 / 新」的值,接著利用這個來判斷「產品」有沒有被改變即可,當產品一樣時會回傳 false
,不會觸發更新,而當產品不一樣時則回傳 true
,所以觸發更新。
這樣子就能確保只有在產品改變時才去更新選項的欄位了:
搭配 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 }
。
背後的原理
其實是用 getValueProps
和 getValueFromEvent
來實作的,不懂的話建議先看滑去下面理解一下這兩個的用法再拉回來看。
簡單來說就是用 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:
總之這背後就是對值做轉換而已,知道原理以後你甚至能這樣子玩:
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>
加上 value
和 onChange
事件,像這樣子:<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
:
這邊會有兩個問題:
1. 基於我們一開始寫的判斷可能會產生非預期結果
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <Form.Item label="date" name="date" getValueProps={(value) => { console.log('value', value) 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) => { 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>
|
附註:CodeSandBox 連結
所以這兩個東西搭配使用的話就可以解決 <DatePicker>
的格式問題囉。