前端的Form 表单主要用于解决数据获取、数据校验、数据赋值 这三大类问题。这篇文章里面的提供的解决方案可以比较完美的用在 React 框架上,可是解决问题的思路相信应该是可使用于任何框架语言。html
中后台的表单组件已经不只仅有 input 和 select,可能还扩展到 范围选择器、日期选择器 等,这些组件每每为了实现更优雅的UI和更使用的交互会在原生的组件上面作多层封装,而通过多层叠加后可能已经看不到原生表单元素的影子了。好比通过封装下面这段 DOM 结构通过样式修改也可能成为一个输入组件,虽然彻底看不到 input 的影子。前端
<span>
<span contentEditable></span>
</span>复制代码
因此为了便于你们理解我这里从传统的原生 form 提及,好让你们有一个递进的过程。react
最初始的一份代码以下,代码很简单,看着也很舒服。git
<form action="/api/post" method="post">
username: <input name="username" />
passowrd: <input name="password" />
<button type="submit">submit</button>
</form>复制代码
可是你开始作数据校验相关,表单就马上变得复杂多了。以下:代码增多了一倍。github
<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的复杂度增高很多。ajax
React 里面全部的DOM结构都是本身经过JS 生成的,JSX也能够方便的实现DOM结构。但这里我拿原生表单举例,只是想说用 React 写出来的原生表单,并不比用原生 js 的优雅多少!!!正则表达式
一样一段最简单的功能,套在 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>
}
}复制代码
好比一样想要实现校验输入自动校验 和 赋值,看下面一段代码,想一想就是一大堆事情要作。api
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 体系也可使用改思路来解决问题。
基于全部表单控件都须要绑定 onChange 作数据获取和校验的原则,因此我设计了一个 Field 工具。这个工具原理很简单,就是能够自动帮你绑定 value + onChange 解决上面一长串代码的问题。
const field = new Field(this);
field.init('username');复制代码
field.init 会自动返回 value + onChange ,内容以下:
{
value: "",
onChange: ƒ ()
}复制代码
下面这张图简单表面 Field 和 React 体系之间的关系。
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… 能够本身调试
既然可以获取到数据了,那边表单校验是顺手的事情,由于校验只依赖数据。咱们只须要对集中固定的交互性形式和校验规则作抽象就行了。
规则名称 |
描述 |
类型 |
触发条件/数据类型 |
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 类型可能有两种
这个时候要求用户必定要返回 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 元素,下拉框是本身作的 )。可是只要你本身写的组件也遵循表单的规则就可使用该方案。
这个规则其实来自原生 html 的组件,咱们本身写的组件只要按照标准来均可以使用 Field。
本身写的组件比起原生的表单组件会更加美观,交互更友好。只要遵循规范都能在 field 里面使用,详细demo 见 codepen.io/frankqian/p…
还有一些其余更加细粒度的规则,是为了让你的组件更加好的适配高级功能,好比:
componentWillReceiveProps(nextProps) {
if ('value' in nextProps ) {
this.setState({
value: nextProps.value === undefined? []: nextProps.value // 设置组件的被清空后的数值
})
}
}复制代码
Fusion Next 的表单组件基本都已是按照这套规范标准实现了,详细能够查看这里的文档 fusion.design/component/f… 拉到最下面
上面知道了 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" /> // 错误状态复制代码
以上咱们仍是 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 数据。
是否有方法让数据获取和校验变得更简单,让代码再进一步的简化呢?
针对以上问题对 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>
}
}复制代码
上面代码中能够看出几个优化点:
Form 的优化必定不会仅仅止于此,由于在实际业务中会遇到更加复杂的功能。
不少业务为了更加方便快捷,会抽象经常使用的组件布局,经过后端接口吐出JSON schema的方式直接在前端动态展现表单,虽然比较业务化当时确实方便快捷,可以极大的解决效率问题;
又或者把经常使用的表单类场景作成业务组件、模块模板,在使用的时候直接下载使用。好比:Fusion的表单类模块:fusion.design/module?cate…
方案不少,总有适合本身的一套。