前言
本文講的如何利用context,將多個元件串聯起來,實現一個更大的聯合元件。最具有這個特性的就是表單元件,所以本文例子就是一個表單元件。本文例子參考 Ant Design 。本次不講 context 知識,需要的話等到下一次分享。
準備
- es6 基本知識。參考地址
- react 基本知識。參考地址
- create-react-app 腳手架。 參考地址
- react context 知識。 參考地址
- react prop-types 相關知識。 參考地址
或者直接使用本文 demo Gitee地址
基本程式碼
<Form onSubmit={(e, v) => {
console.log(e, `error`);
console.log(v, `value`);
}}>
<Form.Item label={`手機號`}>
<Form.Input name={`phone`} rules={[{validator: (e) => /^1[3-9]d+$/.test(e), message: `手機號格式錯誤`}]}/>
</Form.Item>
<Form.Item label={`年齡`}>
<Form.Input name={`age`} rules={[{validator: (e) => /^d+$/.test(e), message: `只允許輸入數字`}]}/>
</Form.Item>
<Form.Button>提交</Form.Button>
<Form.Button type={`reset`}>重置</Form.Button>
</Form>
需求
- 自定義校驗規則
- 表單內容元件不限組合方式
- 點選提交按鈕就可以提交
- 提交時候可以校驗值並且可以自動攔截,然後將錯誤資訊下發給 FormItem 元件並且顯示出來
- 通過傳入 Form 元件的 onSubmit 引數就可以獲取到內容
實現
明白自己所需要的內容後,我們建立基本程式碼中的幾個元件,Form , FormItem ,Input , 以及 Button。
具體內容看程式碼中的註釋
Form
首先我們要知道 Form 元件在聯合元件中的負責的內容
- 資料收集
- 資料校驗
- 提交、重置動作處理
程式碼如下
import React, {Component} from `React`;
import PropTypes from `prop-types`;
import {Item} from `./Item`;
import {Button} from `./Button`;
import {Input} from `./Input`;
export class Form extends Component{
static propTypes = {
onSubmit: PropTypes.func.isRequired, // 需要該引數因為,如果沒有該引數,整個元件就沒有意義
defaultValues: PropTypes.object, // 如果有些需要預設引數的,就需要該引數
children: PropTypes.any,
};
static defaultProps = {
defaultValues: {},
};
static childContextTypes = {
form: PropTypes.any, // 定義上下文引數名稱和格式,格式太麻煩,直接any了或者 object也可以。
};
state = {
validates: {},
change: 0,
};
// 為什麼不將資料全部放在 state 裡面,在本文最後會講到
registerState = {
form: {},
rules: {},
label: {},
};
getChildContext() {
// 定義上下文返回內容
const {validates} = this.state;
const {form} = this.registerState;
return {
form: {
submit: this.submit.bind(this),
reset: this.reset.bind(this),
register: this.register.bind(this),
registerLabel: this.registerLabel.bind(this),
setFieldValue: this.setFieldValue.bind(this),
data: form,
validates,
},
};
}
submit() {
// 提交動作
const {onSubmit} = this.props;
if (onSubmit) {
const validates = [];
const {form, rules, label} = this.registerState;
Object.keys(form).forEach(key => {
const item = form[key];
const itemRules = rules[key];
itemRules.forEach(rule => {
//To do something validator 簡單列出幾種基本校驗方法,可自行新增
let res = true;
// 如果校驗規則裡面有基本規則時候,使用基本規則
if (rule.hasOwnProperty(`type`)) {
switch (rule) {
case `phone`:
/^1[3-9]d+$/.test(item);
res = false;
break;
default:
break;
}
}
// 如果校驗規則裡面有 校驗函式時候,使用它
if (rule.hasOwnProperty(`validator`)) {
res = rule.validator(item);
}
// 校驗不通過,向校驗結果陣列裡面增加,並且結束本次校驗
if (!res) {
validates.push({key, message: rule.message, label: label.hasOwnProperty(key) ? label[key] : ``});
return false;
}
});
});
if (validates.length > 0) {
// 在控制檯列印出來
validates.forEach(item => {
console.warn(`item: ${item.label ? item.label : item.key}; message: ${item.message}`);
});
// 將錯誤資訊返回到 state 並且由 context 向下文傳遞內容,例如 FormItem 收集到該資訊,就可以顯示出錯誤內容和樣式
this.setState({
validates,
});
}
// 最後觸發 onSubmit 引數,將錯誤資訊和資料返回
onSubmit(validates, this.registerState.form);
}
}
reset() {
// 重置表單內容
const {form} = this.registerState;
const {defaultValues} = this.props;
this.registerState.form = Object.keys(form).reduce((t, c) => {
t[c] = defaultValues.hasOwnProperty(c) ? defaultValues[c] : ``;
return t;
}, {});
// 因為值不在 state 中,需要重新整理一下state,完成值在 context 中的更新
this.change();
}
//更新某一個值
setFieldValue(name, value) {
this.registerState.form[name] = value;
this.change();
}
// 值和規則都不在state中,需要藉助次方法更新內容
change() {
this.setState({
change: this.state.change + 1,
});
}
// 註冊引數,最後資料收集和規則校驗都是通過該方法向裡面新增的內容完成
register(name, itemRules) {
if (this.registerFields.indexOf(name) === -1) {
this.registerFields.push(name);
const {defaultValues} = this.props;
this.registerState.form[name] = defaultValues.hasOwnProperty(name) ? defaultValues[name] : ``;
this.registerState.rules[name] = itemRules;
} else {
// 重複的話提示錯誤
console.warn(``${name}` has repeat`);
}
}
// 新增 欄位名稱,優化體驗
registerLabel(name, label) {
this.registerState.label[name] = label;
}
render() {
return (
<div className="form">
{this.props.children}
</div>
); // 這裡使用括號因為在 webStrom 下格式化程式碼後的格式看起來更舒服。
}
}
// 將子元件加入到 Form 中 表示關聯關係
Form.Item = Item;
Form.Button = Button;
Form.Input = Input;
FormItem
它的功能不多
- 向 Form 中註冊 輸入框的關聯名稱
- 從 Form 中獲取 校驗結果並且展示出來
程式碼如下
import React, {Component} from `react`;
import PropTypes from `prop-types`;
export class Item extends Component {
// 這個值在 FormItem 元件 被包裹在 Form 元件中時,必須有
name;
static propTypes = {
label: PropTypes.string,
};
static childContextTypes = {
formItem: PropTypes.any,
children: PropTypes.any,
};
static contextTypes = {
form: PropTypes.object,
};
// 防止重複覆蓋 name 的值
lock = false;
// 獲取到 包裹的輸入元件的 name值,如果在存在 Form 中,則向 Form 註冊name值相對的label值
setName(name) {
if (!this.lock) {
this.lock = true;
this.name = name;
const {form} = this.context;
if (form) {
form.registerLabel(name, this.props.label);
}
} else {
// 同樣,一個 FormItem 只允許操作一個值
console.warn(`Allows only once `setName``);
}
}
getChildContext() {
return {
formItem: {
setName: this.setName.bind(this),
},
};
}
render() {
const {label} = this.props;
const {form} = this.context;
let className = `form-item`;
let help = false;
if (form) {
const error = form.validates.find(err => err.key === this.name);
// 如果有找到屬於自己錯誤,就修改狀態
if (error) {
className += ` form-item-warning`;
help = error.message;
return false;
}
}
return (
<div className={className}>
<div className="label">
{label}
</div>
<div className="input">
{this.props.children}
</div>
{help ? (
<div className="help">
{help}
</div>
) : ``}
</div>
);
}
}
Input
暫時演示輸入元件為 Input ,後面可以按照該元件內容,繼續增加其他操作元件
該型別元件負責的東西很多
- 唯一name,通知 FormItem 它所包裹的是誰
- Form 元件裡面,收集的資料
- 校驗規則
程式碼如下
import React, {Component} from `react`;
import PropTypes from `prop-types`;
export class Input extends Component {
constructor(props, context) {
super(props);
// 如果在 Form 中,或者在 FormItem 中,name值為必填
if ((context.form || context.formItem) && !props.name) {
throw new Error(`You should set the `name` props`);
}
// 如果在 Form 中,不在 FormItem 中,提示一下,不在 FormItem 中不影響最後的值
if (context.form && !context.formItem) {
console.warn(`Maybe used `Input` in `FormItem` can be better`);
}
// 在 FormItem 中,就要通知它自己是誰
if (context.formItem) {
context.formItem.setName(props.name);
}
// 在 Form 中,就向 Form 註冊自己的 name 和 校驗規則
if (context.form) {
context.form.register(props.name, props.rules);
}
}
shouldComponentUpdate(nextProps) {
const {form} = this.context;
const {name} = this.props;
// 當 有 onChange 事件 或者外部使用元件,強行更改了 Input 值,就需要通知 Form 更新值
if (form && this.changeLock && form.data[name] !== nextProps.value) {
form.setFieldValue(name, nextProps.value);
return false;
}
return true;
}
static propTypes = {
name: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
rules: PropTypes.arrayOf(PropTypes.shape({
type: PropTypes.oneOf([`phone`]),
validator: PropTypes.func,
message: PropTypes.string.isRequired,
})),
type: PropTypes.oneOf([`text`, `tel`, `number`, `color`, `date`]),
};
static defaultProps = {
value: ``,
rules: [],
};
static contextTypes = {
form: PropTypes.object,
formItem: PropTypes.object,
};
onChange(e) {
const val = e.currentTarget.value;
const {onChange, name} = this.props;
const {form} = this.context;
if (onChange) {
this.changeLock = true;
onChange(val);
} else {
if (form) {
form.setFieldValue(name, val);
}
}
}
render() {
let {value, name, type} = this.props;
const {form} = this.context;
if (form) {
value = form.data[name] || ``;
}
return (
<input onChange={this.onChange.bind(this)} type={type} value={value}/>
);
}
}
Button
負責內容很簡單
- 提交,觸發 submit
- 重置,觸發 reset
程式碼如下
import React, {Component} from `react`;
import PropTypes from `prop-types`;
export class Button extends Component {
componentWillMount() {
const {form} = this.context;
// 該元件只能用於 Form
if (!form) {
throw new Error(`You should used `FormButton` in the `Form``);
}
}
static propTypes = {
children: PropTypes.any,
type: PropTypes.oneOf([`submit`, `reset`]),
};
static defaultProps = {
type: `submit`,
};
static contextTypes = {
form: PropTypes.any,
};
onClick() {
const {form} = this.context;
const {type} = this.props;
if (type === `reset`) {
form.reset();
} else {
form.submit();
}
}
render() {
return (
<button onClick={this.onClick.bind(this)} className={`form-button`}>
{this.props.children}
</button>
);
}
}
後言
首先先講明為何 不將label 和資料不放在state 裡面因為多個元件同時註冊時候,state更新來不及,會導致部分值初始化不成功,所以最後將值收集在 另外的 object 裡面,並且是直接賦值
看了上面幾個元件的程式碼,應該有所明確,這些元件組合起來使用就是一個大的元件。同時又可以單獨使用,知道該如何使用後,又可以按照規則,更新整個各個元件,而不會說,一個巨大無比的單獨元件,無法拆分,累贅又複雜。通過聯合元件,可以達成很多奇妙的組合方式。上文的例子中,如果沒有 Form 元件, 單獨的 FormInput 加 Input,這兩個組合起來,也可以是一個單獨的驗證器。