好久沒更新博客了, 皮的嘛,就不談了,不過問題不大,今天就結合 項目中寫的一個 React 高階組件 的實例 再來說一講,結合上一篇文章,加深一下印象html
國民組件庫 Ant-Design
的 Form
庫 想必你們都用過, 比較強大, 基於 rc-form
封裝, 功能比較齊全react
最近項目中遇到了一個需求, 普通的一個表單, 表單字段沒有 填完的時候, 提交按鈕 是 disabled
狀態的, 聽起來很簡單, 因爲用的是 antd
翻了翻文檔, copy 了一下代碼 , 發現須要些很多的代碼es6
import { Form, Icon, Input, Button } from 'antd';
const FormItem = Form.Item;
function hasErrors(fieldsError) {
return Object.keys(fieldsError).some(field => fieldsError[field]);
}
@Form.create();
class Page extends React.Component<{},{}> {
componentDidMount() {
this.props.form.validateFields();
}
handleSubmit = (e: React.FormEvent<HTMLButtonElement>) => {
e.preventDefault();
this.props.form.validateFields((err:any, values:any) => {
if (!err) {
...
}
});
}
render() {
const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form;
const userNameError = isFieldTouched('userName') && getFieldError('userName');
const passwordError = isFieldTouched('password') && getFieldError('password');
return (
<Form layout="inline" onSubmit={this.handleSubmit}>
<FormItem
validateStatus={userNameError ? 'error' : ''}
help={userNameError || ''}
>
{getFieldDecorator('userName', {
rules: [{ required: true, message: 'Please input your username!' }],
})(
<Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="Username" />
)}
</FormItem>
<FormItem
validateStatus={passwordError ? 'error' : ''}
help={passwordError || ''}
>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please input your Password!' }],
})(
<Input prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="Password" />
)}
</FormItem>
<FormItem>
<Button
type="primary"
htmlType="submit"
disabled={hasErrors(getFieldsError())}
>
登陸
</Button>
</FormItem>
</Form>
);
}
}
複製代碼
上面的代碼咋一看沒什麼毛病, 給每一個字段綁定一個 validateStatus
去看當前字段 有沒有觸碰過 而且沒有錯, 並在 組件渲染的時候 觸發一次驗證, 經過這種方式 來達到 disabled
按鈕的目的, 可是要命的 只是 實現一個 disabled
的效果, 多寫了這麼多的代碼, 實際遇到的場景是 有10多個這種需求的表單,有沒有什麼辦法不寫這麼多的模板代碼呢? 因而我想到了 高階組件
api
因爲 Form.create()
後 會給 this.props
添加 form
屬性 ,從而使用它提供的 api, 通過觀察 咱們預期想要的效果有如下幾點bash
// 使用效果
@autoBindForm //須要實現的組件
export default class FormPage extends React.PureComponent {
}
複製代碼
要達到以下效果antd
componentDidMount
的時候 觸發一次 字段驗證this.props.hasError
相似的字段給當前組件.控制 按鈕的 disabled
狀態import * as React from 'react'
import { Form } from 'antd'
const getDisplayName = (component: React.ComponentClass) => {
return component.displayName || component.name || 'Component'
}
export default (WrappedComponent: React.ComponentClass<any>) => {
class AutoBindForm extends WrappedComponent {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
autoBindFormHelp: React.Component<{}, {}> = null
getFormRef = (formRef: React.Component) => {
this.autoBindFormHelp = formRef
}
render() {
return (
<WrappedComponent wrappedComponentRef={this.getFormRef} /> ) } return Form.create()(AutoBindForm) } 複製代碼
首先 Form.create
一下咱們須要包裹的組件, 這樣就不用每個頁面都要 create
一次app
而後咱們經過 antd
提供的 wrappedComponentRef
拿到了 form
的引用函數
根據 antd
的文檔 ,咱們要實現想要的效果,須要用到 以下 api優化
validateFields
驗證字段getFieldsValue
獲取字段的值setFields
設置字段的值getFieldsError
獲取字段的錯誤信息isFieldTouched
獲取字段是否觸碰過class AutoBindForm extends WrappedComponent
複製代碼
繼承咱們須要包裹的組件(也就是所謂的反向繼承), 咱們能夠 在初始化的時候 驗證字段ui
componentDidMount(){
const {
form: {
validateFields,
getFieldsValue,
setFields,
},
} = this.props
validateFields()
}
}
複製代碼
因爲進入頁面時 用戶並無輸入, 因此須要手動清空 錯誤信息
componentDidMount() {
const {
form: {
validateFields,
getFieldsValue,
setFields,
},
} = this.props
validateFields()
Object.keys(getFieldsValue())
.forEach((field) => {
setFields({
[field]: {
errors: null,
status: null,
},
})
})
}
}
複製代碼
經過 getFieldsValue()
咱們能夠動態的拿到當前 表單 全部的字段, 而後再使用 setFields
遍歷一下 把全部字段的 錯誤狀態設爲 null
, 這樣咱們就實現了 1,2 的效果,
因爲子組件 須要一個 狀態 來知道 當前的表單是否有錯誤, 因此咱們定義一個 hasError
的值 來實現, 因爲要是實時的,因此不難想到用 getter
來實現,
熟悉Vue
的同窗 可能會想到 Object.definedPropty
實現的 計算屬性,
本質上 Antd
提供的 表單字段收集也是經過 setState
, 回觸發頁面渲染, 在當前場景下, 直接使用 es6
支持的get
屬性便可實現一樣的效果 代碼以下
get hasError() {
const {
form: { getFieldsError, isFieldTouched }
} = this.props
let fieldsError = getFieldsError() as any
return Object
.keys(fieldsError)
.some((field) => !isFieldTouched(field) || fieldsError[field]))
}
複製代碼
代碼很簡單 ,在每次 getter
觸發的時候, 咱們用 some
函數 去判斷一下 當前的表單是否觸碰過 或者有錯誤, 在建立表單這個場景下, 若是沒有觸碰過,必定是沒輸入,因此沒必要驗證是否有錯
最後 在 render
的時候 將 hasError
傳給 子組件
render() {
return (
<WrappedComponent wrappedComponentRef={this.getFormRef} {...this.props} hasError={this.hasError} /> ) } //父組件 console.log(this.prop.hasError) <Button disabled={this.props.hasError}>提交</Button> 複製代碼
同時咱們定義下 type
export interface IAutoBindFormHelpProps {
hasError: boolean,
}
複製代碼
寫到這裏, 建立表單的場景, 基本上能夠用這個高階組件輕鬆搞定, 可是有一些表單有一些非必填項, 這時就會出現,非必填項可是認爲有錯誤的清空, 接下來, 改進一下代碼
非必填字段, 即認爲是一個配置項, 由調用者告訴我哪些是 非必填項, 當時我原本想搞成 自動去查找 當前組件哪些字段不是 requried
的, 可是 antd
的文檔貌似 莫得, 就放棄了
首先修改函數, 增長一層柯里化
export default (filterFields: string[] = []) =>
(WrappedComponent: React.ComponentClass<any>) => {
}
複製代碼
@autoBindForm(['fieldA','fieldB']) //須要實現的組件
export default class FormPage extends React.PureComponent {
}
複製代碼
修改 hasError
的邏輯
get hasError() {
const {
form: { getFieldsError, isFieldTouched, getFieldValue },
defaultFieldsValue,
} = this.props
const { filterFields } = this.state
const isEdit = !!defaultFieldsValue
let fieldsError = getFieldsError()
const needOmitFields = filterFields.filter((field) => !isFieldTouched(field)).concat(needIgnoreFields)
if(!isEmpty(needOmitFields)) {
fieldsError = omit(fieldsError, needOmitFields)
}
return Object
.keys(fieldsError)
.some((field) => {
const isCheckFieldTouched = !isEdit || isEmpty(getFieldValue(field))
return isCheckFieldTouched ? (!isFieldTouched(field) || fieldsError[field]) : fieldsError[field]
})
}
複製代碼
邏輯很簡單粗暴, 遍歷一下須要過濾的字段,看它有沒有觸碰過,若是觸碰過,就不加入錯誤驗證
同理, 在 初始化的時候也過濾一下,
首先經過 Object.keys(getFieldsValue)
拿到當前表單 的全部字段, 因爲 這時候不知道哪些字段 是 requierd
的, 機智的我
validateFields
驗證一下當前表單, 這個函數 返回當前表單的錯誤值, 非必填的字段 此時不會有錯誤, 因此 只須要拿到當前錯誤信息, 和 全部字段 比較 二者 不一樣的值, 使用 loadsh
的 xor
函數 完成
const filterFields = xor(fields, Object.keys(err || []))
this.setState({
filterFields,
})
複製代碼
最後清空 全部錯誤信息
完整代碼:
componentDidMount() {
const {
form: {
validateFields,
getFieldsValue,
getFieldValue,
setFields,
},
} = this.props
const fields = Object.keys(getFieldsValue())
validateFields((err: object) => {
const filterFields = xor(fields, Object.keys(err || []))
this.setState({
filterFields,
})
const allFields: { [key: string]: any } = {}
fields
.filter((field) => !filterFields.includes(field))
.forEach((field) => {
allFields[field] = {
value: getFieldValue(field),
errors: null,
status: null,
}
})
setFields(allFields)
})
}
複製代碼
通過這樣一波修改, 支持非必填字段的需求就算完成了
其實這個很簡單, 就是看子組件是否有默認值 , 若是有 setFieldsValue
一下就搞定了, 子組件和父組件約定一個 defaultFieldsValue
完整代碼以下
import * as React from 'react'
import { Form } from 'antd'
import { xor, isEmpty, omit } from 'lodash'
const getDisplayName = (component: React.ComponentClass) => {
return component.displayName || component.name || 'Component'
}
export interface IAutoBindFormHelpProps {
hasError: boolean,
}
interface IAutoBindFormHelpState {
filterFields: string[]
}
/** * @name AutoBindForm * @param needIgnoreFields string[] 須要忽略驗證的字段 * @param {WrappedComponent.defaultFieldsValue} object 表單初始值 */
const autoBindForm = (needIgnoreFields: string[] = [] ) => (WrappedComponent: React.ComponentClass<any>) => {
class AutoBindForm extends WrappedComponent {
get hasError() {
const {
form: { getFieldsError, isFieldTouched, getFieldValue },
defaultFieldsValue,
} = this.props
const { filterFields } = this.state
const isEdit = !!defaultFieldsValue
let fieldsError = getFieldsError()
const needOmitFields = filterFields.filter((field) => !isFieldTouched(field)).concat(needIgnoreFields)
if(!isEmpty(needOmitFields)) {
fieldsError = omit(fieldsError, needOmitFields)
}
return Object
.keys(fieldsError)
.some((field) => {
const isCheckFieldTouched = !isEdit || isEmpty(getFieldValue(field))
return isCheckFieldTouched ? (!isFieldTouched(field) || fieldsError[field]) : fieldsError[field]
})
}
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
state: IAutoBindFormHelpState = {
filterFields: [],
}
autoBindFormHelp: React.Component<{}, {}> = null
getFormRef = (formRef: React.Component) => {
this.autoBindFormHelp = formRef
}
render() {
return (
<WrappedComponent wrappedComponentRef={this.getFormRef} {...this.props} hasError={this.hasError} /> ) } componentDidMount() { const { form: { validateFields, getFieldsValue, getFieldValue, setFields, }, } = this.props const fields = Object.keys(getFieldsValue()) validateFields((err: object) => { const filterFields = xor(fields, Object.keys(err || [])) this.setState({ filterFields, }) const allFields: { [key: string]: any } = {} fields .filter((field) => !filterFields.includes(field)) .forEach((field) => { allFields[field] = { value: getFieldValue(field), errors: null, status: null, } }) setFields(allFields) // 因爲繼承了 WrappedComponent 因此能夠拿到 WrappedComponent 的 props if (this.props.defaultFieldsValue) { this.props.form.setFieldsValue(this.props.defaultFieldsValue) } }) } } return Form.create()(AutoBindForm) } export default autoBindForm 複製代碼
這樣一來, 若是子組件 有 defaultFieldsValue
這個 props, 頁面加載完就會設置好這些值,而且不會觸發錯誤
import autoBindForm from './autoBindForm'
# 基本使用
@autoBindForm()
class MyFormPage extends React.PureComponent {
...沒有靈魂的表單代碼
}
# 忽略字段
@autoBindForm(['filedsA','fieldsB'])
class MyFormPage extends React.PureComponent {
...沒有靈魂的表單代碼
}
# 默認值
// MyFormPage.js
@autoBindForm()
class MyFormPage extends React.PureComponent {
...沒有靈魂的表單代碼
}
// xx.js
const defaultFieldsValue = {
name: 'xx',
age: 'xx',
rangePicker: [moment(),moment()]
}
<MyformPage defaultFieldsValue={defaultFieldsValue} />
複製代碼
這裏須要注意的是, 若是使用 autoBindForm
包裝過的組件 也就是
<MyformPage defaultFieldsValue={defaultFieldsValue}/>
複製代碼
這時候 想拿到 ref
, 不要忘了 forwardRef
this.ref = React.createRef()
<MyformPage defaultFieldsValue={defaultFieldsValue} ref={this.ref}/>
複製代碼
同理修改 'autoBindForm.js'
render() {
const { forwardedRef, props } = this.props
return (
<WrappedComponent
wrappedComponentRef={this.getFormRef}
{...props}
hasError={this.hasError}
ref={forwardedRef}
/>
)
}
return Form.create()(
React.forwardRef((props, ref) => <AutoBindForm {...props} forwardedRef={ref} />),
)
複製代碼
import * as React from 'react'
import { Form } from 'antd'
import { xor, isEmpty, omit } from 'lodash'
const getDisplayName = (component: React.ComponentClass) => {
return component.displayName || component.name || 'Component'
}
export interface IAutoBindFormHelpProps {
hasError: boolean,
}
interface IAutoBindFormHelpState {
filterFields: string[]
}
/** * @name AutoBindForm * @param needIgnoreFields string[] 須要忽略驗證的字段 * @param {WrappedComponent.defaultFieldsValue} object 表單初始值 */
const autoBindForm = (needIgnoreFields: string[] = []) => (WrappedComponent: React.ComponentClass<any>) => {
class AutoBindForm extends WrappedComponent {
get hasError() {
const {
form: { getFieldsError, isFieldTouched, getFieldValue },
defaultFieldsValue,
} = this.props
const { filterFields } = this.state
const isEdit = !!defaultFieldsValue
let fieldsError = getFieldsError()
const needOmitFields = filterFields.filter((field) => !isFieldTouched(field)).concat(needIgnoreFields)
if (!isEmpty(needOmitFields)) {
fieldsError = omit(fieldsError, needOmitFields)
}
return Object
.keys(fieldsError)
.some((field) => {
const isCheckFieldTouched = !isEdit || isEmpty(getFieldValue(field))
return isCheckFieldTouched ? (!isFieldTouched(field) || fieldsError[field]) : fieldsError[field]
})
}
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
state: IAutoBindFormHelpState = {
filterFields: [],
}
autoBindFormHelp: React.Component<{}, {}> = null
getFormRef = (formRef: React.Component) => {
this.autoBindFormHelp = formRef
}
render() {
const { forwardedRef, props } = this.props
return (
<WrappedComponent
wrappedComponentRef={this.getFormRef}
{...props}
hasError={this.hasError}
ref={forwardedRef}
/>
)
}
componentDidMount() {
const {
form: {
validateFields,
getFieldsValue,
getFieldValue,
setFields,
},
} = this.props
const fields = Object.keys(getFieldsValue())
validateFields((err: object) => {
const filterFields = xor(fields, Object.keys(err || []))
this.setState({
filterFields,
})
const allFields: { [key: string]: any } = {}
fields
.filter((field) => !filterFields.includes(field))
.forEach((field) => {
allFields[field] = {
value: getFieldValue(field),
errors: null,
status: null,
}
})
setFields(allFields)
// 屬性劫持 初始化默認值
if (this.props.defaultFieldsValue) {
this.props.form.setFieldsValue(this.props.defaultFieldsValue)
}
})
}
}
return Form.create()(
React.forwardRef((props, ref) => <AutoBindForm {...props} forwardedRef={ref} />),
)
}
export default autoBindForm
複製代碼
這樣一個 對 Form.create
再次包裝的 高階組件, 解決了必定的痛點, 少寫了不少模板代碼, 雖然封裝的時候遇到了各類各樣奇奇怪怪的問題,可是都解決了, 沒毛病, 也增強了我對高階組件的認知,溜了溜了 :)