Form 表单组件的设计之路
栏目: JavaScript · 发布时间: 5年前
内容简介:前端的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 体系之间的关系。
使用 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…
自己写的表单组件怎么用
现在很多React 组件是在原生组件之上又做了封装,还有很多组件可能并没有包裹表单元素(比如 Fusion Select 里面并没有 select 元素,下拉框是自己做的 )。但是只要你自己写的组件也遵循表单的规则就可以使用该方案。
基本规则: value + onChange 受控规则
这个规则其实来自原生 html 的组件,我们自己写的组件只要按照标准来都可以使用 Field。
自己写的组件比起原生的表单组件会更加美观,交互更友好。只要遵循规范都能在 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 几百次,非常影响性能
- 比如 Slider, 在拖动的时候如果实时触发 onChange,那么在拖动滑块的时候可能会非常卡顿。所以鼠标松开的那个瞬间触发才是比较合理的操作,其他的拖拽事件可以交给 onProgress
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 inline>...</Form>复制代码
- 标签内置
<Form labelAligin="inset">...</Form>复制代码
辅助错误展示
出错的时候自动展示错误信息,不需要自己 getError 判断。 每种状态怎么展现由各自的组件自己实现。减少和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> } }复制代码
上面代码中可以看出几个优化点:
- 不需要关注 Field 用法,改成 Form API 的方式。用法简单直接不少
- 通过 name 来进行数据初始化,也更加接近原生 form 的用法,大家更容易理解。
- 校验功能 API 化,代码更加简洁,可读性增强
后记
Form 的优化一定不会仅仅止于此,因为在实际业务中会遇到更加复杂的功能。
很多业务为了更加方便快捷,会抽象常用的组件布局,通过后端接口吐出JSON schema的方式直接在前端动态展示表单,虽然比较业务化当时确实方便快捷,能够极大的解决效率问题;
又或者把常用的表单类场景做成业务组件、模块模板,在使用的时候直接下载使用。比如:Fusion的表单类模块: fusion.design/module?cate…
方案很多,总有适合自己的一套。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- [ Laravel 5.7 文档 ] 基础组件 —— 表单验证
- 检查Angular2表单是否在组件内有效
- form-create 2.5.1 发布,强大的动态表单组件
- 支持嵌套对象、多级数组的Vue动态多级表单组件 —— vue-dynamic-form-component
- 表单 – 避免Symfony强制显示表单字段
- 细说 Angular 2+ 的表单(二):响应式表单
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。