在開發過程當中,進行表單校驗是一個很經常使用的功能。html
表單校驗一般須要實現如下幾個功能:react
收集各表單項的數據,如Input輸入框,Select選擇框等。git
按照需求,對錶單項數據進行校驗,並顯示校驗結果。github
須要提交表單時,對錶單中全部數據進行校驗,並收集全部數據。redux
這些功能看似簡單,本身實現的話,仍是會產生很多問題。所以,最好使用已有的庫來實現此功能。我在開發中一般使用Ant Design的Form組件。從文檔中的介紹能夠看出,Form組件的功能實現主要是引用了rc-form。數組
rc-form在開發中幫了我很多忙,我對它的實現方式也很感興趣,因而研究了它的源碼,如今與你們分享一下。bash
我將爲你梳理rc-form的主要實現思路,爲你講解rc-form源碼中部分方法的用途,並提供部分代碼的註釋。antd
最後,我還會本身實現一個精簡版的rc-form組件,供你參考。架構
本文中的Demo使用TypeScript編寫,若是你對TypeScript不瞭解,能夠查看TypeScript文檔。但只要有JavaScript基礎,就不會影響閱讀。app
爲了方便理解,你還能夠從GitHub下載Ant Design和rc-form的源碼。
獲取文中示例項目代碼,請點擊這裏。
Demo運行方法:
$ yarn install
$ yarn start # visit http://localhost:3000/
複製代碼
運行後你會看到一個這樣的Demo頁面:
下圖是一個rc-form表單Demo,你能夠在http://localhost:3000/頁面中,點擊表單彈窗
按鈕,查看效果。
這個表單實現了以下功能:
在用戶輸入時和點擊確認按鈕時,會進行表單項的非空校驗。
用戶輸入和選擇的結果,會顯示在表單下方。
點擊確認按鈕,若校驗經過,會彈窗提示用戶輸入結果。
該表單是使用Ant Design Form組件實現的,代碼以下:
示例代碼位置:
/src/components/FormModal.tsx
import React from 'react'
import {Form, Input, Select} from 'antd'
import {FormComponentProps} from 'antd/lib/form'
import FormItem, {FormItemProps} from 'antd/lib/form/FormItem'
import Modal, {ModalProps} from 'antd/lib/modal'
const Option = Select.Option
// FormItem寬度兼容
export const formItemLayout: FormItemProps = {
labelCol: {
xs: {span: 24},
sm: {span: 6}
},
wrapperCol: {
xs: {span: 24},
sm: {span: 16}
}
}
// 性別枚舉
enum SexEnum {
male = 'male',
female = 'female'
}
// 性別名稱枚舉
enum SexNameEnum {
male = '男',
female = '女'
}
// 表單字段類型
export class FormModalValues {
username: string = ''
sex: SexEnum = SexEnum.male
}
export interface Props extends ModalProps, FormComponentProps {
}
export class State {
visible: boolean = false
}
export class FormModalComponent extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = new State()
}
// 打開彈窗
public show = (): void => {
this.setState({
visible: true
})
}
// 關閉彈窗
public hide = (): void => {
this.setState({
visible: false
})
}
public onOk = async () => {
// 方法1:使用回調函數獲取表單驗證結果
/* this.props.form.validateFields(async (errors: any, {username, sex}: FormModalValues) => {
if (!errors) {
Modal.success({
title: '表單輸入結果',
content: `用戶名:${username},性別:${SexNameEnum[sex]}。`
})
this.hide()
}
}) */
// 方法2:使用async函數獲取表單驗證結果
try {
// @ts-ignore
const {username, sex}: FormModalValues = await this.props.form.validateFields()
Modal.success({
title: '表單輸入結果',
content: `用戶名:${username},性別:${SexNameEnum[sex]}。`
})
this.hide()
} catch (error) {
console.error(error)
return error
}
}
// 關閉彈窗後初始化彈窗參數
public afterClose = (): void => {
// 重置表單
this.props.form.resetFields()
this.setState(new State())
}
componentDidMount() {
// 爲表單設置初始值,這裏與getFieldDecorator方法中的initialValue重複。
this.props.form.setFieldsValue(new FormModalValues())
}
render() {
const form = this.props.form
// 獲取用戶輸入的表單數據
const username: string = form.getFieldValue('username')
const sex: SexEnum = form.getFieldValue('sex')
return (
<Modal
visible={this.state.visible}
title={'新建用戶'}
maskClosable
onCancel={this.hide}
onOk={this.onOk}
afterClose={this.afterClose}
>
<FormItem
label={'請輸入用戶名'}
required={true}
{...formItemLayout}
>
{
// getFieldDecorator爲表單字段綁定value和onChange等事件,並實現校驗等功能
form.getFieldDecorator<FormModalValues>(
// 表單項數據字段
'username',
{
// 表單初始值
initialValue: '',
// 表單校驗規則
rules: [
{
required: true,
}
]
}
)(
<Input />
)
}
</FormItem>
<FormItem
label={'請選擇性別'}
required={true}
{...formItemLayout}
>
{
// getFieldDecorator爲表單字段綁定value和onChange等事件,並實現校驗等功能
form.getFieldDecorator<FormModalValues>(
// 表單項數據字段
'sex',
{
// 表單初始值
initialValue: SexEnum.male,
// 表單校驗規則
rules: [
{
required: true,
}
]
}
)(
<Select
style={{width: '60px'}}
>
<Option
value={'male'}
>
男
</Option>
<Option
value={'female'}
>
女
</Option>
</Select>
)
}
</FormItem>
<FormItem
label={'輸入的用戶名'}
{...formItemLayout}
>
{username}
</FormItem>
<FormItem
label={'選擇的性別'}
{...formItemLayout}
>
{
SexNameEnum[sex]
}
</FormItem>
</Modal>
)
}
}
const FormModal = Form.create<Props>()(FormModalComponent)
export default FormModal
複製代碼
在這個Demo中,咱們主要用到了如下幾個方法:
Form.create:建立一個新的表單組件,提供表單校驗、獲取數據等方法,以及存儲表單數據功能。
this.props.form.getFieldDecorator:爲表單字段綁定value和onChange等事件,並實現校驗等功能。
this.props.form.getFieldValue:獲取表單字段值。
this.props.form.setFieldsValue:爲表單字段設置值。
this.props.form.validateFields:進行表單校驗,並返回校驗結果和當前表單數據。
this.props.form.resetFields:重置表單數據爲初始值。
上面列出的方法中,除了Form.create,都是rc-form提供的方法。
但若是查看create方法的實現方式,能夠發現它直接調用了rc-form
下的createDOMForm
,以下:
示例代碼位置:
/ant-design/components/form/Form.tsx
import createDOMForm from 'rc-form/lib/createDOMForm';
static create = function create<TOwnProps extends FormComponentProps>(
options: FormCreateOption<TOwnProps> = {},
): FormWrappedProps<TOwnProps> {
return createDOMForm({
fieldNameProp: 'id',
...options,
fieldMetaProp: FIELD_META_PROP,
fieldDataProp: FIELD_DATA_PROP,
});
};
複製代碼
查看rc-form源碼,能夠看到createDOMForm
方法僅僅是調用了createBaseForm
方法。
createDOMForm示例代碼位置:
/rc-form/src/createDOMForm.js
function createDOMForm(option) {
return createBaseForm({
...option,
}, [mixin]);
}
複製代碼
如今咱們的重點應當放在createBaseForm
方法上,不過它的代碼足足有600多行,很難在短期內弄清楚全部細節。
但咱們只要理解createBaseForm
的大致結構,就能夠知道它主要完成了哪些功能。
如下是我簡化過的createBaseForm
代碼:
createBaseForm示例代碼位置:
/rc-form/src/createBaseForm.js
function createBaseForm(option = {}, mixins = []) {
return function decorate(WrappedComponent) {
const Form = createReactClass({
render() {
return <WrappedComponent {...props} />
}
})
return Form
}
}
export default createBaseForm
複製代碼
從這段代碼能夠看出,createBaseForm
方法實際上就是實現了一個高階組件(HOC)。
咱們如今已經知道createBaseForm
實際上是一個高階組件(HOC),那麼再來看與之用法類似的getFieldDecorator
方法,它的也是實現了一個高階組件(HOC)`。
我簡化過的getFieldDecorator
代碼以下:
getFieldDecorator示例代碼位置:
/rc-form/src/createBaseForm.js
getFieldDecorator(name, fieldOption) {
const props = this.getFieldProps(name, fieldOption);
return (fieldElem) => {
return React.cloneElement(fieldElem, {
...props,
});
};
}
複製代碼
高階組件是一個獲取組件並返回新組件的函數。
高階組件(HOC)有如下特色:
高階組件是對已有組件的封裝,造成了一個新組件,新組件實現了特定的業務邏輯,並將其經過props傳給原有組件。
高階組件一般不須要實現UI,其UI由傳入的原組件實現,它只是爲原組件提供了額外的功能或數據。
下面來看一個簡單的HOC例子:
示例代碼位置:/src/utils/createTimer.tsx
import React from 'react'
export interface Props {
wrappedComponentRef?: React.RefObject<any>
}
export class State {
time: Date = new Date()
}
export interface TimerProps {
time: Date
}
function createTimer(WrappedComponent: React.ComponentClass<TimerProps>): React.ComponentClass<Props> {
class Timer extends React.Component<Props, State> {
timer: number = 0
constructor(props: Props) {
super(props)
this.state = new State()
}
componentDidMount() {
this.timer = window.setInterval(() => {
this.setState({
time: new Date()
})
}, 1000)
}
componentWillUnmount() {
clearInterval(this.timer)
}
render() {
// 爲原組件提供time的props後,將其做爲組件返回顯示,不對UI作修改
return (
<WrappedComponent
ref={this.props.wrappedComponentRef}
time={this.state.time}
/>
)
}
}
// 返回新組件
return Timer
}
export default createTimer
複製代碼
這個例子實現了一個計時器的高階組件,它將當前要顯示的時間經過props中名爲time的屬性傳入原組件。
同時,在返回的新組件中,能夠經過設置wrappedComponentRef屬性,能夠獲取到原組件。
下面是一個使用createTimer
顯示計時器的一個簡單例子,該組件接收了HOC傳過來的time屬性,放入一個p標籤中顯示。
你能夠在http://localhost:3000/頁面中,表單彈窗
按鈕下方看到顯示的時間。
示例代碼位置:/src/components/ShowTimer.tsx
import React from 'react'
import moment from 'moment'
import createTimer, {TimerProps} from '../utils/createTimer';
// 表單字段類型
export interface Props extends TimerProps {
}
export class State {
}
export class ShowTimerComponent extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = new State()
}
render() {
return (
<p>
{moment(this.props.time).format('YYYY-MM-DD HH:mm:ss')}
</p>
)
}
}
// 導出用HOC建立的新組件
const ShowTimer = createTimer(ShowTimerComponent)
export default ShowTimer
複製代碼
下面是一個使用createTimer
建立彈窗顯示計時器的例子,彈窗組件接收了HOC傳過來的time屬性,並將其顯示出來。
同時將彈窗組件經過wrappedComponentRef屬性提供給外部使用,實現了打開、關閉彈窗功能。
你能夠在http://localhost:3000/頁面中,點擊時間彈窗
按鈕,查看效果。
示例代碼位置:/src/components/ShowTimerModal.tsx
import React from 'react'
import moment from 'moment'
import {Modal} from 'antd';
import {ModalProps} from 'antd/lib/modal';
import createTimer, {TimerProps} from '../utils/createTimer';
// 表單字段類型
export interface Props extends ModalProps, TimerProps {
}
export class State {
visible: boolean = false
}
export class ShowTimerModalComponent extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = new State()
}
// 打開彈窗
public show = (): void => {
this.setState({
visible: true
})
}
// 關閉彈窗
public hide = (): void => {
this.setState({
visible: false
})
}
render() {
return (
<Modal
visible={this.state.visible}
title={'彈窗顯示時間'}
maskClosable
cancelButtonProps={{style: {display: 'none'}}}
onCancel={this.hide}
onOk={this.hide}
>
{moment(this.props.time).format('YYYY-MM-DD HH:mm:ss')}
</Modal>
)
}
}
// 導出用HOC建立的新組件
const ShowTimerModal = createTimer(ShowTimerModalComponent)
export default ShowTimerModal
複製代碼
有了HOC的知識做爲鋪墊,咱們就能夠正式進入rc-form源碼解讀了。
開始正式的解讀以前,我先說說我我的對於閱讀源碼的意見,以rc-form爲例,它實現的功能雖然不是十分複雜,但因爲這是一個要提供給不少人使用的庫,爲了不出錯,就須要進行不少的校驗,以及開發環境提示等等。雖然這些都是必要的,但卻會致使代碼十分冗長,若是對代碼不熟悉的話,會有不小的閱讀障礙。
所以,我我的認爲在閱讀源碼的時候,能夠把重點放在如下兩個方面:
理解做者進行開發的思路。就如同談Redux的時候,都要了解的Flux架構。建議在閱讀源碼的時候,重點放在理解做者「爲何要這麼作」,而不是研究做者是如何實現某個功能的。
學習做者的優秀習慣、技巧。上面說重點要理解做者的思路,但並非讓你放棄關注細節,而是要有取捨地看。一些本身徹底有能力實現,或者做者只是在作一些報錯提示之類的代碼,能夠直接跳過。固然若是看到做者的一些優秀習慣、技巧,或者是一些本身沒有想過的實現方式,仍是頗有必要借鑑的。
我梳理了rc-form的實現思路,供你們參考,本次源碼解讀會按照下圖進行講解。建議你在查看rc-form源碼時,時常對照這張圖,這樣更加便於理解。
在以前的高階組件(HOC)講解中,已經解讀過createBaseForm
方法的實現方式,這裏就再也不贅述。
接下來將依次以createBaseForm
中的各個方法,講解一下rc-form的實現邏輯,每段代碼解讀都會提供該段代碼實現的主要功能,爲方便理解,在代碼中也提供了部分註釋。
createBaseForm
使用了createReactClass
方法建立一個React組件類,getInitialState
主要用來爲組件建立一些初始化參數、方法等,至關於ES6中的constructor
。
從代碼中能夠看到,createFieldsStore
方法爲該組件建立了存儲、讀取、設置表單數據等功能,並存儲在this.fieldsStore
屬性中。
表單原始數據,如initialValue(表單項初始值)、rules(表單校驗規則)等,都會存儲在this.fieldsStore.fieldsMeta
屬性中。
當前的表單數據,會存儲在this.fieldsStore.fields
屬性中。
示例代碼位置:
/rc-form/src/createBaseForm.js
getInitialState() {
// option.mapPropsToFields: 將值從props轉換爲字段。用於從redux store讀取字段。
const fields = mapPropsToFields && mapPropsToFields(this.props);
// createFieldsStore爲該組件提供了存儲、讀取、設置表單數據等功能
this.fieldsStore = createFieldsStore(fields || {});
this.instances = {};
this.cachedBind = {};
this.clearedFieldMetaCache = {};
this.renderFields = {};
this.domFields = {};
// 爲組件綁定了一系列方法,這些方法會經過props傳入新組件供其使用
// HACK: https://github.com/ant-design/ant-design/issues/6406
['getFieldsValue',
'getFieldValue',
'setFieldsInitialValue',
'getFieldsError',
'getFieldError',
'isFieldValidating',
'isFieldsValidating',
'isFieldsTouched',
'isFieldTouched'].forEach(key => {
this[key] = (...args) => {
if (process.env.NODE_ENV !== 'production') {
warning(
false,
'you should not use `ref` on enhanced form, please use `wrappedComponentRef`. ' +
'See: https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140'
);
}
// 該組件中的方法,直接調用了fieldsStore中的方法,也就是由createFieldsStore方法建立的
return this.fieldsStore[key](...args);
};
});
return {
submitting: false,
};
}
複製代碼
render方法實現了組裝新組件須要的props屬性與方法,並將其傳入新組件,這是一個普通的高階組件實現方式。
新組件的props主要來自於createBaseForm.js
和createForm.js
中定義的mixin對象,以下面的代碼:
示例代碼位置:
/rc-form/src/createForm.js
export const mixin = {
getForm() {
return {
getFieldsValue: this.fieldsStore.getFieldsValue,
getFieldValue: this.fieldsStore.getFieldValue,
getFieldInstance: this.getFieldInstance,
setFieldsValue: this.setFieldsValue,
setFields: this.setFields,
setFieldsInitialValue: this.fieldsStore.setFieldsInitialValue,
getFieldDecorator: this.getFieldDecorator,
getFieldProps: this.getFieldProps,
getFieldsError: this.fieldsStore.getFieldsError,
getFieldError: this.fieldsStore.getFieldError,
isFieldValidating: this.fieldsStore.isFieldValidating,
isFieldsValidating: this.fieldsStore.isFieldsValidating,
isFieldsTouched: this.fieldsStore.isFieldsTouched,
isFieldTouched: this.fieldsStore.isFieldTouched,
isSubmitting: this.isSubmitting,
submit: this.submit,
validateFields: this.validateFields,
resetFields: this.resetFields,
};
},
};
複製代碼
也就是說,mixin定義了須要傳遞給新組件使用的方法。
示例代碼位置:
/rc-form/src/createBaseForm.js
render() {
const {wrappedComponentRef, ...restProps} = this.props; // eslint-disable-line
const formProps = {
// getForm方法來自於createDOMForm方法調用createBaseForm方法時,傳入的mixin對象
// mixin合併了createForm.js中導出的的mixin
[formPropName]: this.getForm(),
};
if (withRef) {
if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') {
warning(
false,
'`withRef` is deprecated, please use `wrappedComponentRef` instead. ' +
'See: https://github.com/react-component/form#note-use-wrappedcomponentref-instead-of-withref-after-rc-form140'
);
}
formProps.ref = 'wrappedComponent';
} else if (wrappedComponentRef) {
formProps.ref = wrappedComponentRef;
}
// 建立新組件的props
const props = mapProps.call(this, {
...formProps,
...restProps,
});
return <WrappedComponent {...props} />;
},
});
複製代碼
getFieldDecorator
方法主要實現了一個高階組件(HOC),它主要爲新組件增長了綁定value屬性和onChange事件,以及實現了onChange時的表單校驗功能。
新組件的props是經過getFieldProps
方法建立,該方法主要實現了綁定onChange事件,確保表單可以獲取到表單項輸入的值,在onChange的同時使用async-validator進行校驗。
示例代碼位置:
/rc-form/src/createBaseForm.js
// 獲取當前表單項的field、fieldMeta數據
onCollectCommon(name, action, args) {
const fieldMeta = this.fieldsStore.getFieldMeta(name);
if (fieldMeta[action]) {
fieldMeta[action](...args);
} else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) {
fieldMeta.originalProps[action](...args);
}
// 經過getValueFromEvent方法,從event中獲取當前表單項的值,fieldMeta.getValueFromEvent爲用戶自定義的方法。
const value = fieldMeta.getValueFromEvent ?
fieldMeta.getValueFromEvent(...args) :
getValueFromEvent(...args);
if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) {
const valuesAll = this.fieldsStore.getAllValues();
const valuesAllSet = {};
valuesAll[name] = value;
Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key]));
onValuesChange({
[formPropName]: this.getForm(),
...this.props
}, set({}, name, value), valuesAllSet);
}
// 獲取相應字段的field數據
const field = this.fieldsStore.getField(name);
return ({name, field: {...field, value, touched: true}, fieldMeta});
},
// 設置表單數據
onCollect(name_, action, ...args) {
// 獲取當前表單數據及設置
const {name, field, fieldMeta} = this.onCollectCommon(name_, action, args);
const {validate} = fieldMeta;
this.fieldsStore.setFieldsAsDirty();
const newField = {
...field,
dirty: hasRules(validate),
};
this.setFields({
[name]: newField,
});
},
onCollectValidate(name_, action, ...args) {
// 獲取當前表單數據及設置
const {field, fieldMeta} = this.onCollectCommon(name_, action, args);
const newField = {
...field,
dirty: true,
};
this.fieldsStore.setFieldsAsDirty();
// 進行表單校驗,並存儲表單數據
this.validateFieldsInternal([newField], {
action,
options: {
firstFields: !!fieldMeta.validateFirst,
},
});
},
// 返回一個表單項的onChange事件
getCacheBind(name, action, fn) {
if (!this.cachedBind[name]) {
this.cachedBind[name] = {};
}
const cache = this.cachedBind[name];
if (!cache[action] || cache[action].oriFn !== fn) {
cache[action] = {
fn: fn.bind(this, name, action),
oriFn: fn,
};
}
return cache[action].fn;
},
// 建立新的表單項組件
getFieldDecorator(name, fieldOption) {
// 註冊表單項,獲取新表單項的props,主要是value屬性和onChange事件等
const props = this.getFieldProps(name, fieldOption);
return (fieldElem) => {
// We should put field in record if it is rendered
this.renderFields[name] = true;
const fieldMeta = this.fieldsStore.getFieldMeta(name);
const originalProps = fieldElem.props;
// 這段是在生產環境的打印提示語
if (process.env.NODE_ENV !== 'production') {
const valuePropName = fieldMeta.valuePropName;
warning(
!(valuePropName in originalProps),
`\`getFieldDecorator\` will override \`${valuePropName}\`, ` +
`so please don't set \`${valuePropName}\` directly ` + `and use \`setFieldsValue\` to set it.` ); const defaultValuePropName = `default${valuePropName[0].toUpperCase()}${valuePropName.slice(1)}`; warning( !(defaultValuePropName in originalProps), `\`${defaultValuePropName}\` is invalid ` + `for \`getFieldDecorator\` will set \`${valuePropName}\`,` + ` please use \`option.initialValue\` instead.` ); } fieldMeta.originalProps = originalProps; fieldMeta.ref = fieldElem.ref; return React.cloneElement(fieldElem, { ...props, // 該方法用於返回當前表單存儲的value值 ...this.fieldsStore.getFieldValuePropValue(fieldMeta), }); }; }, // 建立表單項組件的props getFieldProps(name, usersFieldOption = {}) { if (!name) { throw new Error('Must call `getFieldProps` with valid name string!'); } if (process.env.NODE_ENV !== 'production') { warning( this.fieldsStore.isValidNestedFieldName(name), `One field name cannot be part of another, e.g. \`a\` and \`a.b\`. Check field: ${name}` ); warning( !('exclusive' in usersFieldOption), '`option.exclusive` of `getFieldProps`|`getFieldDecorator` had been remove.' ); } delete this.clearedFieldMetaCache[name]; const fieldOption = { name, trigger: DEFAULT_TRIGGER, valuePropName: 'value', validate: [], ...usersFieldOption, }; const { rules, trigger, validateTrigger = trigger, validate, } = fieldOption; const fieldMeta = this.fieldsStore.getFieldMeta(name); if ('initialValue' in fieldOption) { fieldMeta.initialValue = fieldOption.initialValue; } const inputProps = { ...this.fieldsStore.getFieldValuePropValue(fieldOption), ref: this.getCacheBind(name, `${name}__ref`, this.saveRef), }; if (fieldNameProp) { inputProps[fieldNameProp] = formName ? `${formName}_${name}` : name; } // 獲取表單項校驗觸發事件及校驗規則 const validateRules = normalizeValidateRules(validate, rules, validateTrigger); // 獲取表單項校驗事件 const validateTriggers = getValidateTriggers(validateRules); validateTriggers.forEach((action) => { // 若已綁定了校驗事件,則返回 if (inputProps[action]) return; // 綁定收集表單數據及校驗事件 inputProps[action] = this.getCacheBind(name, action, this.onCollectValidate); }); // 若validateTriggers爲空,則綁定普通事件,不進行校驗 // 使用onCollect方法,獲取綁定表單項輸入值事件,將其存儲到inputProps中,並返回給組件用做props // make sure that the value will be collect if (trigger && validateTriggers.indexOf(trigger) === -1) { // getCacheBind負責返回一個表單項的onChange事件 inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect); } // 將當前已設置的表單選項,與新表單選項合併,並存入fieldsMeta屬性 const meta = { ...fieldMeta, ...fieldOption, validate: validateRules, }; // 註冊表單項,將表單設置如initialValue、validateRules等,存儲到this.fieldsStore.fieldsMeta[name]中 this.fieldsStore.setFieldMeta(name, meta); if (fieldMetaProp) { inputProps[fieldMetaProp] = meta; } if (fieldDataProp) { inputProps[fieldDataProp] = this.fieldsStore.getField(name); } // This field is rendered, record it this.renderFields[name] = true; return inputProps; }, 複製代碼
示例代碼位置:
/rc-form/src/utils.js
/**
* 將validate、rules、validateTrigger三個參數配置的校驗事件及規則,整理成統一的校驗事件、規則
* @param {Array<object>} validate 校驗事件、規則
* @param {string} validate[].trigger 校驗事件
* @param {object[]} validate[].rules 校驗規則,參考async-validator,https://github.com/yiminghe/async-validator
* @param {object[]} rules 校驗規則,參考async-validator,https://github.com/yiminghe/async-validator
* @param {string} validateTrigger 校驗事件
* @returns {Array<object>} validateRules 校驗事件、規則
* @returns {string[]} validateRules[].trigger 校驗事件
* @returns {object[]} validateRules[].rules 校驗規則,參考async-validator,https://github.com/yiminghe/async-validator
*/
export function normalizeValidateRules(validate, rules, validateTrigger) {
const validateRules = validate.map((item) => {
const newItem = {
...item,
trigger: item.trigger || [],
};
if (typeof newItem.trigger === 'string') {
newItem.trigger = [newItem.trigger];
}
return newItem;
});
if (rules) {
validateRules.push({
trigger: validateTrigger ? [].concat(validateTrigger) : [],
rules,
});
}
return validateRules;
}
/**
* 將validate、rules、validateTrigger三個參數配置的校驗事件及規則,整理成統一的校驗事件、規則
* @param {Array<object>} validateRules 校驗事件、規則
* @param {string[]} validateRules[].trigger 校驗事件
* @param {object[]} validateRules[].rules 校驗規則,參考async-validator,https://github.com/yiminghe/async-validator
* @returns {Array<string>} 校驗事件
*/
export function getValidateTriggers(validateRules) {
return validateRules
.filter(item => !!item.rules && item.rules.length)
.map(item => item.trigger)
.reduce((pre, curr) => pre.concat(curr), []);
}
// 判斷表單項類型,獲取表單數據,默認支持經過event.target.value或event.target.checked獲取
export function getValueFromEvent(e) {
// To support custom element
if (!e || !e.target) {
return e;
}
const {target} = e;
return target.type === 'checkbox' ? target.checked : target.value;
}
複製代碼
createBaseForm.js
中並未實現getFieldsValue
、getFieldValue
方法,而是直接調用了this.fieldsStore.getFieldsValue
、this.fieldsStore.getFieldValue
方法,它們實現的功能是從存儲的數據中,查找出指定的數據。
this.fieldsStore.getFieldsValue
方法如未指定須要查找的數據,則返回全部數據。
this.fieldsStore.getNestedField
是一個公用方法,根據傳入的字段名,或者表單已存儲的字段名,使用傳入的回調函數獲取所需的數據。
this.fieldsStore.getValueFromFields
方法,根據傳入的字段名,獲取當前表單的值,若值不存在,則返回已設置的initialValue。
示例代碼位置:
/rc-form/src/createFieldsStore.js
getFieldsValue = (names) => {
return this.getNestedFields(names, this.getFieldValue);
}
getNestedFields(names, getter) {
const fields = names || this.getValidFieldsName();
return fields.reduce((acc, f) => set(acc, f, getter(f)), {});
}
getFieldValue = (name) => {
const {fields} = this;
return this.getNestedField(name, (fullName) => this.getValueFromFields(fullName, fields));
}
// 從傳入的fields中,按name獲取相應的值,若沒有則直接返回fieldMeta中設置的initialValue
getValueFromFields(name, fields) {
const field = fields[name];
if (field && 'value' in field) {
return field.value;
}
const fieldMeta = this.getFieldMeta(name);
return fieldMeta && fieldMeta.initialValue;
}
// 根據傳入的name,獲取fieldMeta中存在的字段名稱,最終調用getter函數獲取相應的值
getNestedField(name, getter) {
const fullNames = this.getValidFieldsFullName(name);
if (
fullNames.length === 0 || // Not registered
(fullNames.length === 1 && fullNames[0] === name) // Name already is full name.
) {
return getter(name);
}
const isArrayValue = fullNames[0][name.length] === '[';
const suffixNameStartIndex = isArrayValue ? name.length : name.length + 1;
return fullNames
.reduce(
(acc, fullName) => set(
acc,
fullName.slice(suffixNameStartIndex),
getter(fullName)
),
isArrayValue ? [] : {}
);
}
// 獲取存儲的表單字段名稱
getValidFieldsFullName(maybePartialName) {
const maybePartialNames = Array.isArray(maybePartialName) ?
maybePartialName : [maybePartialName];
return this.getValidFieldsName()
.filter(fullName => maybePartialNames.some(partialName => (
fullName === partialName || (
startsWith(fullName, partialName) &&
['.', '['].indexOf(fullName[partialName.length]) >= 0
)
)));
}
// 獲取存儲的表單字段名稱
getValidFieldsName() {
const {fieldsMeta} = this;
// 過濾出fieldsMeta中存儲的未被設置爲hidden的數據
return fieldsMeta ?
Object.keys(fieldsMeta).filter(name => !this.getFieldMeta(name).hidden) :
[];
}
// 獲取存儲的字段數據
getFieldMeta(name) {
this.fieldsMeta[name] = this.fieldsMeta[name] || {};
return this.fieldsMeta[name];
}
複製代碼
setFieldsValue
方法,實現的就是設置表單數據的功能,代碼按以下流程調用:
this.setFieldsValue
→ this.setFields
→ this.fieldsStore.setFields
最終新數據存儲在this.fieldsStore.fields
中。
示例代碼位置:
/rc-form/src/createBaseForm.js
setFieldsValue(changedValues, callback) {
const {fieldsMeta} = this.fieldsStore;
// 過濾出已註冊的表單項的值
const values = this.fieldsStore.flattenRegisteredFields(changedValues);
const newFields = Object.keys(values).reduce((acc, name) => {
const isRegistered = fieldsMeta[name];
if (process.env.NODE_ENV !== 'production') {
warning(
isRegistered,
'Cannot use `setFieldsValue` until ' +
'you use `getFieldDecorator` or `getFieldProps` to register it.'
);
}
if (isRegistered) {
const value = values[name];
acc[name] = {
value,
};
}
return acc;
}, {});
// 設置表單的值
this.setFields(newFields, callback);
if (onValuesChange) {
const allValues = this.fieldsStore.getAllValues();
onValuesChange({
[formPropName]: this.getForm(),
...this.props
}, changedValues, allValues);
}
}
setFields(maybeNestedFields, callback) {
const fields = this.fieldsStore.flattenRegisteredFields(maybeNestedFields);
this.fieldsStore.setFields(fields);
if (onFieldsChange) {
const changedFields = Object.keys(fields)
.reduce((acc, name) => set(acc, name, this.fieldsStore.getField(name)), {});
onFieldsChange({
[formPropName]: this.getForm(),
...this.props
}, changedFields, this.fieldsStore.getNestedAllFields());
}
this.forceUpdate(callback);
}
複製代碼
示例代碼位置:
/rc-form/src/createFieldsStore.js
// 設置表單值
setFields(fields) {
const fieldsMeta = this.fieldsMeta;
// 將當前數據和傳入的新數據合併
const nowFields = {
...this.fields,
...fields,
};
const nowValues = {};
// 按照fieldsMeta中已註冊的字段,從nowFields中取出這些字段的最新值,若是爲空則設置爲initialValue,造成新表單數據nowValues
Object.keys(fieldsMeta)
.forEach((f) => {
nowValues[f] = this.getValueFromFields(f, nowFields);
});
// 若是該表單項有設置normalize方法,則返回normalize以後的數據
// 可參考如這個例子:https://codepen.io/afc163/pen/JJVXzG?editors=001
Object.keys(nowValues).forEach((f) => {
const value = nowValues[f];
const fieldMeta = this.getFieldMeta(f);
if (fieldMeta && fieldMeta.normalize) {
const nowValue =
fieldMeta.normalize(value, this.getValueFromFields(f, this.fields), nowValues);
if (nowValue !== value) {
nowFields[f] = {
...nowFields[f],
value: nowValue,
};
}
}
});
this.fields = nowFields;
}
複製代碼
resetFields
方法,實現了重置表單爲初始值功能。它的實現方式是:
先調用this.fieldsStore.resetFields
方法,清空全部表單數據。
調用this.fieldsStore.setFields
方法設置表單數據。因設置數據時並未傳入新數據,默認會設置爲fieldsMeta中存儲的initialValue,以達到重置表單的目的。
示例代碼位置:
/rc-form/src/createBaseForm.js
resetFields(ns) {
// 獲取清空了全部fields中存儲數據的對象
const newFields = this.fieldsStore.resetFields(ns);
if (Object.keys(newFields).length > 0) {
// 爲newFields中的各個字段賦值,因爲數據都爲空,則會從fieldsMeta中查找initialValue賦值。
this.setFields(newFields);
}
if (ns) {
const names = Array.isArray(ns) ? ns : [ns];
names.forEach(name => delete this.clearedFieldMetaCache[name]);
} else {
this.clearedFieldMetaCache = {};
}
}
複製代碼
示例代碼位置:
/rc-form/src/createFieldsStore.js
resetFields(ns) {
const {fields} = this;
// 獲取須要重置的字段名稱
const names = ns ?
this.getValidFieldsFullName(ns) :
this.getAllFieldsName();
// 若是當前fields中存在數據,則清空。最終返回的是清空了全部現有數據的對象。
return names.reduce((acc, name) => {
const field = fields[name];
if (field && 'value' in field) {
acc[name] = {};
}
return acc;
}, {});
}
複製代碼
this.getFieldError
用於獲取傳入表單項的校驗結果,包括校驗的屬性名稱和提示語。
this.getFieldError
的調用方式與this.getFieldValue
相似,最終也是經過調用this.fieldsStore.getNestedField
方法,同時傳入相應的回調函數,獲取到須要的校驗結果。
示例代碼位置:
/rc-form/src/createBaseForm.js
// 獲取校驗結果,返回的是校驗錯誤提示語的數組,格式爲string[]
getFieldError = (name) => {
return this.getNestedField(
name,
(fullName) => getErrorStrs(this.getFieldMember(fullName, 'errors'))
);
}
// 根據傳入的name,獲取fieldMeta中存在的字段名稱,最終調用getter函數獲取相應的值
getNestedField(name, getter) {
const fullNames = this.getValidFieldsFullName(name);
if (
fullNames.length === 0 || // Not registered
(fullNames.length === 1 && fullNames[0] === name) // Name already is full name.
) {
return getter(name);
}
const isArrayValue = fullNames[0][name.length] === '[';
const suffixNameStartIndex = isArrayValue ? name.length : name.length + 1;
return fullNames
.reduce(
(acc, fullName) => set(
acc,
fullName.slice(suffixNameStartIndex),
getter(fullName)
),
isArrayValue ? [] : {}
);
}
// 從當前字段數據中,獲取傳入member類型的數據
getFieldMember(name, member) {
return this.getField(name)[member];
}
// 獲取存儲的字段數據及校驗結果,並補充字段名稱
getField(name) {
return {
...this.fields[name],
name,
};
}
複製代碼
示例代碼位置:
/rc-form/src/utils.js
// 將錯誤數據整理後返回,返回的是校驗錯誤提示語的數組,格式爲string[]
function getErrorStrs(errors) {
if (errors) {
return errors.map((e) => {
if (e && e.message) {
return e.message;
}
return e;
});
}
return errors;
}
複製代碼
this.validateFields
實現的是,先過濾出有配置rules校驗規則的表單項,調用async-validator
進行校驗,並返回校驗結果。
示例代碼位置:
/rc-form/src/createBaseForm.js
// 校驗表單方法
validateFieldsInternal(fields, {
fieldNames,
action,
options = {},
}, callback) {
const allRules = {};
const allValues = {};
const allFields = {};
const alreadyErrors = {};
// 先清空已有的校驗結果
fields.forEach((field) => {
const name = field.name;
if (options.force !== true && field.dirty === false) {
if (field.errors) {
set(alreadyErrors, name, {errors: field.errors});
}
return;
}
const fieldMeta = this.fieldsStore.getFieldMeta(name);
const newField = {
...field,
};
newField.errors = undefined;
newField.validating = true;
newField.dirty = true;
allRules[name] = this.getRules(fieldMeta, action);
allValues[name] = newField.value;
allFields[name] = newField;
});
// 設置清空後的表單校驗結果
this.setFields(allFields);
// in case normalize
Object.keys(allValues).forEach((f) => {
allValues[f] = this.fieldsStore.getFieldValue(f);
});
if (callback && isEmptyObject(allFields)) {
callback(isEmptyObject(alreadyErrors) ? null : alreadyErrors,
this.fieldsStore.getFieldsValue(fieldNames));
return;
}
// 使用AsyncValidator進行校驗,並返回校驗結果
const validator = new AsyncValidator(allRules);
if (validateMessages) {
validator.messages(validateMessages);
}
validator.validate(allValues, options, (errors) => {
const errorsGroup = {
...alreadyErrors,
};
// 若是校驗不經過,則整理AsyncValidator返回的數據,並存儲到表單數據中
if (errors && errors.length) {
errors.forEach((e) => {
const errorFieldName = e.field;
let fieldName = errorFieldName;
// Handle using array validation rule.
// ref: https://github.com/ant-design/ant-design/issues/14275
Object.keys(allRules).some((ruleFieldName) => {
const rules = allRules[ruleFieldName] || [];
// Exist if match rule
if (ruleFieldName === errorFieldName) {
fieldName = ruleFieldName;
return true;
}
// Skip if not match array type
if (rules.every(({type}) => type !== 'array') && errorFieldName.indexOf(ruleFieldName) !== 0) {
return false;
}
// Exist if match the field name
const restPath = errorFieldName.slice(ruleFieldName.length + 1);
if (/^\d+$/.test(restPath)) {
fieldName = ruleFieldName;
return true;
}
return false;
});
const field = get(errorsGroup, fieldName);
if (typeof field !== 'object' || Array.isArray(field)) {
set(errorsGroup, fieldName, {errors: []});
}
const fieldErrors = get(errorsGroup, fieldName.concat('.errors'));
fieldErrors.push(e);
});
}
const expired = [];
const nowAllFields = {};
Object.keys(allRules).forEach((name) => {
const fieldErrors = get(errorsGroup, name);
const nowField = this.fieldsStore.getField(name);
// avoid concurrency problems
if (!eq(nowField.value, allValues[name])) {
expired.push({
name,
});
} else {
nowField.errors = fieldErrors && fieldErrors.errors;
nowField.value = allValues[name];
nowField.validating = false;
nowField.dirty = false;
nowAllFields[name] = nowField;
}
});
// 存儲新表單數據及結果
this.setFields(nowAllFields);
if (callback) {
if (expired.length) {
expired.forEach(({name}) => {
const fieldErrors = [{
message: `${name} need to revalidate`,
field: name,
}];
set(errorsGroup, name, {
expired: true,
errors: fieldErrors,
});
});
}
callback(isEmptyObject(errorsGroup) ? null : errorsGroup,
this.fieldsStore.getFieldsValue(fieldNames));
}
});
},
// 校驗表單方法,主要用於整理須要校驗的表單項數據後,調用validateFieldsInternal進行校驗
validateFields(ns, opt, cb) {
const pending = new Promise((resolve, reject) => {
// 因傳入的3個參數都爲可選,須要將它們整理成固定的names, options, callback參數。
const {names, options} = getParams(ns, opt, cb);
let {callback} = getParams(ns, opt, cb);
if (!callback || typeof callback === 'function') {
const oldCb = callback;
callback = (errors, values) => {
if (oldCb) {
oldCb(errors, values);
} else if (errors) {
reject({errors, values});
} else {
resolve(values);
}
};
}
const fieldNames = names ?
this.fieldsStore.getValidFieldsFullName(names) :
this.fieldsStore.getValidFieldsName();
// 獲取須要校驗的表單項
const fields = fieldNames
// 過濾出已配置rules的字段
.filter(name => {
const fieldMeta = this.fieldsStore.getFieldMeta(name);
return hasRules(fieldMeta.validate);
})
// 獲取當前表單數據
.map((name) => {
const field = this.fieldsStore.getField(name);
field.value = this.fieldsStore.getFieldValue(name);
return field;
});
if (!fields.length) {
callback(null, this.fieldsStore.getFieldsValue(fieldNames));
return;
}
if (!('firstFields' in options)) {
options.firstFields = fieldNames.filter((name) => {
const fieldMeta = this.fieldsStore.getFieldMeta(name);
return !!fieldMeta.validateFirst;
});
}
// 調用表單校驗方法,進行校驗
this.validateFieldsInternal(fields, {
fieldNames,
options,
}, callback);
});
pending.catch((e) => {
if (console.error && process.env.NODE_ENV !== 'production') {
console.error(e);
}
return e;
});
return pending;
},
複製代碼
在上一小節,我已經爲你梳理了rc-form的實現思路,以及部分經常使用方法的實現方式。
相信你已經發現,rc-form的實現思路其實不復雜,分別使用HOC爲新表單和表單項提供了所須要的擴展方法。
而createFieldsStore.js
主要是爲了實現這些拓展方法,而這些實現較爲分複雜,雖然都是必要的,但確實對理解代碼形成了一些障礙。
在閱讀的時候,能夠沒必要要過於拘泥於其中的細節,其實只要理解了使用HOC進行封裝這一點,即便不理解createFieldsStore.js
中的具體實現方式,也足夠指導咱們按照本身的思路來實現rc-form了。
我根據分析的rc-form實現思路,本身實現了一個rc-form功能,你能夠在http://localhost:3000/頁面中,點擊新表單彈窗
按鈕,查看效果。
實現rc-form示例代碼位置:
/src/utils/createForm.tsx
import React from 'react'
import {observer} from 'mobx-react';
import {observable, runInAction, toJS} from 'mobx';
import hoistStatics from 'hoist-non-react-statics';
import AsyncValidator, {Rules, ValidateError, ErrorList, RuleItem} from 'async-validator';
// setFieldsValue設置表單數據時傳入的數據類型
export class Values {
[propName: string]: any
}
// 表單項設置
export class FieldOption {
initialValue?: any
rules: RuleItem[] = []
}
// 表單項數據
export class Field {
value: any
errors: ErrorList = []
}
// 表格數據
export class Fields {
[propName: string]: Field
}
// 表單項設置數據
export class FieldMeta {
name: string = ''
fieldOption: FieldOption = new FieldOption()
}
// 表格設置數據
export class FieldsMeta {
[propName: string]: FieldMeta
}
export interface Props {
wrappedComponentRef: React.RefObject<React.Component<FormComponentProps, any, any>>
}
// 爲原組件添加的form參數
export interface FormProps {
getFieldDecorator: (name: string, fieldOption: FieldOption) => (fieldElem: React.ReactElement) => React.ReactElement
getFieldValue: (name: string) => any
setFieldsValue: (values: any) => void
getFieldsValue: () => any
validateFields: (callback?: (errors: any, values: any) => void) => void
resetFields: () => void
getFieldError: (name: string) => ErrorList
}
// 爲原組件添加的props
export interface FormComponentProps {
form: FormProps
}
export class State {
}
function createForm(WrappedComponent: React.ComponentClass<FormComponentProps>): React.ComponentClass<Props> {
@observer
class Form extends React.Component<Props, State> {
// 表單數據
@observable
private fields: Fields = new Fields()
// 表單原始數據
@observable
private fieldsMeta: FieldsMeta = new FieldsMeta()
constructor(props: Props) {
super(props)
this.state = new State()
}
// 建立表單項的props,提供給getFieldDecorator綁定事件
private getFieldProps = (
name: string,
fieldOption: FieldOption = new FieldOption()
): any => {
const initialValue = fieldOption.initialValue
runInAction(() => {
if (!this.fields[name]) {
this.fields[name] = new Field()
if (initialValue) {
this.fields[name].value = initialValue
}
}
if (!this.fieldsMeta[name]) {
this.fieldsMeta[name] = {
name,
fieldOption
}
}
})
return {
value: toJS(this.fields)[name].value,
onChange: (event: React.ChangeEvent<HTMLInputElement> | string): void => {
if (typeof event === 'string') {
this.fields[name].value = event
} else {
this.fields[name].value = event.target.value
}
this.forceUpdate()
this.validateField(name)
}
}
}
// 建立新表單項組件的HOC
private getFieldDecorator = (
name: string,
fieldOption: FieldOption = new FieldOption()
): (fieldElem: React.ReactElement) => React.ReactElement => {
const props = this.getFieldProps(name, fieldOption)
return (fieldElem: React.ReactElement): React.ReactElement => {
return React.cloneElement(
fieldElem,
props
)
}
}
// 獲取表單項數據
private getFieldValue = (name: string): any => {
const field = toJS(this.fields)[name]
return field && field.value
}
// 獲取全部表單數據
private getFieldsValue = (): Values => {
const fields = toJS(this.fields)
let values: Values = {}
Object.keys(fields).forEach((name: string): void => {
values[name] = fields[name]
})
return values
}
// 設置表單項的值
private setFieldsValue = (values: Values): void => {
const fields = toJS(this.fields)
Object.keys(values).forEach((name: string): void => {
fields[name].value = values[name]
})
this.fields = fields
}
// 獲取用於表單校驗的值和規則
private getRulesValues = (name?: string): {rules: Rules, values: Fields} => {
const fields = toJS(this.fields)
const fieldsMeta = toJS(this.fieldsMeta)
const fieldMetaArr: FieldMeta[] = name ? [fieldsMeta[name]] : Object.values(fieldsMeta)
const values: Fields = new Fields()
const rules: Rules = fieldMetaArr.reduce((rules: Rules, item: FieldMeta): Rules => {
if (item.fieldOption.rules.length) {
values[item.name] = fields[item.name].value
return {
...rules,
[item.name]: item.fieldOption.rules
}
}
return rules
}, {})
return {rules, values}
}
// 校驗單個表單項
private validateField = (name: string): void => {
const {rules, values} = this.getRulesValues(name)
const validator = new AsyncValidator(rules)
validator.validate(values, {}, (errors: ErrorList): void => {
this.fields[name].errors = []
if (errors) {
errors.forEach((error: ValidateError): void => {
this.fields[name].errors.push(error)
})
}
})
}
// 校驗整個表單
private validateFields = (callback?: (errors: ErrorList | null, values: Fields) => void): void => {
const {rules, values} = this.getRulesValues()
const validator = new AsyncValidator(rules)
validator.validate(values, {}, (errors: ErrorList): void => {
Object.keys(values).forEach((name: string): void => {
this.fields[name].errors = []
})
if (errors) {
errors.forEach((error: ValidateError): void => {
this.fields[error.field].errors.push(error)
})
}
callback && callback(errors, values)
})
// 強制渲染組件,避免
this.forceUpdate()
}
// 重置表單
private resetFields = (): void => {
this.fields = Object.values(toJS(this.fieldsMeta)).reduce((fields: Fields, item: FieldMeta): Fields => {
fields[item.name] = new Field()
fields[item.name].value = item.fieldOption.initialValue
return fields
}, new Fields())
}
// 獲取表單項的校驗結果
private getFieldError = (name: string): ErrorList => {
return this.fields[name] ? this.fields[name].errors : []
}
render() {
let props: FormComponentProps = {
form: {
getFieldDecorator: this.getFieldDecorator,
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
setFieldsValue: this.setFieldsValue,
validateFields: this.validateFields,
resetFields: this.resetFields,
getFieldError: this.getFieldError,
}
}
return (
<WrappedComponent
ref={this.props.wrappedComponentRef}
{...props}
/>
)
}
}
// 使用hoist-non-react-statics庫,複製全部靜態方法,請查看:
// https://github.com/mridgway/hoist-non-react-statics
// https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over
return hoistStatics(Form, WrappedComponent)
}
export default createForm
複製代碼
使用方法示例代碼位置:
/src/utils/NewFormModal.tsx
import React from 'react'
import {Input, Select} from 'antd'
import FormItem, {FormItemProps} from 'antd/lib/form/FormItem'
import Modal, {ModalProps} from 'antd/lib/modal'
import createForm, {FormComponentProps, FormProps} from '../utils/createForm'
import {ErrorList, ValidateError} from 'async-validator'
const Option = Select.Option
// FormItem寬度兼容
export const formItemLayout: FormItemProps = {
labelCol: {
xs: {span: 24},
sm: {span: 6}
},
wrapperCol: {
xs: {span: 24},
sm: {span: 16}
}
}
// 性別枚舉
enum SexEnum {
male = 'male',
female = 'female'
}
enum SexNameEnum {
male = '男',
female = '女'
}
// 表單字段類型
export class FormModalValues {
username: string = ''
sex: SexEnum = SexEnum.male
}
export interface Props extends ModalProps, FormComponentProps {
}
export class State {
visible: boolean = false
}
export class NewFormModalComponent extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = new State()
}
// 打開彈窗
public show = (): void => {
this.setState({
visible: true
})
}
// 關閉彈窗
public hide = (): void => {
this.setState({
visible: false
})
}
// 點擊確認按鈕
public onOk = () => {
// 讀取當前表單數據
const values: FormModalValues = this.props.form.getFieldsValue()
console.log(values)
this.props.form.validateFields((errors: any, {username, sex}: FormModalValues) => {
if (!errors) {
Modal.success({
title: '表單輸入結果',
content: `用戶名:${username},性別:${SexNameEnum[sex]}。`
})
this.hide()
}
})
}
// 關閉彈窗後初始化彈窗參數
public afterClose = (): void => {
this.props.form.resetFields()
this.setState(new State())
}
componentDidMount() {
this.props.form.setFieldsValue(new FormModalValues())
}
render() {
const visible = this.state.visible
const form: FormProps = this.props.form
const username = form.getFieldValue('username')
const sex: SexEnum = form.getFieldValue('sex')
const usernameError: ErrorList = form.getFieldError('username')
const sexError: ErrorList = form.getFieldError('sex')
return (
<Modal
visible={visible}
title={'新建用戶'}
onCancel={this.hide}
onOk={this.onOk}
afterClose={this.afterClose}
>
<FormItem
label={'請輸入用戶名'}
required={true}
validateStatus={usernameError.length ? 'error' : undefined}
help={usernameError.length ? usernameError.map((item: ValidateError) => item.message).join(',') : undefined}
{...formItemLayout}
>
{
form.getFieldDecorator(
'username',
{
initialValue: '',
rules: [
{
required: true,
}
]
}
)(
<Input />
)
}
</FormItem>
<FormItem
label={'請選擇性別'}
required={true}
validateStatus={sexError.length ? 'error' : undefined}
help={sexError.length ? sexError.map((item: ValidateError) => item.message).join(',') : undefined}
{...formItemLayout}
>
{
form.getFieldDecorator(
'sex',
{
initialValue: SexEnum.male,
rules: [
{
required: true,
}
]
}
)(
<Select
style={{width: '60px'}}
>
<Option
value={'male'}
>
男
</Option>
<Option
value={'female'}
>
女
</Option>
</Select>
)
}
</FormItem>
<FormItem
label={'輸入的用戶名'}
{...formItemLayout}
>
{username}
</FormItem>
<FormItem
label={'選擇的性別'}
{...formItemLayout}
>
{
SexNameEnum[sex]
}
</FormItem>
</Modal>
)
}
}
const NewFormModal = createForm(NewFormModalComponent)
export default NewFormModal
複製代碼
在本文中,我圍繞rc-form爲你介紹了以下內容:
但願可以對你理解和使用rc-form有所幫助。