12

React(Hook)- 表单验证组件封装

  • 造个轮着(表单验证),实现方式属个人思想,非最佳实践,欢迎指点
  • 验证插件使用:async-validator
完整代码

代码已打包上传NPM

yarn add rh-hook-form
期望的调用方式
 <Form ref={formRef} labelPosition="left">
     <FormItem label="活动名称" isRequire prop="name" rules={rules.name} defaultValue={form.name}>
       <input fromctr="true" className='xt-input' type="text" />
     </FormItem>
 </Form>

Form组件

目的

  1. 表单样式特征可控
  2. 暴漏一个验证整个表单的方法
  3. 返回整个当前数据

确认一下参数

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的传递
 <FromContext.Provider value={{ eventKey }}>
      <div className={classNameMerge}>
        {React.Children.map(children, item => {
          return item;
        })}
      </div>
 </FromContext.Provider>
  • 需要将eventKey传递给FormItem,FormItem才能emit发送通知给对应事件监听
  • 由于在Form中通过遍历Children拿到嵌套的子组件,这种形式导致我们无法通过以下形式传递eventKey
<item eventKey={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: ''}]
       }
    }
}

如上则
<FormItem prop='a.b.c.1.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,
  };
}

D_Q_
483 声望12 粉丝

前端萌萌哒