React(Hook)- 表单验证组件封装
- 造个轮着(表单验证),实现方式属个人思想,非最佳实践,欢迎指点
- 验证插件使用:async-validator
完整代码
代码已打包上传NPM
yarn add rh-hook-form
期望的调用方式
Form组件
目的
- 表单样式特征可控
- 暴漏一个验证整个表单的方法
- 返回整个当前数据
确认一下参数
function XtForm({ labelPosition, className, reft, children })
样式
- 接受className
- labelPosition label位置控制
使用useMemo优化,在labelPosition, className不变的情况下classNameMerge不从新计算,其中top/left/right是类名
const classNameMerge = useMemo(() => {
let classNameMerge;
switch (labelPosition) {
case 'top':
classNameMerge = `${xtForm} ${top} ${className || ''}`;
break;
case 'left':
classNameMerge = `${xtForm} ${left} ${className || ''}`;
break;
case 'right':
classNameMerge = `${xtForm} ${right} ${className || ''}`;
break;
}
return classNameMerge;
}, [labelPosition, className]);
整个表单验证
- 确认一下思路,为了使每个表单项验证灵活,独立,此处我们将校验放在FormItem上,即表单项各通过async-validator初始化各自的验证实例。
- 而作为Form通过事件监听的方式,将formItem各自的验证方法收集起来,重而实现验证整个表单的方法
- 关于整个表单的值(formData),按照验证的思路,表单项自己管理自己的验证与表单值,而Form需要提供一个实时的整个表单(formData),我依然使用事件通知的形式,在FormItem值改变后,发送给Form来收集
下面是Form的事件监听
- 需要注意的是 eventKey 可以看作一个不重复的随即值。他的意图是为每个表单创建一个独立的事件监听池(为了一个页面上同时创建多个表单,而公用一个事件池,导致通信错乱)
- 我们用一个事件监听处理 验证方法和表单项值的接收
function addEventListen() {
const formData = {}; //表单对象(实时的值)
const formCheckList = []; //表单收集各表单项的验证方法
const eventKey = new Date().getTime() + Math.floor(Math.random() * 100); // 随机不重复key
event.on('addVla' + eventKey, ({ type, prop, value, validate }) => {
switch (type) {
case 'INIT':
formCheckList.push({ validate, prop });
formData[prop] = value;
break;
case 'VALUE':
formData[prop] = value;
break;
}
});
return {
formCheckList,
eventKey,
formData,
};
};
关于eventKey的传递
{React.Children.map(children, item => {
return item;
})}
- 需要将eventKey传递给FormItem,FormItem才能emit发送通知给对应事件监听
- 由于在Form中通过遍历Children拿到嵌套的子组件,这种形式导致我们无法通过以下形式传递eventKey
- 因此我们采用context的形式传递
关于event
- 此处我们自己实现一个简单的满足业务的event.js
- 相信各位大佬关于实现逻辑都手到擒来,我们只附上代码
class MyEvent {
constructor() {
this.list = {};
}
on(type, fn) {
this.list[type] ? this.list[type].push(fn) : this.list[type] = [fn];
};
emit(type, data) {
if (this.list[type] && this.list[type].length > 0) {
this.list[type].forEach(item => {
item(data);
});
}
};
remove(type, fn) {
if (fn) {
if (this.list[type] && this.list[type].length > 0) {
for (let i = 0; i < this.list[type].length; i++) {
if (this.list[type][i] === fn) {
this.list[type].splice(i, 1);
break;
}
}
}
} else {
this.list[type] = [];
}
}
}
export default MyEvent;
Form Ref对外暴漏方法
- useImperativeHandle 将需要的方法暴漏出去,毕竟裸奔不太好,哈哈
useImperativeHandle(ref, () => ({
getFormData: (format) => {
return format ? formatData(format, formData) : formData;
},
validatorForm: (cb, format) => {
format = JSON.parse(JSON.stringify(format));
const length = formCheckList.length;
let count = 0;
let checkResult = true;
formCheckList.forEach((obj) => {
obj.validate(formData[obj.prop], (err) => {
if (err && checkResult) checkResult = false;
count++;
if (count >= length) {
cb(checkResult, format ? formatData(format, formData) : formData);
}
});
});
},
}));
FormItem
目的
- FormItem的样式(包含错误样式)可控
- 自动创建label,必填标示
- 用户调用后无需手动onchange
- 添加change后验证输入结果,并提示错误信息
- 将自己的表单验证方法上传给Form
- 将自己的表单值实时同步给Form
FormItem - prop
- prop 在整个表单中的 值对应的位置路径
- children 嵌套的子组件
- rules 验证规则
- isRequire 必填标示
- label labelName
- defaultValue 默认值
- className 样式
- contentClass 内部表单区域样式
- errClass 错误提示样式
- style 用户内联样式
function FormItem({ prop, children, rules, isRequire, label, defaultValue, className, contentClass, errClass, style })
初始化验证实例
- validator只需在 rules, prop 改变后再重新new,所以这里使用useMemo优化
const validator = useMemo(() => {
console.log('initMemo', prop);
return new Schema({ [prop]: rules });
}, [rules, prop]);
将当前的表单验证方法传递给Form
- 创建一个表单验证方法
function checkFormItem(prop, validator, setMessage) {
return function(value, call) {
validator.validate({ [prop]: value }, { first: true }).then(() => {
setMessage({
value: value,
message: '',
});
call && call();
}).catch(({ errors }) => {
setMessage({
value: value,
message: errors[0].message,
});
call && call(errors);
});
};
}
- 传送 推送验证方法和实时值用的同一个事件监听,所以通过type = INIT区分
useEffect(() => {
console.log('initEvent', prop);
event.emit('addVla' + eventKey, {
validate: checkFormItem(prop, validator, setFormItemInfo),
type: 'INIT',
prop,
value,
});
}, [eventKey, prop]);
注入onChange事件
- 通过 React.cloneElement 为用户的表单组件注入onChange事件
- 由于FormItem中用户可写多个兄弟组件,其中可能包含非表单组件,所以需要用户在自己的表单组件上 添加fromctr属性告诉FormItem这是一个表单件
- 用户的自己的组件上仍然可以写onChange事件,我们通过child.props拿到,然后在我们注入的onChange中执行他即可
function createFormItem(child, eventKey, value, message, validator, prop, setFormItemInfo) {
const { fromctr, preChange, className } = child.props;
if (!fromctr) return child; //是否是表单件
return React.cloneElement(child, {
value,
className: message ? className + ' validateErr' : className,
onChange: (event) => {
event.persist && event.persist();
const data = event.target.value;
preChange && preChange(data);
emitValue(eventKey, prop, data);
// 验证表单
validator.validate({ [prop]: data }).then(() => {
setFormItemInfo({
value: data,
message: '',
});
}).catch(({ errors }) => {
setFormItemInfo({
value: data,
message: errors[0].message,
});
});
},
});
}
- emitValue 推送验证方法和实时值用的同一个事件监听,所以通过type = VALUE区分
function emitValue(eventKey, prop, value) {
event.emit('addVla' + eventKey, {
type: 'VALUE',
prop,
value,
});
}
特殊说明
- FormItem -> prop 表单值属性所在位置路径
let formData = {
a: {
b: {
c: [{name: ''}]
}
}
}
如上则
- 这样做的目的是,当表单项由数组遍生成,当值修改为name值为1时,你的值保存在Form中是这样的
formData: {
a.b.c.1.name: 1
}
通过ref.validatorForm(cb, format) 其中format为格式模版,如下
{
a: {
b: {
c: [{name: ''}]
}
}
}
通过prop 可以找到对应位置,放入值
- 通过format找对应的位置的方法如下,如果看过element-ui源码的同学,会在Form/utils中找到这个方法(作者抄袭,求别举报)
function getPropByPath(obj, path, strict) {
path = path.toString();
let tempObj = obj;
path = path.replace(/\[(\w+)\]/g, '.$1');
path = path.replace(/^\./, '');
const keyArr = path.split('.');
let i = 0;
for (let len = keyArr.length; i < len - 1; ++i) {
if (!tempObj && !strict) break;
const key = keyArr[i];
if (key in tempObj) {
tempObj = tempObj[key];
} else {
if (strict) {
throw new Error('please transfer a valid prop path to form item!');
}
break;
}
}
return {
o: tempObj,
k: keyArr[i],
v: tempObj ? tempObj[keyArr[i]] : null,
};
}