React form 表单组件的解决方案

来源 | https://imweb.io/topic/5ca03c119239213a22d7549d

一直以来,表单对于前端来说都是一个不得不面对的坑。而对于设计一个表单组件来说,主要需要考虑以下三点:

  • 各个元素如何排版布局

  • 管理各个元素的值

  • 表单验证(即时校验及提交的全部校验)

目前已经有了一批优秀的 form 表单解决方案,但是要解决上述的三大问题,都比较费劲,于是搞了个 react-form-next ,力求将整个表单组件所涉及到的问题都简化点。

简单演示

以下图的一个简单的表单为例:

布局采用传统的一行一个表单项,验证条件如下:

  • 用户名默认为ycxu,不允许为空

  • 邮箱不能为空,邮箱格式验证

  • 年龄不能为空,只能是数字,且范围为18-30之间的数字。除此以外,onChange 的时候要立即校验。

  • 提交之前校验全部

体验地址为: React form demo。核心组件代码如下:

下面简单解释下各个组件的用途:

  • FormReducer 组件,使用最新的 react hook api 自动管理整个表单的数据。并且创建了一个 context。

  • FormReducerItemContext 组件,表示自动注入了 context(value, checkMsg, onChange) 的表单项组件。所以组件中没有value,checkMsg(校验信息),onChange 这三个属性。其中年龄中 changeAutoCheck 属性表示值改变的时候立即校验。

  • FormItem 组件,表示表单项组件,主要解决了各个元素如何排版布局的问题。

  • FormReducerSubmitContext 表示注入了提交之前先全部校验的逻辑。

下面我们一一分析具体的实现。

各个元素如何排版布局

首先,整个表单可以分为多个表单项。而一个表单项从结构上可能会涉及到 6 个部分:label、前缀、表单元素(或自定义的表单元素)、后缀、说明文字,校验态。大概如下图:

大体 HTML 结构可以设计如下:

div.f-item    label.f-label // 如果需要标注required,子元素还要加个required判断    div.f-field        div.f-element-wrap // 如果没有前后缀,可以不需要该层            span.f-prefix // 前缀            input.f-element // 输入框等表单元素            span.f-suffix // 后缀        p.f-description // 描述说明        div.f-msg // 检验信息

当然还有一些非常简单的情况,不需要进行验证,而提示信息也可以使用 placeholder 来搞定,所以是不需要这么多层结构的,于是结构可以简化如下:

div.f-item    label.f-label // 如果需要标注required,子元素还要加个required判断    span.f-prefix // 前缀    input.f-element // 输入框等表单元素    span.f-suffix // 后缀

对于自定义的元素,替代掉 input.f-element 的位置就好。这样我们就可以设计出一个 FormItem 组件。该组件有大概如下几个属性:

  • simple:是否使用简化版的 HTML,默认为 false

  • className:自定义 class

  • name:表单项名称,用于表单元素的 name 属性,和 label 的 for 属性

  • label: 标签内容

  • isRequired:是否必须

  • prefix:前缀内容

  • type:单个输入框类型(email,areatext,password...),默认为 text

  • placeholder

  • suffix:后缀内容

  • des:描述说明

  • checkMsg:检验信息

  • value:值

  • onChange:值改变事件

除此之外,我们还可以将剩余属性全部透传给表单元素,如设置 focus 、 blur 事件或设置 disabled 禁用等。

由于表单元素的复杂性,所以组件封装默认只处理一些 type 输入框类型的。对于非输入框类型的表单元素,统一使用 children 的形式来。

如下几种使用:

<FormItem name="username" label="用户名" value={} onChange={} /><FormItem name="id" label="证件类型" />    {/* 选择框,单选多选,多个元素,自定义组件等使用 children 形式 */}    <select value={} onChange={}></select>    <input type="text" value="" onChange={}></FormItem><FormItem name="comnent" type="areatext" label="申诉" value={} onChange={} />

这样 HTML 结构的基本架子已经搭建好了,现在需要考虑一些排版上的问题。

排版布局

对于第一种完善的 HTML 结构,由于标签留得比较足够,对于排版布局的变化只需要通过 class 去控制即可。而对于极简模式下的 HTML 结构,由于标签没有多余,所以在排版布局方面的变化没有那么灵活,不过既然是极简模式,想必也没有那么复杂。

标签与表单元素同行

这种情况属于多数情况,所以我们作为默认的效果。上面的效果图就是这种。

多个表单项同行显示

如下这种多个表单项同行显示的情况也是比较常见的,所以可以通过新增一个属性 inline 来控制,默认为false,设置为 true即启用该效果。效果图如下:

标签单行显示

同样对于一些 label 标签需要单行显示的。也需要多加一个 vertical 属性来控制,默认为 false, 设置为 true 即启用该效果。效果图如下:

PS:由于该效果与上面的多个表单项同行显示属于可以共存的,所以需要两个属性来单独控制。

校验信息

同样默认的话,检验信息是放在表单元素的右侧,但是有些情况需要在表单元素的下方显示,所以新增一个属性 checkMsgShowBelow来控制,同样也是默认为 false,设置为 true 即启用该效果,如下图:

除此之外,还有一个特例情况,它既不显示在表单元素的右边也不是下面。而是在其他地方进行提示。这时候就需要隐藏掉检验信息了,所以同样新增一个属性 checkMsgHide 来控制,如下图元素框显示错误态,但是提示信息放到其他地方显示:

最后得到 FormItem 的属性如下:

具体使用可查看 FormItem 组件 demo

表单验证

对于一个表单项 FormItem 组件来说,验证一般会涉及到三个属性valueonChangecheckMsg。如果一个表单中只有多个表单项,每个都会写一遍,实在是有点不怎么好看。

为了表现更优美点,所以设计这三个通用的检验属性由 Form 组件传入,然后通过 context 来绑定到 FormItem 组件,当然这样也方便后面的统一检验逻辑的处理等。

context

1、 创建 context

const FormContext = React.createContext({  values: {},  checkMsg: {},  onChange: () => {},});

2、创建 Form 组件,使用 Provider。主要代码为:

  const {    children, values, checkMsg, onChange, ...rest  } = props;
  // 将传入values,checkMsg,onChange 设置为 context 值  const value = {    values,    checkMsg,    onChange,  };
  return (    <FormContext.Provider value={value} >      <form {...rest} >        {children}      </form>    </FormContext.Provider>  );

3、创建高阶组件 FormItemContext,使用 useContext,通过 name 属性,得到相应的各个表单项对应的valuecheckMsg。透传其他属性。

onChange 事件统一管理,默认将带有三个参数:name、value、event 对象

export function withFormContext(Component) {  return props => {    const { name } = props;
    const { values, checkMsg, onChange } = useContext(FormContext);
    return (      <Component        value={values[name]}        checkMsg={checkMsg[name]}        onChange={onChange}        {...props}      />    );  };}
const FormItemContext = withFormContext(FormItem);
export default FormItemContext;

最后使用如下,手动管理各个表单项的值,通过 onChange 去更新:

// statethis.state = {    values: {        username: 'xxx',        email: 'xxx',    },    formErr: {},}
// change 事件handleChange = (name, value, e) => {    // 用户名验证    if (name === 'username') {}    // 邮箱验证    if (name === 'email') {}}
// 其他事件,如 blur 事件,因为是透传的,所以没有任何参数提供handleBlur = () => {}
<Form values={this.state.values} checkMsg={this.state.formErr} onChange={this.handleChange}>    <FormItemContext label="用户名" name="username" onBLur={this.handleBlur} />    <FormItemContext label="邮箱" type="email" name="email" /></Form>

自动管理数据

到目前位置,其实整个解决方案的思路就很清晰了。但是要手动去维护数据实在是太繁琐了。于是该拿出 hook api 来搞定了。

1、创建 reducer

export const initialState = {  values: {}, // 值数据  checkMsg: {}, // 校验信息数据};
export function formState(state, action) {  // action.type 的值为 values 或 checkMsg  const { type, name, value, data } = action;
  // 提交前的全部校验,checkMsg 全部更新  if (data) {    return Object.assign({}, state, {      [type]: data,    });  }
  // 单个values或checkMsg更新  Object.assign(state[type], {    [name]: value,  });  return { ...state };}

2、创建 FormReducer 组件

由于使用了自动管理状态,所以不需要传入 values 和 checkMsg 属性了,但是表单项的默认值还得通过另一个属性 defaultValues 传入,除此以外,由于 checkMsg 也除掉了,所以我们把校验规则通过另一个属性(formModel)了(具体校验方法见下面校验设计部分)。而在 context 方面,由于以后要处理自动管理值,所以添加了另外两个值 dispatch(用于更新数据) 和 formModel (用于校验),核心代码如下:

// 对比之前的 values,checkMsg,onChange 三大属性,// 这里改成了 defaultValues, formModel, onChange 三大属性const { children, onChange, formModel, defaultValues, ...rest } = props;
  // 当该表单项的值未定义时才使用默认值  if (defaultValues) {    Object.keys(defaultValues).forEach(item => {      if (initialState.values[item] === undefined) {        initialState.values[item] = defaultValues[item];      }    });  }
  const [state, dispatch] = useReducer(formState, initialState);  const { values, checkMsg } = state;
  const contextValue = {    values,    checkMsg,    formModel,    dispatch,    onChange,  };
  return (    <FormContext.Provider value={contextValue}>      <form {...rest}>        {children}      </form>    </FormContext.Provider>  );

3、创建 FormReducerItemContext 组件自动管理表单项数据,对比 FormItemContext 主要提供了自动更新值的功能及 onChange是否立即校验(具体校验可参考下面的校验设计部分)等。核心代码如下:

const changeHandler = (itemName, value, obj) => {      let returnValue;      if (onChange) {        returnValue = onChange(name, value, obj);      }
      // 如果没有返回值则更新数据      if (returnValue === undefined) {        dispatch({ type: 'values', name, value });      } else if (returnValue !== 'noDispatch') {        // 如果返回数据,且返回值不为noDispatch,则更新为返回值        value = returnValue;        dispatch({ type: 'values', name, value });      }
      // 立即校验      if (changeAutoCheck) {        dispatch({ type: 'checkMsg', name, value: formModel.checkForField(name, value) });      }    };
    // 只有值和校验信息改变的时候才更新    return useMemo(() => {      return <Component value={values[name]} checkMsg={checkMsg[name]} onChange={changeHandler} {...formItemRest} />;    }, [values[name], checkMsg[name]]);

4、创建FormReducerSubmitContext 组件自动实现提交之前先验证所有的规则。

// 提交之前先自动校验      const submitHandler = () => {        // 得到所有的校验数据        const data = formModel.check(values);        dispatch({ type: 'checkMsg', data });
        const hasError = Object.keys(data).find(item => {          return data[item].hasError;        });
        if (hasError === undefined && onClick) {          onClick(values);        }      };

校验设计

检验将使用 schema-typed 这个数据建模及数据验证工具。使用大概如下图,先创建一个 SchemaModel,然后使用该 model 去校验对应的数据的,返回的结果就是校验是否通过的数据信息:

整个校验设计非常赞,简直是眼前一亮,具体可以查看文档。

总结

  • FormItem 组件单独使用 demo:主要解决了表单项中各元素的排版布局问题。

  • Form demo:主要将 values,checkMsg,onChange 三大属性统一集中在 Form 组件中管理,并设计了一个高阶组件 FormItemContext,简化了属性的传递。

  • FormReducer demo:在 Form 的基础上,主要解决了自动管理数据问题。

最后奉上 NPM:react-form-next

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值