Form 表单组件的设计之路

栏目: JavaScript · 发布时间: 7年前

内容简介:前端的Form 表单主要用于解决数据获取、数据校验、数据赋值 这三大类问题。这篇文章里面的提供的解决方案能够比较完美的用在 React 框架上,但是解决问题的思路相信应该是可以使用于任何框架语言。中后台的表单组件已经不仅仅有 input 和 select,可能还扩展到 范围选择器、日期选择器 等,这些组件往往为了实现更优雅的UI和更使用的交互会在原生的组件上面做多层封装,而经过多层叠加后可能已经看不到原生表单元素的影子了。比如经过封装下面这段 DOM 结构经过样式修改也可能成为一个输入组件,虽然完全看不到

前端的Form 表单主要用于解决数据获取、数据校验、数据赋值 这三大类问题。这篇文章里面的提供的解决方案能够比较完美的用在 React 框架上,但是解决问题的思路相信应该是可以使用于任何框架语言。

中后台的表单组件已经不仅仅有 input 和 select,可能还扩展到 范围选择器、日期选择器 等,这些组件往往为了实现更优雅的UI和更使用的交互会在原生的组件上面做多层封装,而经过多层叠加后可能已经看不到原生表单元素的影子了。比如经过封装下面这段 DOM 结构经过样式修改也可能成为一个输入组件,虽然完全看不到 input 的影子。

<span>
  <span contentEditable></span>
</span>复制代码

所以为了便于大家理解我这里从传统的原生 form 说起,好让大家有一个递进的过程。

引子:原生 form 表单

最初始的一份代码如下, 代码很简单,看着也很舒服

<form action="/api/post" method="post">
  username: <input name="username" />
  passowrd: <input name="password" />
  <button type="submit">submit</button>
</form>复制代码

但是你开始做数据校验相关,表单就立刻变得复杂多了。如下:代码增多了一倍。

<script>
  function checkname(target) {
    const value = target.value;
    if (value.length < 10) {
      document.getElementById('username_msg').innerHTML = '长度必须>10'
    } else {
      document.getElementById('username_msg').innerHTML = ''
    }
  }
  function checkpassword(target) {
    const value = target.value;
    if (!value.match(/^[\w]{6,16}$/)) {
      document.getElementById('password_msg').innerHTML = '密码必须 6-16 位字母数字'
    } else {
      document.getElementById('password_msg').innerHTML = ''
    }
  }
  function getInitData() {
    ajax({
      url:'/api/data', 
      success:function(data) {
        document.getElementById('username') = data.username;
    });
  }
  getInitData();
</script>

<form action="/api/post" method="post">
 username: <input name="username" onchange="checkname(this)"/>
           <span id="username_msg"></span>
 passowrd: <input name="password" onchange="checkpassword(this)"/>
           <span id="password_msg"></span>
 <button type="submit">submit</button>
</form>复制代码

如果把DOM的部分也用JS来实现,基本可以做到只修改JS不需要再动DOM结构,但是也让JS的复杂度增高不少。

React 里面所有的DOM结构都是自己通过JS 生成的,JSX也可以方便的实现DOM结构。但这里我拿原生表单举例,只是想说用 React 写出来的原生表单,并不比用原生 js 的优雅多少!!!

React 中的原生 form 表单

同样一段最简单的功能,套在 react 框架下面是这个样子。

class Demo extends React.Component {
  render() {
    return <form action="/api/post" method="post">
      username: <input name="username" />
      passowrd: <input name="password" />
      <button type="submit">submit</button>
    </form>
  }
}复制代码

比如同样想要实现校验输入自动 校验 和 赋值 ,看下面一段代码,想想就是一大堆事情要做。

class Demo extends React.Component {
  state = {
    username: '',
    password: '',
    usernameMsg: '',
    passwordMsg: '',
  };
  checkname = e => {
    // 获取数据
    const value = e.target.value;
    // 受控模式赋值
    this.setState({
      username: value,
    });
    // 校验数据
    if (value.length < 10) {
      this.setState({
        usernameMsg: '长度必须>10',
      });
    } else {
      this.setState({
        usernameMsg: '',
      });
    }
  };
  checkpassword = e => {
    // 获取数据
    const value = e.target.value;
    // 受控模式赋值
    this.setState({
      password: value,
    });
    // 校验数据
    if (!value.match(/^[\w]{6,16}$/)) {
      this.setState({
        passwordMsg: '密码必须 6-16 位字母数字',
      });
    } else {
      this.setState({
        passwordMsg: '',
      });
    }
  };
  handleSubmit = () => {
    ajax({
      url: '/api/post',
      data: {
        username: this.state.username,
        password: this.state.password,
      },
      success: () => { 
        // success
      },
    });
  };
  render() {
    // 获取数据和错误信息
    const { username, password, usernameMsg, passwordMsg } = this.state;
    return (
      <form action="/api/post" method="post">
        username: <input value={username} onChange={this.checkname} />
        <span>{usernameMsg}</span>
        passowrd: <input value={password} onChange={this.checkpassword} />
        <span>{passwordMsg}</span>
        <button type="submit" onClick={this.handleSubmit}>
          submit
        </button>
      </form>
    );
  }
}复制代码

代码有点长,但是基本可以总结出一个现象,要想实现表单数据获取、校验,基本离不开 onChange 这个方法,而且是有几个表单控件,就要写几个 onChange 。(以上代码可直接运行,可以在 codepen.io/frankqian/p… 调试)

其实这里和框架并没有什么关系,因为不管用什么框架要想做到 赋值校验 这两个功能,基本一定要在 input 上面绑定 onChange。 所以如果有个通用的 工具 可以自动帮你把这些onChange的绑定都做了,再把校验规则固定下,是不是所有的表单问题都可以解决了呢?是的通用表单解决方案就是按照这种思路设计出来的!

适用于所有 React 表单组件的解决方案

所有的用 React 写成的组件都可以使用该方案。甚至 非 React 体系也可以使用改思路来解决问题。

基于所有表单控件都需要绑定 onChange 做数据获取和校验的原则,所以我设计了一个 Field 工具。这个工具原理很简单,就是可以自动帮你绑定 value + onChange 解决上面一长串代码的问题。

const field = new Field(this);

field.init('username');复制代码

field.init 会自动返回 value + onChange ,内容如下:

{
  value: "",
  onChange: ƒ ()
}复制代码

下面这张图简单表面 Field 和 React 体系之间的关系。

Form 表单组件的设计之路

使用 Field 获取数据

import {Field} from '@alifd/next';

class Demo extends React.Component {
  field = new Field(this);
  handleSubmit = () => {
    console.log(this.field.getValues()); // 获取数据
  }
  render() {
    const {init} = this.field;
    return <form>
      username: <input {...init('username')} />
      passowrd: <input {...init('password')} />
      <button onClick={this.handleSubmit} >submit</button>
    </form>
  }
}复制代码

这样一个表单的数据获取问题就解决了,代码简洁了很多。 Demo 在这里 codepen.io/frankqian/p… 可以自己调试

表单校验

既然能够获取到数据了,那边表单校验是顺手的事情,因为校验只依赖数据。我们只需要对集中固定的交互性形式和校验规则做抽象就好了。

交互形式上大概包含以下三类

  • 输入的时候实时校验,一般 onChange 触发
  • 离开焦点的时候校验,一般 onBlur 触发
  • 通过自定义的操作来触发校验,自己调用 api 触发

常见的校验规则抽象

规则名称

描述

类型

触发条件/数据类型

required 不能为空 Boolean undefined/null/“”/[]
pattern 校验正则表达式 正则
minLength 字符串最小长度 / 数组最小个数 Number String/Number/Array
maxLength 字符串最大长度 / 数组最大个数 Number String/Number/Array
length 字符串精确长度 / 数组精确个数 Number String/Number/Array
min 最小值 Number String/Number
max 最大值 Number String/Number
format

对常用 pattern 的总结

url/email/tel/number

String String
validator 自定义校验 Function

这里说明下表单是弱类型的数据。比如 input 框里面你希望用户输入的是整数,返回的 value 类型可能有两种

  • "123456", String 类型的整数校验方式为 :/\d+/
  • 123456, Number 类型的整数校验方式为: typeof Value === 'number'

这个时候要求用户一定要返回 Number 类型才能校验非常不友好,所以在 Field 校验逻辑里面就把类型的问题处理掉了,而不是交给用户去判断。

上面是小插曲,我们继续看如下 Field + 表单的代码,解决了数据获取、表单校验的所有功能

import { Field } from '@alifd/next';

class Demo extends React.Component {
  field = new Field(this);
  handleSubmit = (e) => {
    e.preventDefault();
    this.field.validate(); // 自定义校验
    console.log(this.field.getValues()); // 获取数据
  }
  render() {
    const {init, getError} = this.field;
    
    return <form>
      username: <input {...init('username', {rules: { required: true, minLength: 10}})} />
      <span style={{color: 'red'}}>{getError('username')}</span>  {/**错误信息**/}
      passowrd: <input {...init('password', {rules: {
          pattern: /^[\w]{6,16}$/,
          message: '密码必须 6-16 位字母数字'
        }})} />
      <span style={{color: 'red'}}>{getError('password')}</span>  {/**错误信息**/}
      <button onClick={this.handleSubmit} >validate</button>
    </form>
  }
}复制代码

这样之前可能需要 70 行的代码 24 行就可以解决了,可以让代码清晰不少。调试demo见: codepen.io/frankqian/p…

Form 表单组件的设计之路

自己写的表单组件怎么用

现在很多React 组件是在原生组件之上又做了封装,还有很多组件可能并没有包裹表单元素(比如 Fusion Select 里面并没有 select 元素,下拉框是自己做的 )。但是只要你自己写的组件也遵循表单的规则就可以使用该方案。

基本规则: value + onChange 受控规则

这个规则其实来自原生 html 的组件,我们自己写的组件只要按照标准来都可以使用 Field。

Form 表单组件的设计之路

自己写的组件比起原生的表单组件会更加美观,交互更友好。只要遵循规范都能在 field 里面使用,详细demo 见 codepen.io/frankqian/p…

更人性化的功能

还有一些其他更加细粒度的规则,是为了让你的组件更加好的适配高级功能,比如:

  • 一键 reset 清空所有数据。因为每个组件的接收数据类型不一样,所以统一为在 willReceiveProps 里面接收 value=undefined
componentWillReceiveProps(nextProps) {
    if ('value' in nextProps ) {
        this.setState({
           value: nextProps.value === undefined? []: nextProps.value   //  设置组件的被清空后的数值
        })
    }
}复制代码
  • 一次交互操作只抛一次 onChange
  • 比如 upload 上传,如果一次上传触发上百次 onChange,那么整个页面会跟着一起 Render 几百次,非常影响性能

Form 表单组件的设计之路

  • 比如 Slider, 在拖动的时候如果实时触发 onChange,那么在拖动滑块的时候可能会非常卡顿。所以鼠标松开的那个瞬间触发才是比较合理的操作,其他的拖拽事件可以交给 onProgress

Form 表单组件的设计之路

Fusion Next 的表单组件基本都已经是按照这套规范标准实现了,详细可以查看这里的文档 fusion.design/component/f… 拉到最下面

Form 组件让体验持续升级

上面知道了 Field 可以解决校验、获取、赋值等数据方面的问题,但是并不能解决 UI 和 交互的问题,在布局和错误展示的时候需要自己来控制。

让布局更轻松

场景的布局有水平 inline 布局、垂直的分栏布局,通过 FormItem 的 api 可以非常轻松的做到

  • 垂直布局
<Form>
  <FormItem label="Username:">
    <Input name="first"  placeholder="first"/>
    <Input name="second" placeholder="second"/>
  </FormItem>
  <FormItem label="Password:" required>
    <Input htmlType="password" name="pass" placeholder="Please enter your password!"/>
  </FormItem>
  <FormItem label=" ">
    <Form.Submit>Submit</Form.Submit>
  </FormItem>
</Form>复制代码
Form 表单组件的设计之路
  • 水平布局
  • <Form inline>...</Form>复制代码

    Form 表单组件的设计之路

    • 标签内置
    <Form labelAligin="inset">...</Form>复制代码

    Form 表单组件的设计之路

    辅助错误展示

    出错的时候自动展示错误信息,不需要自己 getError 判断。 每种状态怎么展现由各自的组件自己实现。减少和Form的耦合

    Form 表单组件的设计之路

    每个组件的加载中、成功、失败,都由组件自己实现,Form 只是在校验的时候传递 state 给各个组件,这样不需要 Form 去关心每个组件应该展现为什么样!

    <Input state="error" />  // 错误状态
    <Input state="loading" /> // 加载中
    <Input state="success" /> // 成功
    <DatePicker state="error" /> // 错误状态复制代码

    进一步优化 Form 让使用更简单

    以上我们还是 Field + Form 配合来使用的,代码基本是这个样子。

    import { Form, Input, Field, Button } from '@alifd/next';
    
    const FormItem = Form.Item;
    
    class Demo extends React.Component {
      field = new Field(this);
      handleSubmit = () => {
        this.field.validate();
      }
      render() {
        const {init} = this.field;
        return  <Form field={this.field}>
            <FormItem label="Username:">
                <Input {...init('username', {
                  rules: {required}
                })} />
            </FormItem>
            <FormItem label="Password:">
                <Input {...init('password', {
                  rules: {pattern:/[\w]{6,16}/}
                })} htmlType="password" />
            </FormItem>
            <FormItem label=" ">
                <Button onClick={this.handleSubmit} >Submit</Button>
            </FormItem>
        </Form>
      }
    }复制代码

    可能写多了之后就会想, 每个组件都要使用 init 、都需要写 rules 规则 ,而且在 jsx 中写一大串的 JSON 数据。

    是否有方法让数据获取和校验变得更简单,让代码再进一步的简化呢?

    进一步集成 Field 能力而弱化用法

    针对以上问题对 Form 进一步优化,把 Field 的能力整合进了 Form,而把 Field 的用法进一步弱化,让大家不需要再关心 init/取数据 等问题。代码如下:

    import { Form, Input, Button } from '@alifd/next';
    
    const FormItem = Form.Item;
    
    class Demo extends React.Component {
      handleSubmit = (values, errors) => {
        if (errors) {
          // 校验出错 
          return;
        }
        console.log(values) // 获取数据
      }
      render() {
        return  <Form>
            <FormItem label="Username:" required>
                <Input name"username" />
            </FormItem>
            <FormItem label="Password:" pattern={/[\w]{6,16}/}>
                <Input name="password" htmlType="password" />
            </FormItem>
            <FormItem label=" ">
                <Form.Submit validate onClick={this.handleSubmit} >Submit</Form.Submit>
            </FormItem>
        </Form>
      }
    }复制代码

    上面代码中可以看出几个优化点:

    1. 不需要关注 Field 用法,改成 Form API 的方式。用法简单直接不少
    2. 通过 name 来进行数据初始化,也更加接近原生 form 的用法,大家更容易理解。
    3. 校验功能 API 化,代码更加简洁,可读性增强

    后记

    Form 的优化一定不会仅仅止于此,因为在实际业务中会遇到更加复杂的功能。

    很多业务为了更加方便快捷,会抽象常用的组件布局,通过后端接口吐出JSON schema的方式直接在前端动态展示表单,虽然比较业务化当时确实方便快捷,能够极大的解决效率问题;

    又或者把常用的表单类场景做成业务组件、模块模板,在使用的时候直接下载使用。比如:Fusion的表单类模块: fusion.design/module?cate…

    方案很多,总有适合自己的一套。


    以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

    查看所有标签

    本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

    Is Parallel Programming Hard, And, If So, What Can You Do About

    Is Parallel Programming Hard, And, If So, What Can You Do About

    Paul E. McKenney

    The purpose of this book is to help you understand how to program shared-memory parallel machines without risking your sanity.1 By describing the algorithms and designs that have worked well in the pa......一起来看看 《Is Parallel Programming Hard, And, If So, What Can You Do About 》 这本书的介绍吧!

    SHA 加密
    SHA 加密

    SHA 加密工具

    正则表达式在线测试
    正则表达式在线测试

    正则表达式在线测试

    HEX CMYK 转换工具
    HEX CMYK 转换工具

    HEX CMYK 互转工具