简洁易⽤的表单数据设置和收集管理组件
阅读⽬录
这篇⽂章要分享的是我在做表单界⾯开发的⼀部分经验,关于表单数据设置和收集这⼀块的。整体⽽⾔,这篇⽂章总结的东西有以下的特点:
1)api简单,使⽤起来很容易;
2)简化了表单新增和编辑,可以让新增和编辑使⽤同⼀个表单页⾯;
3)基本上与UI分离,所以很容易应⽤到各类项⽬的开发当中。
涉及到的组件不⽌⼀个,⽽且将来还会扩充,这些组件都是根据以前的⼯作经验开发出来的,没有很⾼级的东西,每个组件的代码都很少,所以即使⽂中有介绍不到的地⽅,你也能通过阅读代码来详细了解。不过我想⼤部分⼈应该没有见过这样的使⽤⽅式(除了我毕业的时候进的那家公司的同事),我上家公司的朋友刚开始看到我⽤这种写法的时候都不太理解,但是⼤家最后都接受并认可了这种⽤法,因为在开发的时候效率确实还挺⾼的,这也是我写这篇⽂章分享出来的⽬的。
本⽂相关的代码我都放在github上⾯去了,原来我都直接上传在博客园,后来发现有的时候要改点东西
每次都得重新上传,挺不⽅便的,还是直接git简单点,另外git还可以通过gh-pages分⽀来显⽰静态内容,正好可以⽤来查看demo。
关于demo的简单说明:
这两个地址分别⽤来模拟了⼀个表单页⾯的新增和编辑时的场景,我⽤mode这个url参数来区分当前这个页⾯是新增还是编辑的状态,mode=1表⽰新
增,mode=2表⽰编辑。在这个页⾯⾥⾯⼀共有9个表单元素:
id: ⽤的是text[type=”hidden”]
name: ⽤的是text[type=”text”]
birthday: ⽤的是text[type=”text”],但是带⽇期下拉选择的功能
hobby: 是checkbox
gender: 是radio
work:是单选的select
industry:是多选的select
desc:是textarea
detailDesc: 也是textarea,只不过是⽤富⽂本编辑器呈现的。
这9个元素涵盖了常见了的表单元素类型,即使将来要增加其它的类型,也逃脱不了使⽤基本的表单元素来存取值,⽐如你可能见过的带下拉框或者输⼊提⽰的⽂本框,从本质上来说,在我们获取该字段元素的时候,只会从⽂本框获取值,⽽跟下拉框或者输⼊提⽰的框没有关系,下拉框仅仅起⼀个辅助录⼊的作⽤,跟我们表单数据收集没有关系,demo中⽣⽇这个表单元素就是⼀个很好的说明,它虽然⽤到了⽇期选择的插件,但是即使没有这个插件,也不会影响到⽂本框值的存取。我把这个说明出来其实是想表达,在表单数据收集或设置的时候,应该考虑⼀下分离的思想,只有这样写出来的组件才能够不受项⽬的影响。关于这部分的思想,我推荐⼀篇更好的⽂章,感兴趣的可以深⼊阅读:
demo相关的html⽂件是src/html/demo1.html,js⽂件是src/js/app/demo1.js。整个项⽬⽤了seajs做模块化,⽤了gulp来做简单构建,还⽤到以前的⼏篇博客总结的⼀些东西:
1):提供⼀个class.js,⽤来定义javascript的类和构建类的继承关系;
2):提供⼀个eventBase.js,⽤来给任意组件实例提供类似DOM的事件管理功能。
在src/js/app/demo1.js中你可以看到,demo这个表单,在点击保存,收集数据的时候是多么的简单:
demo如果想在本地运⾏起来的话,可以参考github上readme.md提供的说明。下⾯开始详细介绍这整套组件的内容。
1. 前⾔
在传统的表单界⾯开发中,我们可能会碰到以下这些问题:
1)表单新增跟表单编辑到底是⼀个页⾯还是两个页⾯?
如果⽤两个页⾯,开发的时候好像很⽅便,但是将来维护的时候会很⿇烦,因为会存在⼤量的重复代码。所以我个⼈更倾向于⽤⼀个页⾯,但是⽤⼀个页⾯的话,在设置表单元素的初始值时会加不少重复的逻辑判断,因为⼤部分表单元素在新增的时候初始值都是空的,⽽在编辑的时候可能都是有值的,⽽我们的表单元素只有⼀个value属性,如果我们是通过jsp或php等模板来个表单元素赋值,这个处理起来也会很繁琐;
2)checkbox radio以及设置了multiple属性的select元素在设置value的时候也很繁琐,因为它们都不
是直接通过value属性来确定初始化值的,⽽是通过checked或selected属性来判断的,所以在设置初始值的时候需要判断每⼀个checkbox radio或select的option元素的value值与要设定的初始值是否相等才能给它添加checked或selected属性;
3)select元素的下拉内容有可能不是已知的,需要另外请求再渲染出来,这个时候如果每次都单独为这种需求的select下ajax逻辑显得太低效了
4)在收集表单数据并提交到后台的时候,现有的DOM⽅式在获取的时候不是很⽅便,虽然jquery简化了这部分的处理,但是它没有约定,会将所有的表单数据都收集起来,有时候这⾥⾯会有⼀些不必要的数据,如果能够提前约定好要收集的表单元素,就可以避免收集不必要的数据;
5)浏览器标准事件中给所有的表单元素都提供了change事件,但是这个事件有时候还不够⽅便,要是能把这个事件拆分成⼀对事件,⽐如beforeChange跟afterChange,就能适应更复杂的需求场景。从名字⼤概能猜到这两个事件的作⽤和触发的时机,这两个事件我没法说它的具体作⽤到底⽐单个的change事件强多少,但是从我以前做ERP管理软件的经验来说,beforeChange能够起到很多控制作⽤,afterChange也能够代替原来的change事件;
6)每个表单元素都有些相似的属性或者相似的⾏为,如果我们把这些相似的东西都抽象出来,每个表单元素的使⽤将会变得⾮常简单,⽽且将来要扩充像下拉输⼊这类的表单元素也都会很容易。
为了解决这些问题我的思路是:
1)将页⾯分为3种模式,新增,编辑,查看模式,分别⽤url参数mode=1,mode=2,mode=3来区分,新增模式表⽰当前页⾯正在录⼊新的数据,是还没在数据库保存过的;编辑模式表⽰当前页⾯正在编辑已经在数据库中存在的数据;查看模式表⽰当前页⾯正在查看从数据库中查询出的数据,但是只能看不能改。也许有⼈会说查看模式没有什么⽤处,但是在ERP管理系统数据的修改控制是很重要的,所以曾经公司的开发平台⾥⾯⽤了这三种模式来控制页⾯的状态。不过这个做法有⼀定的风险,就是知晓这个原理的⼈,可能通过修改url后⾯的参数来看到不⽤的页⾯状态,⽐如他只有权限能看到mode=3的页⾯,但是只要将地址⾥的mode=3改成mode=2再回车,就能进⼊编辑的页⾯状态,所以在⼀些关键的逻辑的处理中,必须⽤数据的业务状态来做判断,⽽不能使⽤mode,mode仅仅能做到在UI层⾯的控制;
2)⽤defaultValue这个option来指定表单元素在新增时候的初始值,⽤value属性来表⽰表单元素在编辑或查看模式时的初始值,这样在jsp或者php模板⾥⾯,
我们只要把初始值写在不同的位置即可,当前端根据mode初始化完组件之后就会显⽰正确的初始值,⽽defaultValue这个option我们可以通过data-default-value直接写在表单元素的html上,value属性本⾝就是表单元素的标准属性,所以可以直接写,如:
3)我把表单元素的相似的属性跟⾏为统⼀封装到了formFieldBase这个组件⾥⾯,其它各个表单元素只要继承它即可。
下⾯先来看看formFieldBase.js的内容,它是最基础最重要的⼀个组件。
代码如下:
formFieldBase,为所有的表单元素组件定义了以下基本的option:
其中:
name:⽤来唯⼀标识⼀个表单元素,不能重复。如果某个需求中,某个字段需要可能⽤到多个表单元素,可以在name属性上添加⼀些索引前缀或后缀来处理。它除了可以在组件初始化的时候通过options传递给组件的构造函数,还可以直接在组件相关的元素上通过name属性或者data-name来属性来设置。type:⽤来指定这个组件的类型,它是⾃定义的,跟input元素上的type完全没有关系。每⼀个继承formFieldBase的组件都有⼀个type。它要么是options来传递,要么就是通过data-type来传递,⽬前已开发的组件有
formFieldCheckbox,formFieldDate,formFieldRadio,formFieldText,formFieldSelect,formFieldUeditor,对应的type值是:
text,checkbox,radio,select,date跟ueditor。
value:编辑或查看模式时的初始值。
defaultValue: 新增模式时的初始值。
onBeforeChange: 它是beforeChange事件的回调,在值发⽣改变前触发,在该事件中,如果通过e.preventDefault()阻⽌了默认⾏为,表单元素的值将会被重置为上⼀次修改的后的值,并且不会再触发后⾯的afterChange事件。
onAfterChange: 它是afterChange事件的回调,在值发⽣改变后触发。
onInit:它formFieldInit事件的回调,这个事件表⽰组件何时初始化完毕。触发的时机由具体实现的⼦类来决定,formFieldBase提供了triggerInit()⽅法,⼦类可通过调⽤这个⽅法来触发formFieldInit,之所以这么做,是因为各个表单元素触发这个事件的时机是不定的,所以不能在formFieldBase⾥⾯来做触
发,formFieldBase仅仅提供统⼀的事件注册,通常在⼦类的init⽅法的最后被触发,但也可能不是,⽐如formFieldSelect组件⾥⾯,你就可以看到不⼀样的触发逻辑。
每个表单元素的初始值都是根据mode来判断获取的,在mode为1的时候只会通过defaultValue这个option来获取初始值,在mode=2的时候,还会通过jquery的val⽅法来进⼀步获取值。初始值的设置通过调⽤reset⽅法即可,调⽤时机由各个⼦类的去决定,⼀般都是在⼦类的init⽅法⾥⾯。
通过formFieldBase为所有的表单元素提供了⼀下api⽅法:
1) setValue(value, trigger)
⽤来给表单元素设置值,第⼆个参数可选,默认调⽤这个⽅法的时候都会触发表单元素的change事件,不然beforeChange跟afterChange都⽆法正确管理。只有当第⼆个参数为false的时候,才不会触发change事件。formFieldBase提供了_setValue⽅法,⼦类不需要覆盖setValue⽅法,只要覆盖_setValue⽅法即可。这么做的原因是setValue⽅法⾥⾯有⼀些公共的逻辑,可以抽象到formFieldBase⾥⾯去。这样当调⽤⼦类的setValue⽅法时将会调⽤⽗类的setValue⽅法,最后通过_setValue这个⽅法来实现不同的⼦类的逻辑。
2)getValue()
获取表单元素的值。
3)enable()
启⽤
4)disable()
禁⽤
5)reset()
重置为初始值,不会触发change,beforeChange以及afterChange事件。
希望前⾯这些内容能够让你把formFieldBase这个组件的⼀些我⾃⼰的想法看的明⽩,如果有不明⽩的可以直接私信跟我交流。下⾯基于这个formFieldBase,来看下各个不同的表单元素组件是如何实现的。
代码如下:
这个组件是最简单的⼀个,所以就不过多介绍代码,简单说下它的⽤法。⾮checkbox和radio的input元素以及textarea元素都能使⽤它:
如果是直接通过formFieldText构造函数可以这么⽤:
(这个例⼦只是为了说明FormFieldText这个组件的⽤法,没有任何需求背景)。
代码说明:
代码也很简单,不过有以下⼏点值得说明:
1)getValue时如果有多个checkbox被选中,那么最后会把多个值以英⽂逗号分隔的⽅式返回
2)setValue的时候如果⼀次性设置多个checkbox被选中,得传⼊⼀个英⽂逗号分隔的字符串的值
3)为了避免去设定各个checkbox的checked属性,这个组件并不是针对单个的checkbox元素来使⽤的,⽽是把这些checkbox的某个公共的⽗元素作为这个组件的关键元素,所以这个组件在使⽤的时候,要⽤data-name,data-value来指定元素的名称和编辑时的初始值。
举例如下:
注意以上代码中的div,它才是真正使⽤formFieldCheckbox的element。还需要说明的是,尽管这个div元素上还有⼀些特殊的css,如checkbox,checkbox-md,这些仅仅是UI相关的,跟js逻辑没有关系。
初始化的⽅式是:
仅展⽰代码,要说明的东西跟formFieldCheckbox区别很⼩:
代码如下:
这个组件功能相对多⼀点,它还提供了⼏个额外的option:
url: 默认是空的,如果有值的话,将在初始化的时候通过该值发起ajax请求加载下拉的数据。
textField: 只有在url不为空的情况下才会⽤到,表⽰ajax返回的数据中哪个字段是⽤来显⽰<option>的
⽂本的。
valueField: 只有在url不为空的情况下才会⽤到,表⽰ajax返回的数据中哪个字段是⽤来显⽰<option>的value的。
autoAddEmptyOption: 只有在url不为空的情况下才会⽤到,表⽰是否⾃动添加⼀个空的option。
jquery是什么功能组件emptyOptionText: 只有在url不为空的情况下才会⽤到,表⽰空option的⽂本。
parseAjax: 回调,只有在url不为空的情况下才会⽤到,⽤来解析ajax返回的数据,需要返回⼀个数组,存放需要渲染成下拉内容的数据。
还需要说明的是:
1)getValue的时候,如果有多个选中的option,它们的值将以英⽂逗号分隔的形式返回;
2)setValue的时候,如果要⼀次性设置多个option的选中状态,得以英⽂逗号分隔的字符串传值;
3)它还提供了⼀个render(data, clear),接收2个参数,第⼆个参数可选,可以⽤⼀份新的数据来替换下拉框的内容,第⼆个参数如果为true,则会把之前的下拉内容清空。
实际⽤法:
构造函数的使⽤⽅式与前⾯的相同,所以不再详细介绍了。
代码如下:
这个组件跟formFieldText没有太多区别,需要说明的是:
1)它依赖了bootstrap-datetimepicker这个插件,来实现⽇期选择的效果。如果想替换成其它的插件来实现⽇期选择,需要改这部分的源码;
2)它依赖的⽇期插件仅仅是起到辅助录⼊的作⽤,不影响formFieldBase定义的那些基本的属性和⾏为。
实际使⽤的时候,只要把Input元素的data-type指定为date即可:
代码如下:
之所以写这个完全是为了简化ueditor的使⽤,就像前⾯说的,ueditor不改变表单元素在表单开发中的本质,仅仅是辅助录⼊的作⽤,实现起来也没什么难度,只要想办法实现setValue getValue enable disabled reset这些重要的api⽅法即可,ueditor只是个插件,在init⽅法内个合适的位置做⼀下初始化就好了。
实际使⽤:
注意data-type。
10. ⽂中⼩结
以上部分已经把formFieldBase以及现有的各个表单元素组件都介绍完了,但是在实际使⽤过程中,如果我们每个元素都要⼿动调⽤构造函数去初始化的话,那就太⿇烦了,这远不是本⽂想要起到的作⽤,所以为了简化最终的表单数据设置和收集的功能,我还另外写了两个组件:formFieldMap,这就是个映射表;formMap,这是个容器组件,管理内部所有的表单元素组件实例。在实际需求中,⼀般只要⽤到formMap即可。
这个仅仅是为了formMap服务的,因为formMap会根据某个规则到所有的待初始化的元素,然后根据元素的data-type属性,再根据formFieldMap来到具体的构造函数:
代码如下:
它提供了三个option:
mode: 跟formFieldBase的mode作⽤是⼀样的,只不过因为formMap是作⽤于form元素上的,所以它的mode属性相当于是全局的,会对所有的表单元素都起作⽤;
fieldSelector: ⽤来过滤需要被初始化的表单元素的选择器,默认是.form-field,只要⼀个元素上有这个class,就会被这个容器管理,通常保留默认值即可;fieldOptions:可以通过它传递各个表单元素的各⾃的option。
使⽤举例:
它还提供了以下api⽅法,在实际⼯作中可以⽤得到:
1)get(name),⽤来获取某个字段的表单元素组件的实例
2)add($field, option),将⼀个新的元素添加到容器来管理,这个在⼀些需要动态对表单元素进⾏增删的时候会经常⽤到
3)remove(name),移除某个元素的在容器中的组件实例
4)getData(),获取容器内所有表单元素组件的值,以Object实例的形式返回
5)setData(data, trigger),统⼀设置容器内所有的表单组件的值,第⼆个参数如果为false,则不会触发各个组件的change事件
6)reset(),重置整个容器内所有的表单元素组件的值为初始值。
13. 本⽂总结
本⽂介绍的内容很多,但从我个⼈⽽⾔,⽤途还是很⼤的,去年的公司⾥⾯有很多个项⽬都是⽤这种⽅式开发完成的,开发速度很快,⽽且整体上逻辑都⽐较清晰,⽐较好理解,所以⾮常希望这⾥⾯的东西也能够给其它⼈带来帮助。前段时间⼯作还⽐较忙,有很多的时间都花在这篇⽂章现有成果的思考和优化⽅⾯,将来也还会继续改进,⽬的就是为了希望在项⽬开发过程中能够更加省时省⼒,同时
还要保质保量。下⼀步我会介绍⾃⼰如何进⼀步封装form组件以及form校验这⼀块的内容,已经有成果了,只是要等下周六⽇才会有时间来总结,请再关注。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论