获取form表单_【第1535期】前端 Form 的表单的一个通用解决方案

前言

今日早读文章由阿里@布达投稿分享。

@布达,Alibaba Fusion项目组的。花名潕量。主要专注在设计系统、组件、可视化搭建这个领域

正文从这开始~~

Fusion Next - Form 表单解决方案

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

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

 contentEditable>

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

引子:原生 form 表单

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

 action="/api/post" method="post">
 username: name="username" />
 password: name="password" /> type="submit">submit

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

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(); action="/api/post" method="post">
username: name="username" onchange="checkname(this)"/> id="username_msg">
passowrd: name="password" onchange="checkpassword(this)"/> id="password_msg"> type="submit">submit

如果把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">submitbutton>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} />{usernameMsg}span>
       passowrd: <input value={password} onChange={this.checkpassword} />{passwordMsg}span><button type="submit" onClick={this.handleSubmit}>
         submit        button>form>);}}

代码有点长,大家大致看一眼干什么的就行,从上面一系列代码中基本可以总结出一个现象,要想实现表单数据获取、校验,基本离不开 onChange 这个方法,而且是有几个表单控件,就要写几个 onChange 。(以上代码可直接运行,可以在 https://codepen.io/frankqian/pen/XOROBw?editors=0010 调试)

其实这里和框架并没有什么关系,因为不管用什么框架要想做到 赋值和校验 这两个功能,基本一定要在 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 体系之间的关系。

dbfbe0e106a84ceaeb0466531e1dec47.png
1. 使用 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
     username: <input {...init('username')} />
     passowrd: <input {...init('password')} /><button onClick={this.handleSubmit} >submitbutton>form>}}

这样一个表单的数据获取问题就解决了,代码简洁了很多。 Demo 在这里 https://codepen.io/frankqian/pen/xMdoxZ?editors=0010 可以自己调试

2. 表单校验

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

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

  • 输入的时候实时校验,一般 onChange 触发

  • 离开焦点的时候校验,一般 onBlur 触发

  • 通过自定义的操作来触发校验,自己调用 api 触发

常见的校验规则抽象
规则名称描述类型触发条件/数据类型
required不能为空Booleanundefined/null/“”/[]
pattern校验正则表达式正则
minLength字符串最小长度 / 数组最小个数NumberString/Number/Array
maxLength字符串最大长度 / 数组最大个数NumberString/Number/Array
length字符串精确长度 / 数组精确个数NumberString/Number/Array
min最小值NumberString/Number
max最大值 / 数组精确个数NumberString/Number
format对常用 pattern 的总结url/email/tel/numberString
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
     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} >validatebutton>form>}}

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

94600d5deae519f69e4fa8311a7124ee.png
3. 写自定义的事件

既然 init 会自动返回 onChange,那么如果我希望自己在 onChange 里面加一些逻辑改怎么处理呢。

如果直接写 onChange 会被 init 覆盖掉,不管写前面还是后面总有一个onChange会被覆盖掉

 onChange={(value) => console.log(value)}
 {...init('username')} />

所以在 init 提供了 props 可以把组件原生的 props 透传进去,Field内部会做好 hook 逻辑处理。

 {...init('username', {props: {onChange:(value) => console.log(value)}
})} />

大家用起来觉得很麻烦,后面会再介绍如果在 Form 层面设计的更加趋向于原生方法的使用

4. 自己写的表单组件怎么用

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

4.1 基本: value + onChange 受控规则

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

184d9d46e022a6aa9b23e36c9a65c54e.png

自己写的组件比起原生的表单组件会更加美观,交互更友好。只要遵循规范都能在 field 里面使用,详细demo 见 https://codepen.io/frankqian/pen/gqRWJx?editors=0010这个规则其实来自原生 html 的组件,我们自己写的组件只要按照标准来都可以使用 Field。

4.2 高级功能:满足更加人性化的需求

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

一键 reset 清空所有数据。因为每个组件的接收数据类型不一样,所以统一为在 willReceiveProps 里面接收 value=undefined

componentWillReceiveProps(nextProps) {if ('value' in nextProps ) {this.setState({
          value: nextProps.value === undefined? []: nextProps.value   //  设置组件的被清空后的数值})}}

一次交互操作只抛一次 onChange

比如 upload 上传,如果一次上传触发上百次 onChange,那么整个页面会跟着一起 Render 几百次,非常影响性能

9030697bde1b6c962533aef997515038.png
Upload 上传完成才是用户想要的结果

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

82a5568bb89a99982b0e2dbdcecb892b.png
Slider 的拖动释放的那一刻可能才是用户想要的数据

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

Form 组件让体验持续升级

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

常用场景抽象让布局更轻松

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

垂直布局

 label="Username:"> name="first"  placeholder="first"/> name="second" placeholder="second"/> label="Password:" required> htmlType="password" name="pass" placeholder="Please enter your password!"/> label=" ">Submit
e5911a84553a5ef8a4f3e52942c48938.png
垂直布局

水平布局

 inline>...
7da35389ab0d2cccf6fb8467306109be.png
水平布局

标签内置

 labelAligin="inset">...
e029f02d19c54f8801a4ea3a828f1223.png
标签内嵌
  1. 辅助错误展示

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

77295a1396ec9a508444811ecad5ed67.png

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

 state="error" />  // 错误状态 state="loading" /> // 加载中 state="success" /> // 成功 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} >SubmitButton>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} >SubmitForm.Submit>FormItem>Form>}}

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

  • 不需要关注 Field 用法,改成 Form API 的方式。用法简单直接不少

  • 通过 name 来进行数据初始化,也更加接近原生 form 的用法,大家更容易理解。

  • 校验功能 API 化,代码更加简洁,可读性增强

后记

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

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

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

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

相关链接

  • field 组件 demo: https://fusion.design/component/field

  • form 组件 demo: https://fusion.design/component/form

  • Fusion Next 组件仓库: https://github.com/alibaba-fusion/next

关于本文
作者:@布达
原文:https://www.yuque.com/docs/share/8238bd4e-cfb2-42de-afa7-bc2b2f2ab0dc

e2e64abc9c5c86d7d926f7afaa5fc02e.png

最后,为你推荐

【第998期】JSON schema与表单验证

【第937期】探索两种优雅的表单验证——策略设计模式和ES6的Proxy代理模式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/530630.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

react取消捕获_React学习笔记(三)

React学习笔记&#xff08;三&#xff09;&#xff0c;组件的生命周期React中组件也有生命周期&#xff0c;也就是说也有很多钩子函数供我们使用, 组件的生命周期&#xff0c;我们会分为四个阶段&#xff0c;初始化、运行中、销毁、错误处理(16.3之后)初始化在组件初始化阶段会…

快捷键_AutoCAD 2021中的默认快捷键、新建或编辑快捷键

文&#xff1a;CAD钟日铭。欢迎关注和点赞支持。在使用AutoCAD进行制图的过程中&#xff0c;掌握一些快捷键会提高操作效率。所谓的快捷键是指用于启动命令的键组合。例如&#xff0c;按CtrlN组合键(快捷键)新建图形文件&#xff0c;按 CtrlO组合键可打开图形文件&#xff0c;按…

出现画面抖动_连续抖动20小时!虎门大桥桥面如波浪翻滚,专家:个人感觉没问题...

前两天(5月5日)&#xff0c;虎门大桥桥面出现如波浪翻滚的起伏&#xff0c;引发广泛的关注&#xff1a;次日&#xff0c;广东交通集团通报&#xff0c;虎门大桥震动是涡振现象&#xff0c;悬索桥结构安全。专家初步判断此次涡振和桥上设置水马有关。但直到6日早上11时&#xff…

安全模式 提权_记一次渗透挖洞提权实战

摘要&#xff1a;这是一次挖掘cms通用漏洞时发现的网站&#xff0c;技术含量虽然不是很高&#xff0c;但是也拿出来和大家分享一下吧&#xff0c;希望能给一部分人带来收获。0x01 进入后台在通过googlehack语法挖掘beescms时发现了这个站点利用网上的payload&#xff0c;在/mx_…

win7无法连接打印机拒绝访问_“Windows无法连接打印机,操作失败,错误为0x000003e3”...

请求大家帮助&#xff0c;昨天在共享打印机时出现“Windows无法连接打印机&#xff0c;操作失败&#xff0c;错误为0x000003e3”。在百度百科查询的资料修改了都不行(在不重装系统前提下&#xff0c;联想台式机都是win732位系统)&#xff0c;给大家一一列举&#xff0c;请求广大…

类型全部为string_python小讲堂丨学了这么久的,这6种基本数据类型你真的理解了吗...

哈喽&#xff0c;大家好&#xff0c;欢迎来到python小讲堂&#xff0c;间歇性的努力&#xff0c;会导致持续性的一事无成&#xff0c;即使今天是除夕&#xff0c;我们也不能懈怠啊&#xff0c;今天我给大家带来的是关于python的6种基本数据类型的深入讲解&#xff0c;话不多说让…

防抖 节流_【前端面试】节流与防抖

我们用两张图表示什么是节流和防抖。防抖节流由图可见&#xff0c;防抖的意思是&#xff0c;当用户在一段时间内连续频繁的试图执行一个函数的时候&#xff0c;只有最后一次&#xff0c;函数被真正的执行。节流的意思是&#xff0c;当用户在某一个时刻执行了一次函数的时候&…

没数据时y轴不显示_Matplotlib数据可视化

1.Matplotlib介绍什么是MatplotlibMatplotlib是一个Python的绘图库&#xff0c;它可与 NumPy 一起使用&#xff0c;可以代替MMatplotlib安装由于Matplotlib是第三方库&#xff0c;所以我们需要安装它才可以使用。注意&#xff0c;Matplotlib3.0要求python3版本才可安装使用。安…

循环划线_经济内外双循环下的思考!

原创&#xff1a;群赢说到这个内外双循环&#xff0c;我们汽车后市场朋友们&#xff0c;应该知道车子发动机水冷系统就是一个双循环&#xff0c;内部小循环&#xff0c;外部大循环。在发动机温度不高的时候&#xff0c;水在做小循环&#xff0c;在发动机周边流动。当发动机温度…

计算机组成原理实验软件仿真系统_计算机系统组成原理(基础)

网上关于计算机系统的讨论众说纷纭。刚开始&#xff0c;我卯足了劲想寻找一个标准答案&#xff0c;后来发现这并不存在&#xff01;因为计算机系统层次不一&#xff0c;看你从什么角度来理解这个问题。在这篇文章里&#xff0c;我的介绍一切从简&#xff0c;不求锦上添花&#…

1110: 最近共同祖先(函数专题)

1110: 最近共同祖先&#xff08;函数专题&#xff09; 时间限制: 1 Sec 内存限制: 128 MB 提交: 3818 解决: 3290 [提交] [状态] [讨论版] [命题人:admin] 题目描述 如上图所示&#xff0c;由正整数1, 2, 3, …组成了一棵无限大的二叉树。从某一个结点到根结 点&#xff08;编…

echart freemarker 模板_SpringBoot集成Freemarker模板生成Echarts图片(三)

紧接着前面两篇&#xff0c;本篇采用Freemarker模板来生成Echarts图片。一. 项目结构二. Freemarker模板(1)柱状图{"title": {"text": "${title}","textStyle": {"color": "red","fontSize": 15,"…

ios 系统提示框_ios13终于能屏蔽系统更新了!附详细教程

很多人会遇到这样的烦恼&#xff0c;ios频繁更新&#xff0c;自己用惯了当前系统版本&#xff0c;系统总提示新的更新请求&#xff0c;不小心点到之后&#xff0c;系统就会自动更新安装&#xff0c;有很多小伙伴更新后&#xff0c;手机变卡顿&#xff0c;耗电量加大&#xff0c…

判断是否有小数_一线老师笔记:数学判断题拿高分的技巧

01什么是判断题&#xff1f;判断是关于对象和它属性有所肯定或者否定的思维形式。在研究数学中&#xff0c;经常要对现实世界的空间形式和数量关系&#xff0c;作出肯定或者否定的回答&#xff0c;因而要大量使用判断&#xff0c;并把一些正确的判断作为进一步研究问题的依据。…

mysql 导入 sqlite_Mysql 数据导入SQlite

SQlite 有一个很强大的管理工具名字叫做Sqlite Developer (官方网站)。在Sqlite Developer中有关于数据导入的强大功能。可以将MSSQL Oracl Aeecss等等数据库导入到Mysql中。但是&#xff0c;默认在Win7环境中没有Mysql的倒入方式。你需要一个叫做“Mysql ODBC”的win客户端工具…

centos 安装mysql 5.7.9_CentOS 6.6下RPM方式安装MySQL 5.7.9

说明&#xff1a;从MySQL5.7.4起&#xff0c;以RPM包的方式安装后的MySQL的部署&#xff0c;默认是安全的&#xff0c;并且有这些特点&#xff1a;1. 在大多数情况下&#xff0c;你只需要安装MySQL-server和MySQL-client安装包就可以安装上一个标准功能的MySQL。对于一个标准安…

mongodb 导入 mysql_将mongodb 数据指定字段导出,然后指定字段导入mysql 实例 及相关问题解决...

需求&#xff1a;将mongodb 数据指定字段导出&#xff0c;然后再指定字段导入mysql 表中直接上图吧&#xff0c;最后 会将遇到几个问题及解决方案贴出&#xff0c;以便遇到类似问题可以迅速解决(期间所用命令具体用法不在本文详解之内)将mongodb 数据指定字段导出&#xff0c;…

mysql外部排序_深入浅出MySQL优先队列(你一定会踩到的order by limit 问题)

0.先抛问题假设字段category无索引且有重复值&#xff0c;order by category 和 limit 组合使用的结果会和预期不符。问题复现&#xff1a;表结构(就是两个字段)CREATE TABLE ratings (id int(11) NOT NULL AUTO_INCREMENT,category int(11) DEFAULT NULL,PRIMARY KEY (id)) EN…

navicat fo mysql 教程_Navicat For MySQL的简单使用教程

1.前提是必须先安装好MySQL数据库(Mac下安装MySQL数据库见前一篇)2.安装Navicat3.点击navicate左上角&#xff1a;连接->MySQL->先测链接下&#xff0c;如果提示连接成功&#xff0c;就可以填写连接名&#xff0c;点击连接即可。双击刚创建的连接下面会有四个数据库用naV…

mysql官网 ab_MySQLAB同步

MySQL 支持单向、异步复制,复制过程中一个服务器充当主服务器,而一个或多个其它服务器充当从服务器。主服务器将更新写入二进制日1 . 介绍MySQL 支持单向、异步复制,复制过程中一个服务器充当主服务器,而一个或多个其它服务器充当从服务器。主服务器将更新写入二进制日志文件,并…