前言
本文讲的如何利用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,这两个组合起来,也可以是一个单独的验证器。