• 简洁易用的表单数据设置和收集管理组件


    这篇文章要分享的是我在做表单界面开发的一部分经验,关于表单数据设置和收集这一块的。整体而言,这篇文章总结的东西有以下的特点:
    1)api简单,使用起来很容易;
    2)简化了表单新增和编辑,可以让新增和编辑使用同一个表单页面;
    3)基本上与UI分离,所以很容易应用到各类项目的开发当中。
    涉及到的组件不止一个,而且将来还会扩充,这些组件都是根据以前的工作经验开发出来的,没有很高级的东西,每个组件的代码都很少,所以即使文中有介绍不到的地方,你也能通过阅读代码来详细了解。不过我想大部分人应该没有见过这样的使用方式(除了我毕业的时候进的那家公司的同事),我上家公司的朋友刚开始看到我用这种写法的时候都不太理解,但是大家最后都接受并认可了这种用法,因为在开发的时候效率确实还挺高的,这也是我写这篇文章分享出来的目的。

    本文相关的代码我都放在github上面去了,原来我都直接上传在博客园,后来发现有的时候要改点东西每次都得重新上传,挺不方便的,还是直接git简单点,另外git还可以通过gh-pages分支来显示静态内容,正好可以用来查看demo。

    代码地址:
    https://github.com/liuyunzhuge/blog/tree/master/form
    demo地址:
    http://liuyunzhuge.github.io/blog/form/dist/html/demo1.html?mode=1
    http://liuyunzhuge.github.io/blog/form/dist/html/demo1.html?mode=2

    关于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中生日这个表单元素就是一个很好的说明,它虽然用到了日期选择的插件,但是即使没有这个插件,也不会影响到文本框值的存取。我把这个说明出来其实是想表达,在表单数据收集或设置的时候,应该考虑一下分离的思想,只有这样写出来的组件才能够不受项目的影响。关于这部分的思想,我推荐一篇更好的文章,感兴趣的可以深入阅读:

    顺势而为,HTML发展与UI组件设计进化

    demo相关的html文件是src/html/demo1.html,js文件是src/js/app/demo1.js。整个项目用了seajs做模块化,用了gulp来做简单构建,还用到以前的几篇博客总结的一些东西:

    1)详解Javascript的继承实现:提供一个class.js,用来定义javascript的类和构建类的继承关系;
    2)jquery技巧之让任何组件都支持类似DOM的事件管理:提供一个eventBase.js,用来给任意组件实例提供类似DOM的事件管理功能。

    在src/js/app/demo1.js中你可以看到,demo这个表单,在点击保存,收集数据的时候是多么的简单:

    image 

    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属性本身就是表单元素的标准属性,所以可以直接写,如:
    image

    3)我把表单元素的相似的属性跟行为统一封装到了formFieldBase这个组件里面,其它各个表单元素只要继承它即可。

    下面先来看看formFieldBase.js的内容,它是最基础最重要的一个组件。

    2. formFieldBase.js

    代码如下:

    define(function (require, exports, module) {
        var $ = require('jquery'),
            EventBase = require('mod/eventBase'),
            Class = require('mod/class');
    
        var DEFAULTS = {
            name: '',//字段名称
            type: '',//字段类型:text checkbox radio date number select ueditor等
            value: '',//在mode为2,3时显示的值
            defaultValue: '',//在mode为1时显示的值
            mode: 1,//可选值有1,2,3,分别代表字段属于新增,编辑和查看模式
            onBeforeChange: $.noop,//在字段相关表单元素的value的值发生改变前触发,这个回调可以对字段的值做一些校验
            onAfterChange: $.noop,//在字段相关表单元素的value的值发生改变后触发
            onInit: $.noop//在字段初始化完毕之后调用
        };
    
        function parseValue(value) {
            //特殊情况处理:当initValue是一个函数或者对象时
            typeof(value) == 'function' && (value = value());
            typeof(value) == 'object' && (value = JSON.stringify(value));
            return $.trim(value);
        }
    
        var FormFieldBase = Class({
            instanceMembers: {
                init: function (element, options) {
                    var $element = this.$element = $(element);
                    //通过this.base调用父类EventBase的init方法
                    this.base($element);
    
                    var opts = this.options = this.getOptions(options), that = this;
                    //获取field的name
                    //name有三种来源:opts.name,data-name属性以及name属性
                    this.name = opts.name || $element.attr('name');
                    //获取field的mode值:1,2,3分别代表新增,编辑和查看模式
                    this.mode = ~~opts.mode;
                    //获取field的初始值
                    this.initValue = (function () {
                        var initValue;
                        if (that.mode === 1) {
                            //新增模式时使用defaultValue作为初始值
                            initValue = opts.defaultValue;
                        } else {
                            //非新增模式时一般情况下用value作为初始值
                            //如果value值为空,判断字段对应的元素有没有val的jquery方法
                            //有的话通过该方法再获取一次值
                            initValue = $.trim(opts.value) == '' ?
                                (('val' in $element) && $element.val()) :
                                opts.value;
                        }
    
                        return parseValue(initValue);
                    })();
                    //获取field的类型
                    this.type = opts.type;
    
                    delete opts.value;
                    delete opts.defaultValue;
                    delete opts.mode;
                    delete opts.name;
                    delete opts.type;
    
                    //注册两个基本事件的监听
                    if (typeof(opts.onAfterChange) === 'function') {
                        this.on('afterChange', $.proxy(opts.onAfterChange, this));
                    }
    
                    if (typeof(opts.onBeforeChange) === 'function') {
                        this.on('beforeChange', $.proxy(opts.onBeforeChange, this));
                    }
    
                    if (typeof(opts.onInit) === 'function') {
                        this.on('formFieldInit', $.proxy(opts.onInit, this));
                        this.on('formFieldInit', $.proxy(function(){
                            if(this.mode === 3) {
                                this.disable();
                            }
                        }, this));
                    }
    
                    $element.data('formField', this);
                },
                getOptions: function (options) {
                    var defaults = this.getDefaults(),
                        _opts = $.extend({}, defaults, this.$element.data() || {}, options),
                        opts = {};
    
                    //保证返回的对象内容项始终与当前类定义的DEFAULTS的内容项保持一致
                    for (var i in defaults) {
                        if (Object.prototype.hasOwnProperty.call(defaults, i)) {
                            opts[i] = _opts[i];
                        }
                    }
    
                    return opts;
                },
                getDefaults: function () {
                    return DEFAULTS;
                },
                triggerInit: function () {
                    this.trigger('formFieldInit');
                },
                destroy: function () {
                    this.base();
                    this.$element.data('formField', null);
                    this.options = undefined;
                    this.$element = undefined;
                    this.name = undefined;
                    this.initValue = undefined;
                    this.mode = undefined;
                    this.type = undefined;
                },
                setValue: function (value, trigger) {
                    value = $.trim(parseValue(value));
    
                    //如果跟原来的值相同则不处理
                    if (value === $.trim(this.getValue())) return;
    
                    //将input的值设置成value
                    this.setFieldValue(value);
    
                    this._setValue(value, trigger);
                },
                //子类实现这个
                _setValue: $.noop,
                setFieldValue: $.noop,
                getValue: $.noop,
                enable: $.noop,
                disable: $.noop,
                reset: $.noop
            },
            extend: EventBase,
            staticMembers: {
                DEFAULTS: DEFAULTS
            }
        });
    
        return FormFieldBase;
    });

    formFieldBase,为所有的表单元素组件定义了以下基本的option:
    image
    其中:
    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,来看下各个不同的表单元素组件是如何实现的。

    3. formFieldText.js

    代码如下:

    define(function (require, exports, module) {
        var $ = require('jquery'),
            FormCtrlBase = require('mod/formFieldBase'),
            Class = require('mod/class');
    
        var DEFAULTS = $.extend({}, FormCtrlBase.DEFAULTS);
    
        var FormFieldText = Class({
            instanceMembers: {
                init: function (element, options) {
                    //通过this.base调用父类FormCtrlBase的init方法
                    this.base(element, options);
                    //设置初始值
                    this.reset();
    
                    var that = this,
                        $element = this.$element;
    
                    //监听input元素的change事件,并最终通过beforeChange和afterChange来管理
                    $element.on('change', function (e) {
                        var val = that.getValue(), event;
    
                        if(val === that.lastValue) return;
    
                        that.trigger((event = $.Event('beforeChange')), val);
                        //判断beforeChange事件有没有被阻止默认行为
                        //如果有则把input的值还原成最后一次修改的值
                        if (event.isDefaultPrevented()) {
                            that.setFieldValue(that.lastValue);
                            $element.focus().select();
                            return;
                        }
    
                        //记录最新的input的值
                        that.lastValue = val;
                        that.trigger('afterChange', val);
                    });
    
                    this.triggerInit();
                },
                getDefaults: function () {
                    return DEFAULTS;
                },
                _setValue: function (value, trigger) {
                    //只要trigger不等于false,调用setValue的时候都要触发change事件
                    trigger !== false && this.$element.trigger('change');
                },
                setFieldValue: function (value) {
                    var $element = this.$element,
                        elementDom = this.$element[0];
                    if (elementDom.tagName.toUpperCase() === 'TEXTAREA') {
                        var v = ' ' + value;
                        elementDom.value = v;
                        elementDom.value = v.substring(1);
                    } else {
                        $element.val(value);
                    }
                },
                getValue: function () {
                    return this.$element.val();
                },
                disable: function () {
                    this.$element.addClass('disabled').prop('readonly', true);
                },
                enable: function () {
                    this.$element.removeClass('disabled').prop('readonly', false);
                },
                reset: function () {
                    this.setFieldValue(this.initValue);
                    this.lastValue = this.initValue;
                }
            },
            extend: FormCtrlBase,
            staticMembers: {
                DEFAULTS: DEFAULTS
            }
        });
    
        return FormFieldText;
    });

    这个组件是最简单的一个,所以就不过多介绍代码,简单说下它的用法。非checkbox和radio的input元素以及textarea元素都能使用它:

    <input class="form-control form-field"
         name="id"
         data-type="text"
         data-default-value=""
         value="1"
         type="hidden"
         placeholder="">
    
    <input class="form-control form-field"
         name="name"
         data-type="text"
         data-default-value=""
         value="felix"
         type="text"
         placeholder="">
    
    <textarea class="form-control form-field"
              name="desc"
              data-type="text"
              data-default-value=""
              rows="3"
              placeholder="">felix</textarea>

    如果是直接通过formFieldText构造函数可以这么用:

    new FormFieldText('#name',{
        onInit: function(){
            console.log(this.getValue());
        },
        onBeforeChange: function(e, val){
            if(val == 'xx') {
                e.preventDefault();
            }
        }
    });

    (这个例子只是为了说明FormFieldText这个组件的用法,没有任何需求背景)。

    4. formFieldCheckbox.js

    代码说明:

    define(function (require, exports, module) {
        var $ = require('jquery'),
            FormCtrlBase = require('mod/formFieldBase'),
            Class = require('mod/class');
    
        var DEFAULTS = $.extend({
                //defaultValue 以及value使用checkbox的值
                useInputValue: {
                    forDefaultValue: false,
                    forValue: false
                }
            }, FormCtrlBase.DEFAULTS),
            INPUT_SELECTOR = 'input[type=checkbox]';
    
        var FormFieldCheckbox = Class({
            instanceMembers: {
                init: function (element, options) {
    
                    //通过this.base调用父类FormCtrlBase的init方法
                    this.base(element, options);
    
                    var that = this,
                        $element = this.$element;
    
                    //获取所有的input元素
                    var $inputs = this.$inputs = $element.find(INPUT_SELECTOR);
                    //设置它们的name属性,以便能够呈现复选的效果
                    $inputs.prop('name', this.name);
    
                    var opts = this.options;
                    if((this.mode == 1 && opts.useInputValue.forDefaultValue) ||
                        opts.useInputValue.forValue) {
                        this.initValue = this.getValue();
                    }
    
                    //设置初始值
                    this.reset();
    
                    //监听input元素的change事件,并最终通过$element的beforeChange和afterChange来管理
                    $element.on('change', INPUT_SELECTOR, function (e) {
                        var val = that.getValue(), event;
    
                        if (val === that.lastValue) return;
    
                        that.trigger((event = $.Event('beforeChange')), val);
                        //判断beforeChange事件有没有被阻止默认行为
                        //如果有则把input的值还原成最后一次修改的值
                        if (event.isDefaultPrevented()) {
                            that.setFieldValue(that.lastValue);
                            return;
                        }
    
                        //记录最新的input的值
                        that.lastValue = val;
                        that.trigger('afterChange', val);
                    });
    
                    this.triggerInit();
                },
                getDefaults: function () {
                    return DEFAULTS;
                },
                _setValue: function (value, trigger) {
                    //只要trigger不等于false,调用setValue的时候都要触发change事件
                    trigger !== false && this.$inputs.eq(0).trigger('change');
                },
                setFieldValue: function (value) {
                    this.$inputs.val(value.split(','));
                },
                getValue: function () {
                    var val = [];
                    this.$inputs.filter(':checked').each(function () {
                        val.push(this.value);
                    });
                    return val.join(',');
                },
                disable: function () {
                    this.$element.addClass('disabled');
                    this.$inputs.prop('disabled', true);
                },
                enable: function () {
                    this.$element.removeClass('disabled');
                    this.$inputs.prop('disabled', false);
                },
                reset: function () {
                    this.setFieldValue(this.initValue);
                    this.lastValue = this.initValue;
                }
            },
            extend: FormCtrlBase,
            staticMembers: {
                DEFAULTS: DEFAULTS
            }
        });
    
        return FormFieldCheckbox;
    });

    代码也很简单,不过有以下几点值得说明:

    1)getValue时如果有多个checkbox被选中,那么最后会把多个值以英文逗号分隔的方式返回
    2)setValue的时候如果一次性设置多个checkbox被选中,得传入一个英文逗号分隔的字符串的值
    3)为了避免去设定各个checkbox的checked属性,这个组件并不是针对单个的checkbox元素来使用的,而是把这些checkbox的某个公共的父元素作为这个组件的关键元素,所以这个组件在使用的时候,要用data-name,data-value来指定元素的名称和编辑时的初始值。

    举例如下:

    <div class="col-xs-5 checkbox checkbox-md form-field"
         data-name="hobby"
         data-type="checkbox"
         data-default-value=""
         data-value="电影,音乐">
      <label>
        <input type="checkbox" value="电影">
        <i class="fa checked"></i>
        电影
      </label>
      <label>
        <input type="checkbox" value="音乐">
        <i class="fa checked"></i>
        音乐
      </label>
      <label>
        <input type="checkbox" value="游戏">
        <i class="fa checked"></i>
        游戏
      </label>
    </div>

    注意以上代码中的div,它才是真正使用formFieldCheckbox的element。还需要说明的是,尽管这个div元素上还有一些特殊的css,如checkbox,checkbox-md,这些仅仅是UI相关的,跟js逻辑没有关系。

    初始化的方式是:

    new FormFieldCheckbox('#hobby',{
        onInit: function(){
            console.log(this.getValue());
        },
        onBeforeChange: function(e, val){
            if(val == 'xx') {
                e.preventDefault();
            }
        }
    });

    5. formFieldRadio.js

    仅展示代码,要说明的东西跟formFieldCheckbox区别很小:

    define(function (require, exports, module) {
        var $ = require('jquery'),
            FormCtrlBase = require('mod/formFieldBase'),
            Class = require('mod/class');
    
        var DEFAULTS = $.extend({}, FormCtrlBase.DEFAULTS),
            INPUT_SELECTOR = 'input[type=radio]';
    
        var FormFieldRadio = Class({
            instanceMembers: {
                init: function (element, options) {
                    //通过this.base调用父类FormCtrlBase的init方法
                    this.base(element, options);
    
                    var that = this,
                        $element = this.$element;
    
                    //获取所有的input元素
                    var $inputs = this.$inputs = $element.find(INPUT_SELECTOR);
                    //设置它们的name属性,以便能够呈现复选的效果
                    $inputs.prop('name', this.name);
    
                    //设置初始值
                    this.reset();
    
                    //监听input元素的change事件,并最终通过$element的beforeChange和afterChange来管理
                    $element.on('change', INPUT_SELECTOR, function (e) {
                        var val = that.getValue(), event;
    
                        if(val === that.lastValue) return;
    
                        that.trigger((event = $.Event('beforeChange')), val);
                        //判断beforeChange事件有没有被阻止默认行为
                        //如果有则把input的值还原成最后一次修改的值
                        if (event.isDefaultPrevented()) {
                            that.setFieldValue(that.lastValue);
                            return;
                        }
    
                        //记录最新的input的值
                        that.lastValue = val;
                        that.trigger('afterChange', val);
                    });
    
                    this.triggerInit();
                },
                getDefaults: function () {
                    return DEFAULTS;
                },
                _setValue: function (value, trigger) {
                    //只要trigger不等于false,调用setValue的时候都要触发change事件
                    trigger !== false && this.$inputs.eq(0).trigger('change');
                },
                setFieldValue: function (value) {
                    if (value !== '') {
                        this.$inputs.filter('[value="' + value + '"]').prop('checked', true);
                    } else {
                        this.$inputs.filter(':checked').each(function () {
                            this.checked = false;
                        });
                    }
                },
                getValue: function () {
                    return this.$inputs.filter(':checked').val();
                },
                disable: function () {
                    this.$element.addClass('disabled');
                    this.$inputs.prop('disabled', true);
                },
                enable: function () {
                    this.$element.removeClass('disabled');
                    this.$inputs.prop('disabled', false);
                },
                reset: function () {
                    this.setFieldValue(this.initValue);
                    this.lastValue = this.initValue;
                }
            },
            extend: FormCtrlBase,
            staticMembers: {
                DEFAULTS: DEFAULTS
            }
        });
    
        return FormFieldRadio;
    });

    7. formFieldSelect.js

    代码如下:

    define(function (require, exports, module) {
        var $ = require('jquery'),
            FormCtrlBase = require('mod/formFieldBase'),
            Class = require('mod/class'),
            Ajax = require('mod/ajax');
    
        var DEFAULTS = $.extend({}, FormCtrlBase.DEFAULTS, {
            url: '',
            textField: 'text',
            valueField: 'value',
            autoAddEmptyOption: true,
            emptyOptionText: '&nbsp;',
            parseAjax: function (res) {
                if (res.code == 1) {
                    return res.data || [];
                } else {
                    return [];
                }
            }
        });
    
        var FormFieldSelect = Class({
            instanceMembers: {
                init: function (element, options) {
                    //通过this.base调用父类FormCtrlBase的init方法
                    this.base(element, options);
    
                    var opts = this.options, _ajax;
                    if (!opts.url) {
                        //设置初始值
                        this.reset();
                    } else {
                        _ajax = Ajax.get(opts.url);
                    }
    
                    var that = this,
                        $element = this.$element;
    
                    //监听input元素的change事件,并最终通过beforeChange和afterChange来管理
                    $element.on('change', function (e) {
                        var val = that.getValue(), event;
    
                        if (val === that.lastValue) return;
    
                        that.trigger((event = $.Event('beforeChange')), val);
                        //判断beforeChange事件有没有被阻止默认行为
                        //如果有则把input的值还原成最后一次修改的值
                        if (event.isDefaultPrevented()) {
                            that.setFieldValue(that.lastValue);
                            $element.focus();
                            return;
                        }
    
                        //记录最新的input的值
                        that.lastValue = val;
                        that.trigger('afterChange', val);
                    });
    
                    if (!_ajax) {
                        this.triggerInit();
                    } else {
                        _ajax.done(function (res) {
                            var data = opts.parseAjax(res);
                            that.render(data);
                            that.reset();
                            that.triggerInit();
                        })
                    }
                },
                getDefaults: function () {
                    return DEFAULTS;
                },
                _setValue: function (value, trigger) {
                    //只要trigger不等于false,调用setValue的时候都要触发change事件
                    trigger !== false && this.$element.trigger('change');
                },
                render: function (data, clear) {
                    if (Object.prototype.toString.call(data) != '[object Array]') {
                        data = [];
                    }
    
                    var opts = this.options,
                        textField = opts.textField,
                        valueField = opts.valueField,
                        l = data.length,
                        $element = this.$element;
    
                    if(clear === true){
                        $element.html('');
                        this.lastValue = '';
                    }
    
                    if (opts.autoAddEmptyOption) {
                        var o = {};
                        o[textField] = opts.emptyOptionText;
                        o[valueField] = '';
                        //other fileds ?
                        l = data.unshift(o);
                    }
    
                    var html = [];
                    for (var i = 0; i < l; i++) {
                        html.push(['<option value="',
                            data[i][valueField],
                            '">',
                            data[i][textField],
                            '</option>'].join(''));
                    }
    
                    l && $element.append(html.join(''));
                },
                setFieldValue: function (value) {
                    this.$element.val(value.split(','));
                },
                getValue: function () {
                    var value = this.$element.val();
                    if (Object.prototype.toString.call(value) === '[object Array]') {
                        return value.join(',');
                    }
                    return value === null ? '' : value;
                },
                disable: function () {
                    this.$element.addClass('disabled').prop('disabled', true);
                },
                enable: function () {
                    this.$element.removeClass('disabled').prop('disabled', false);
                },
                reset: function () {
                    this.setFieldValue(this.initValue);
                    this.lastValue = this.initValue;
                }
            },
            extend: FormCtrlBase,
            staticMembers: {
                DEFAULTS: DEFAULTS
            }
        });
    
        return FormFieldSelect;
    });

    这个组件功能相对多一点,它还提供了几个额外的option:

    url: 默认是空的,如果有值的话,将在初始化的时候通过该值发起ajax请求加载下拉的数据。
    textField: 只有在url不为空的情况下才会用到,表示ajax返回的数据中哪个字段是用来显示<option>的文本的。
    valueField: 只有在url不为空的情况下才会用到,表示ajax返回的数据中哪个字段是用来显示<option>的value的。
    autoAddEmptyOption: 只有在url不为空的情况下才会用到,表示是否自动添加一个空的option。
    emptyOptionText: 只有在url不为空的情况下才会用到,表示空option的文本。
    parseAjax: 回调,只有在url不为空的情况下才会用到,用来解析ajax返回的数据,需要返回一个数组,存放需要渲染成下拉内容的数据。

    还需要说明的是:
    1)getValue的时候,如果有多个选中的option,它们的值将以英文逗号分隔的形式返回;
    2)setValue的时候,如果要一次性设置多个option的选中状态,得以英文逗号分隔的字符串传值;
    3)它还提供了一个render(data, clear),接收2个参数,第二个参数可选,可以用一份新的数据来替换下拉框的内容,第二个参数如果为true,则会把之前的下拉内容清空。

    实际用法:

    <select class="form-control form-field"
          name="work"
          data-type="select"
          data-default-value=""
          data-value="UI设计">
    <option value="">请选择职业</option>
    <option value="前端开发">前端开发</option>
    <option value="UI设计">UI设计</option>
    <option value="JAVA后端">JAVA后端</option>
    </select>

    构造函数的使用方式与前面的相同,所以不再详细介绍了。

    8. formFieldDate.js

    代码如下:

    define(function (require, exports, module) {
        var $ = require('jquery'),
            FormCtrlBase = require('mod/formFieldBase'),
            hasOwn = Object.prototype.hasOwnProperty,
            Class = require('mod/class');
    
        //引入picker组件
        require('mod/datepicker');
    
        var DEFAULTS = $.extend({}, FormCtrlBase.DEFAULTS),
            DATEPICKER_DEFAULTS = $.extend($.fn.datepicker.defaults, {
                autoclose: true,
                language: 'zh-CN',
                format: 'yyyy-mm-dd',
                todayHighlight: true
            });
    
        function getPickerOptions(options) {
            var opts = {};
            for (var i in DATEPICKER_DEFAULTS) {
                if (hasOwn.call(DATEPICKER_DEFAULTS, i) && (i in options)) {
                    opts[i] = options[i];
                }
            }
            return opts;
        }
    
        var FormFieldDate = Class({
            instanceMembers: {
                init: function (element, options) {
                    //通过this.base调用父类FormCtrlBase的init方法
                    this.base(element, options);
                    //设置初始值
                    this.reset();
    
                    //pickerOptions是datepick组件需要的
                    this.pickerOptions = getPickerOptions(this.options);
    
                    var that = this,
                        $element = this.$element;
    
                    //监听input元素的change事件,并最终通过beforeChange和afterChange来管理
                    $element.on('change', function (e) {
                        var val = that.getValue(), event;
    
                        if(val === that.lastValue) return;
    
                        that.trigger((event = $.Event('beforeChange')), val);
                        //判断beforeChange事件有没有被阻止默认行为
                        //如果有则把input的值还原成最后一次修改的值
                        if (event.isDefaultPrevented()) {
                            that.setFieldValue(that.lastValue);
                            return;
                        }
    
                        //记录最新的input的值
                        that.lastValue = val;
                        that.trigger('afterChange', val);
                    });
    
                    //初始化datepicker组件
                    $element.datepicker(this.pickerOptions);
    
                    this.triggerInit();
                },
                getDefaults: function () {
                    return DEFAULTS;
                },
                _setValue: function (value, trigger) {
                    //只要trigger不等于false,调用setValue的时候都要触发change事件
                    trigger !== false && this.$element.trigger('change');
                },
                setFieldValue: function (value) {
                    this.$element.val(value).datepicker('update').blur();
                },
                getValue: function () {
                    return this.$element.val();
                },
                disable: function () {
                    //datapicker组件没有disable的方法
                    //所以禁用和启用只能通过destroy后重新初始化来实现
                    this.$element.addClass('disabled').prop('readonly', true).datepicker('destroy');
                },
                enable: function () {
                    this.$element.removeClass('disabled').prop('readonly', false).datepicker(this.pickerOptions);
                },
                reset: function () {
                    this.setFieldValue(this.initValue);
                    this.lastValue = this.initValue;
                }
            },
            extend: FormCtrlBase,
            staticMembers: {
                DEFAULTS: DEFAULTS
            }
        });
    
        return FormFieldDate;
    });

    这个组件跟formFieldText没有太多区别,需要说明的是:

    1)它依赖了bootstrap-datetimepicker这个插件,来实现日期选择的效果。如果想替换成其它的插件来实现日期选择,需要改这部分的源码;
    2)它依赖的日期插件仅仅是起到辅助录入的作用,不影响formFieldBase定义的那些基本的属性和行为。

    实际使用的时候,只要把Input元素的data-type指定为date即可:

    <input class="form-control form-field"
         name="birthday"
         data-type="date"
         data-default-value=""
         type="text"
         placeholder=""
         readonly
         value="2000-01-01">

    9. formFieldUeditor.js

    代码如下:

    define(function (require, exports, module) {
        var $ = require('jquery'),
            FormCtrlBase = require('mod/formFieldBase'),
            Class = require('mod/class');
    
        var DEFAULTS = $.extend({
            height: 400,
            ueConfig: {}
        }, FormCtrlBase.DEFAULTS);
    
        var FormFieldUeditor = Class({
            instanceMembers: {
                init: function (element, options) {
                    //通过this.base调用父类FormCtrlBase的init方法
                    this.base(element, options);
    
                    var that = this,
                        $element = this.$element,
                        opts = this.options;
    
                    //监听input元素的change事件,并最终通过beforeChange和afterChange来管理
                    $element.on('change', function (e) {
                        var val = that.getValue(), event;
    
                        if (val === that.lastValue) return;
    
                        that.trigger((event = $.Event('beforeChange')), val);
                        //判断beforeChange事件有没有被阻止默认行为
                        //如果有则把input的值还原成最后一次修改的值
                        if (event.isDefaultPrevented()) {
                            that.setFieldValue(that.lastValue);
                            $element.focus().select();
                            return;
                        }
    
                        //记录最新的input的值
                        that.lastValue = val;
                        that.trigger('afterChange', val);
                    });
    
                    var editorId = this.name + '-editor',
                        editorName = this.name + '-editor-text',
                        ueScript = [
                            '<script id="'
                            , editorId
                            , '" name="'
                            , editorName
                            , '" type="text/plain" style="100%;height:'
                            , opts.height
                            , 'px;">'
                            , '</script>'
                        ].join('');
    
                    this.$element.before(ueScript);
    
                    //初始化UE组件
                    this.ue = UE.getEditor(editorId, opts.ueConfig);
    
                    this.ue && this.ue.ready(function () {
                        that._ueReady = true;
    
                        /*//粘贴时只粘贴文本
                         that.ue.execCommand('pasteplain');
    
                         //粘贴后再次做格式清理
                         that.ue.addListener('afterpaste', function (t, arg) {
                         that.ue.execCommand('autotypeset');
                         });*/
    
                        //编辑器文本变化
                        that.subscribeUeContentChange();
    
                        //设置初始值
                        that.reset();
    
                        that.triggerInit();
                    });
    
                },
                subscribeUeContentChange: function () {
                    var editor = this.ue,
                        $element = this.$element;
    
                    this._ueContentChange = function () {
                        $element.val(editor.getContent()).trigger('change');
                    };
    
                    editor.addListener('contentChange', this._ueContentChange);
                },
                offUeContentChange: function offUeContentChange() {
                    var editor = this.ue;
                    editor.removeListener('contentChange', this._ueContentChange);
                    this._ueContentChange = undefined;
                },
                getDefaults: function () {
                    return DEFAULTS;
                },
                _setValue: function (value, trigger) {
                    //只要trigger不等于false,调用setValue的时候都要触发change事件
                    trigger !== false && this.$element.trigger('change');
                },
                setFieldValue: function (value) {
                    var elementDom = this.$element[0],
                        v = ' ' + value;
    
                    elementDom.value = v;
                    elementDom.value = v.substring(1);
    
                    var ue = this.ue;
                    if (ue && this._ueReady) {
                        this.offUeContentChange();
                        ue.setContent(value);
                        this.subscribeUeContentChange();
                    }
                },
                getValue: function () {
                    return this.$element.val();
                },
                disable: function () {
                    this.ue && this._ueReady && this.ue.setDisabled();
                },
                enable: function () {
                    this.ue && this._ueReady && this.ue.setDisabled();
                },
                reset: function () {
                    this.setFieldValue(this.initValue);
                    this.lastValue = this.initValue;
                }
            },
            extend: FormCtrlBase,
            staticMembers: {
                DEFAULTS: DEFAULTS
            }
        });
    
        return FormFieldUeditor;
    });

    之所以写这个完全是为了简化ueditor的使用,就像前面说的,ueditor不改变表单元素在表单开发中的本质,仅仅是辅助录入的作用,实现起来也没什么难度,只要想办法实现setValue getValue enable disabled reset这些重要的api方法即可,ueditor只是个插件,在init方法内找个合适的位置做一下初始化就好了。

    实际使用:

    <textarea class="form-control form-field"
              name="detailDesc"
              data-type="ueditor"
              data-default-value=""
              rows="3"
              placeholder=""><p>I'm felix.</p></textarea>

    注意data-type。

    10. 文中小结

    以上部分已经把formFieldBase以及现有的各个表单元素组件都介绍完了,但是在实际使用过程中,如果我们每个元素都要手动调用构造函数去初始化的话,那就太麻烦了, 这远不是本文想要起到的作用,所以为了简化最终的表单数据设置和收集的功能,我还另外写了两个组件:formFieldMap,这就是个映射表;formMap,这是个容器组件,管理内部所有的表单元素组件实例。在实际需求中,一般只要用到formMap即可。

    11. formFieldMap

    这个仅仅是为了formMap服务的,因为formMap会根据某个规则找到所有的待初始化的元素,然后根据元素的data-type属性,再根据formFieldMap来找到具体的构造函数:

    define(function (require, exports, module) {
        var FormFieldText = require('mod/formFieldText'),
            FormFieldCheckbox = require('mod/formFieldCheckbox'),
            FormFieldRadio = require('mod/formFieldRadio'),
            FormFieldSelect = require('mod/formFieldSelect'),
            FormFieldDate = require('mod/formFieldDate'),
            FormFieldUeditor = require('mod/formFieldUeditor');
    
        return {
            checkbox: FormFieldCheckbox,
            date: FormFieldDate,
            radio: FormFieldRadio,
            text: FormFieldText,
            select: FormFieldSelect,
            ueditor: FormFieldUeditor
        }
    });

    12. formMap

    代码如下:

    define(function (require, exports, module) {
    
        var $ = require('jquery'),
            FormFieldMap = require('mod/formFieldMap'),
            Class = require('mod/class'),
            hasOwn = Object.prototype.hasOwnProperty,
            DEFAULTS = {
                mode: 1, //跟FormFieldBase一致
                fieldSelector: '.form-field', //用来获取要初始化的表单元素
                fieldOptions: {} //表单组件的option
            };
    
        var FormMap = Class({
            instanceMembers: {
                init: function (element, options) {
                    var $element = this.$element = $(element),
                        opts = this.options = this.getOptions(options);
    
                    //存储所有的组件实例
                    this.cache = {};
    
                    var that = this;
                    //初始化所有需要被FormMap管理的组件
                    $element.find(opts.fieldSelector).each(function () {
                        var $field = $(this);
    
                        //要求各个表单元素必须得有name或者data-name属性,否则fieldOptions起不到作用
                        that.add($field, $.extend({
                            mode: opts.mode
                        }, opts.fieldOptions[$field.attr('name') || $field.data('name')] || {}));
                    });
                },
                getOptions: function (options) {
                    var defaults = this.getDefaults(),
                        _opts = $.extend({}, defaults, this.$element.data() || {}, options),
                        opts = {};
    
                    //保证返回的对象内容项始终与当前类定义的DEFAULTS的内容项保持一致
                    for (var i in defaults) {
                        if (hasOwn.call(defaults, i)) {
                            opts[i] = _opts[i];
                        }
                    }
    
                    return opts;
                },
                getDefaults: function () {
                    return DEFAULTS;
                },
                add: function ($field, fieldOption) {
                    //要求要被FormMap管理的组件必须有data-type属性
                    var type = $field.data('type');
    
                    if (!(type in FormFieldMap)) return;
    
                    var formField = new FormFieldMap[type]($field, fieldOption || {});
    
                    this.cache[formField.name] = {
                        formField: formField,
                        fieldName: formField.name
                    };
                },
                get: function (name) {
                    var field = this.cache[$.trim(name)];
                    return field && field.formField;
                },
                remove: function (name) {
                    var formField = this.get(name);
                    if (formField) {
                        delete this.cache[name];
                        formField.destroy();
                    }
                },
                reset: function () {
                    var cache = this.cache;
                    for (var i in cache) {
                        if (hasOwn.call(cache, i)) {
                            cache[i].formField.reset();
                        }
                    }
                },
                getData: function () {
                    var cache = this.cache,
                        data = {};
    
                    for (var i in cache) {
                        if (hasOwn.call(cache, i)) {
                            data[cache[i].fieldName] = cache[i].formField.getValue();
                        }
                    }
    
                    return data;
                },
                setData: function (data, trigger) {
                    if (Object.prototype.toString.call(data) !== '[object Object]') return;
    
                    var cache = this.cache;
    
                    for (var i in cache) {
                        if (hasOwn.call(cache, i) && (i in data)) {
                            cache[i].formField.setValue(data[i], trigger);
                        }
                    }
                }
            },
            staticMembers: {
                DEFAULTS: DEFAULTS
            }
        });
    
        return FormMap;
    });

    它提供了三个option:

    mode: 跟formFieldBase的mode作用是一样的,只不过因为formMap是作用于form元素上的,所以它的mode属性相当于是全局的,会对所有的表单元素都起作用;
    fieldSelector: 用来过滤需要被初始化的表单元素的选择器,默认是.form-field,只要一个元素上有这个class,就会被这个容器管理,通常保留默认值即可;
    fieldOptions:可以通过它传递各个表单元素的各自的option。

    使用举例:

    appForm = new Form('#appForm', {
        mode: Url.getParam('mode'),
        fieldOptions: {
            name: {
                onInit: function(){
                    
                }
            },
            work: {
                onAfterChange: function (e, val) {
                    if(val == 'xxx') {
                        
                    }
                }
            }
        }
    });

    它还提供了以下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校验这一块的内容,已经有成果了,只是要等下周六日才会有时间来总结,请再关注。

  • 相关阅读:
    13.6 线程通信
    13.5 线程同步
    13.4 控制线程
    13.3 线程的生命周期
    13.2 线程的创建与启动
    13.1 线程概述
    12.10 NIO.2的功能和用法
    bs4
    mysql基本命令
    HDU-1021 Fibonacci Again
  • 原文地址:https://www.cnblogs.com/lyzg/p/5467691.html
Copyright © 2020-2023  润新知