簡潔易用的表單數據設置和收集管理組件

這篇文章要分享的是我在作表單界面開發的一部分經驗,關於表單數據設置和收集這一塊的。總體而言,這篇文章總結的東西有如下的特色:
1)api簡單,使用起來很容易;
2)簡化了表單新增和編輯,可讓新增和編輯使用同一個表單頁面;
3)基本上與UI分離,因此很容易應用到各種項目的開發當中。
涉及到的組件不止一個,並且未來還會擴充,這些組件都是根據之前的工做經驗開發出來的,沒有很高級的東西,每一個組件的代碼都不多,因此即便文中有介紹不到的地方,你也能經過閱讀代碼來詳細瞭解。不過我想大部分人應該沒有見過這樣的使用方式(除了我畢業的時候進的那家公司的同事),我上家公司的朋友剛開始看到我用這種寫法的時候都不太理解,可是你們最後都接受並承認了這種用法,由於在開發的時候效率確實還挺高的,這也是我寫這篇文章分享出來的目的。javascript

本文相關的代碼我都放在github上面去了,原來我都直接上傳在博客園,後來發現有的時候要改點東西每次都得從新上傳,挺不方便的,仍是直接git簡單點,另外git還能夠經過gh-pages分支來顯示靜態內容,正好能夠用來查看demo。php

代碼地址:
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=2css

關於demo的簡單說明:html

這兩個地址分別用來模擬了一個表單頁面的新增和編輯時的場景,我用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中生日這個表單元素就是一個很好的說明,它雖然用到了日期選擇的插件,可是即便沒有這個插件,也不會影響到文本框值的存取。我把這個說明出來實際上是想表達,在表單數據收集或設置的時候,應該考慮一下分離的思想,只有這樣寫出來的組件纔可以不受項目的影響。關於這部分的思想,我推薦一篇更好的文章,感興趣的能夠深刻閱讀:java

順勢而爲,HTML發展與UI組件設計進化jquery

demo相關的html文件是src/html/demo1.html,js文件是src/js/app/demo1.js。整個項目用了seajs作模塊化,用了gulp來作簡單構建,還用到之前的幾篇博客總結的一些東西:git

1)詳解Javascript的繼承實現:提供一個class.js,用來定義javascript的類和構建類的繼承關係;
2)jquery技巧之讓任何組件都支持相似DOM的事件管理:提供一個eventBase.js,用來給任意組件實例提供相似DOM的事件管理功能。github

在src/js/app/demo1.js中你能夠看到,demo這個表單,在點擊保存,收集數據的時候是多麼的簡單:ajax

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="width: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校驗這一塊的內容,已經有成果了,只是要等下週六日纔會有時間來總結,請再關注。

相關文章
相關標籤/搜索