supce's blog

表单工厂


任务目的

  • 加强对JavaScript的掌握
  • 熟悉常用表单处理逻辑
  • 学习如何模块如何设计,不同模块间如何尽量解耦

任务描述

  • 实现以JavaScript对象的方式定义表单及验证规则
  • 表单配置参考示例如下:(不需要一致,仅为参考)

    {

    label: '名称',                    // 表单标签
    type: 'input',                   // 表单类型
    validator: function () {...},    // 表单验证规
    rules: '必填,长度为4-16个字符',    // 填写规则提示
    success: '格式正确',              // 验证通过提示
    fail: '名称不能为空'               // 验证失败提示
    

    }

  • 基于该配置项,实现一套逻辑,可以自动生成表单的展现、交互、验证
  • 使用你制作的表单工厂,在一个页面上创建两套样式不同的表单

整体思路

常见的控件类型一般有文本框,单选框,复选框,下拉菜单和文本域。我们可以让用户生成这5种控件,并且添加基本的配置以及自动生成验证规则等。

首先准备好HTML框架,HTML分为两部分,一部分是表单的生成配置部分,另一部分用来存放生成的表单。

第一部分:

<form id="data_create">
    <fieldset id="type_box" class="input">
        <legend>控件类型</legend>
        <select id="widget">
            <option value="input">文本框</option>
            <option value="radio">单选框</option>
            <option value="checkbox">复选框</option>
            <option value="select">下拉菜单</option>
            <option value="textarea">文本域</option>
        </select>
    </fieldset>
    <fieldset id="basic_box" class="necessary">
        <legend>控件配置</legend>
        <label>名称:</label>
        <input type="text" id="label_box" value="input">
        <select id="necessary">
            <option value="necessary">必填</option>
            <option value="unnecessary">选填</option>
        </select>
        <p>
            <label>样式:</label>
            <select id="style_box">
                <option value="style_one">样式一</option>
                <option value="style_two">样式二</option>
            </select>
        </p>
    </fieldset>
    <fieldset id="rule_input" class="text">
        <legend>检验规则</legend>
        <select>
            <option value="text">文本</option>
            <option value="password">密码</option>
            <option value="number">数字</option>
            <option value="email">邮箱</option>
            <option value="phone">电话</option>
        </select>
    </fieldset>
    <fieldset id="length_control">
        <legend>长度</legend>
        <label>字符长度:</label>
        <input id="min_length" type="number" min="0" value="4" class="numInput">
        <span>——</span>
        <input id="max_length" type="number" min="1" value="16" class="numInput">
    </fieldset>
    <fieldset id="box_item">
        <legend>选项</legend>
        <input id="box_item_input" placeholder="可用空格,逗号,回车来分隔选项" />
        <span id="box_item_show"></span>
    </fieldset>
    <button id="btn_add" type="button">提交</button>
</form>

第二部分:

<div id="form_box">
    <h1 id="title">表单展示区</h1>
    <form id="result" class="style_one">
        <button type="button" id="submit_form">提交</button>
        <input type="text" class="hide">
    </form>
</div>

在生成单选框,复选框,下拉菜单时,每一个选项的添加利用了之前Tag输入的功能。即实现了一个tag输入框,用户输入空格,逗号,回车时,都自动把当前输入的内容作为一个tag放在输入框下面,遇到重复输入的Tag,自动忽视。

我们把这部分功能封装为showTagModel

function ShowTag(ipt,box){
    this.arr = [];  //数据存放数组
    this.ipt = ipt; //选项输入框
    this.box = box; //显示tag的容器
    this.length = 20; //option的数量
}
ShowTag.prototype = {
    getData : function(){
        return this.arr;
    },
    //显示标签
    show : function(){
        var text = '';
        for (var index = 0; index < this.arr.length; index++) {
            text += '<div data-num="' + index + '" class="item"><span>点击删除</span>' + this.arr[index] + '</div>';
        }
        this.box.innerHTML = text;
        return this;
    },
    //去重和限制长度
    trim : function(){
        var i,j;
        for(i=0;i<this.arr.length;i++){
            for(j=i+1;j<this.arr.length;j++){
                if(this.arr[i] == this.arr[j]){
                    this.arr.splice(j,1);
                    j--;
                }
            }
        }
        while(this.arr.length > this.length){
            this.arr.shift();
        }
        this.show();
        return this;
    },
    //添加数据
    add : function(){
        var strs = this.ipt.value.split(/[ ,、, \n\t]/);
        for(var i=0;i<strs.length;i++){
            var item = strs[i];
            if(item != ""){
                this.arr.push(item);
            }
        }
        this.trim();
        return this;
    },
    //点击删除数据
    deleteEvent : function(e){
        //点击span或者点击span内的文本
        var item = e.target.className == 'item' ? e.target : e.target.parentNode.className == 'item' ? e.target.parentNode : null;
        if (item == null) {
            return 0;
        }
        //删除第n个元素,之后重新显示元素
        this.arr.splice(item.getAttribute('data-num'), 1);
        this.show();
    }
};
//input对象
function TagIpt(tag_ipt,tag_box){
    ShowTag.call(this,tag_ipt,tag_box);
}
//建立一个由ShowTag.prototype继承而来的TagIpt.prototype对象.
TagIpt.prototype = Object.create(ShowTag.prototype);
//设置构造函数为ShowTag
TagIpt.constructor = ShowTag;
TagIpt.prototype.init=function() {
    //绑定事件
    addHandler(this.box, 'click', this.deleteEvent.bind(this));//删除元素事件的绑定
    addHandler(this.ipt, 'keyup', this.keyUp.bind(this));    //输入框输入内容事件的绑定
    addHandler(this.ipt, 'keydown', this.preventDefault);    //阻止输入框的默认事件
};
TagIpt.prototype.keyUp=function(e) {
    if (e.keyCode == 188 || e.keyCode == 32 || e.keyCode == '13') {
        this.add();
        this.ipt.value = '';
    }
};
TagIpt.prototype.preventDefault=function(e) {
    if (e.keyCode == '13') {
        e.preventDefault ? e.preventDefault() : e.returnValue = false;
    }
};

由于要从页面中获取很多数据,这里用一个对象把需要的控件封装,集成再在同一个对象中,避免频繁对页面的某个元素频繁重复的获取。

这里把页面用的元素封装再dataModel中

//存放需要的id节点
var data_box = {
    type_box : {             //控件类型             
        box: $("#type_box"), 
        value: "className"   //获取方式
    },
    label_box : {            //控件名称
        box : $("#label_box"),
        value: "value"
    },
    necessary_box : {        //控件是否必填
        box : $("#basic_box"),
        value : "className"
    },
    style_box : {            //控件样式
        box : $("#style_box"),
        value : "value"
    },
    input_type_box : {       //input类型
        box : $("#rule_input"),
        value : "className"
    },
    min_length_box : {       //控件字符最短长度
        box : $("#min_length"),
        value : "value"
    },
    max_length_box : {       //控件字符最长长度
        box : $("#max_length"),
        value : "value"
    },
    item_box : [
        $("#box_item_input"),
        $('#box_item_show'),  //选项展示区
        document.getElementsByClassName('item') //获取所有node节点
    ],
    add_btn : $("#btn_add"),  //添加控件按钮
    result_box : $("#result"),//控件展示区
    submit_form : $("#submit_form")  //展示区的提交按钮
};

下一步就可以建立一个表单工厂,通过data_box获取相应的数据,并且根据用户输入的信息建立表单控件。在表单工厂中,首先是初始化部分,建立配置选项和类名的对应关系,为下一步获取页面配置数据做准备

//数据工厂
function Data_factory(data_box){
    this.box = data_box;
    this.id = 0;
}
Data_factory.prototype = {
    init : function(){
        this.addEvent();  //初始化给form绑定事件
    },
    addEvent : function(){
        addHandler($("#data_create"),"change",this.changeClass.bind(this));  //下拉框内容改变时更改对应元素的类名
        addHandler(this.box.style_box.box,"change",this.changeStyle.bind(this)); //样式改变时修改控件样式
    },
    changeClass : function(e){
        var box = e.target;
        if(box.type == "select-one"){
            var value = box.options[box.selectedIndex].value;
            box.parentNode.className = value;
            if(!/necessary/.test(box.id)){
                this.box.label_box.box.value = value;  //改变控件的名字
            }
        }
    },
    changeStyle : function(){
        var style = this.getText(this.box.style_box);
        this.box.result_box.className = style;
    },

然后是数据获取,在获取数据时,先获取基本的信息,比如控件类型和控件名称,然后根据类型再获取其他所需要的数据。因为不同的控件需要的数据是不同的

getText : function(data_box){
        return data_box.box[data_box.value];  //根据不同的属性获取不同的值
    },
    getData : function(){      //获取页面数据
        var data = {
            type : '',        //控件类型
            label : '',
            necessary : true,
            input_type : '',  //input的类型 text number password...
            min_length : 0,
            max_length : 1,
            item : [],        //存放选项的数组
            id : 0,           //控件id
            default_text : '',   //默认提示
            success_text : '',   //成功提示
            fail_text : '',      //失败提示
            validator : function(){}  //检验规则
        };
        //获取基础数据
        data = this.getBaseData(data);
        //根据类型完善其他数据
        switch(data.type){
            case 'input':
                switch(data.input_type){
                    case 'text':
                    case 'password':
                        data = this.getLengthData(data);
                        break;
                    case 'number':
                    case 'email':
                    case 'phone':
                        data = this.getIptData(data);
                        break;
                }   
                break;
            case 'textarea':
                data = this.getLengthData(data);
                break;
            case 'radio':
            case 'checkbox':
            case 'select':
                data = this.getItemData(data);
                break;
        }
        return data;
    },
    getBaseData : function(data){
        data.type = this.getText(this.box.type_box);
        data.label = this.getText(this.box.label_box);
        data.necessary = this.getText(this.box.necessary_box)  == "necessary";
        data.input_type = this.getText(this.box.input_type_box);
        data.id = "form" + this.id++;
        return data;
    },
    //补充text password和textarea的信息
    getLengthData : function(data){ 
        data.min_length = this.getText(this.box.min_length_box);
        data.max_length = this.getText(this.box.max_length_box);
        data.validator = validator.length_control;
        data.default_text = '长度为' + data.min_length + '——' + data.max_length + '个字符,' + (data.necessary?"必填":"选填");
        data.success_text = data.label + '格式正确';
        data.fail_text = [
            data.label + '不能为空',
            data.label + '长度不能小于' + data.min_length + '个字符',
            data.label + '长度不能大于' + data.max_length + '个字符'
        ];
        return data;
    },
    //补充numer emai 和phone的信息
    getIptData : function(data){
        data.input_type = this.getText(this.box.input_type_box);
        data.validator = validator[data.input_type];
        data.default_text = '请输入' + data.label + (data.necessary?"必填":"选填");
        data.success_text = data.label + '格式正确';
        data.fail_text = [
            data.label + '不能为空',
            data.label + '格式不正确'
        ];
        return data;
    },
    //补充radio checkbox 和select的信息
    getItemData : function(data){
        var items = this.box.item_box[2] //获取所有选项节点
        data.item = []; //清空数据
        for(var i=0;i<items.length;i++){
            data.item.push(items[i].childNodes[1].data);
        }
        if(data.item.length == 0){
            alert('你还没有添加' + data.label + '的选项');
            data = null;
        }else if(data.item.length == 1){
            alert('你只添加了一个选项,无法创建' + data.label);
            data = null;
        }else{
            data.default_text = '请选择' + data.label + (data.necessary?" 必填":"选填");
            data.success_text = data.label + '已选择';
            data.fail_text = [data.label + '未选择'];
        }
        return data;
    },

有了数据,就可以向页面插入控件了

//根据数据生成控件
addWidget : function(data){
    switch(data.type){
        case 'input':
            this.addIptWidget(data);
            break;
        case 'radio':
            this.addRadioWidget(data);
            break;
        case 'checkbox':
            this.addCheckWidget(data);
            break;
        case 'select':
            this.addSelectWidget(data);
            break;
        case 'textarea':
            this.addAreaWidget(data);
            break;
    }
},
addIptWidget : function(data){
    var div = document.createElement("div");
    div.innerHTML = '<label>' + data.label + ':</label>' + '<input type="' + data.input_type + '" id="' + data.id + '"><span></span>';
    this.box.result_box.insertBefore(div,this.box.submit_form);
},
addRadioWidget : function(data){
    var div = document.createElement("div"),text = "";
    div.className = "radio_box";
    text += '<div id="' + data.id + '"><label className="widgetNameLabel" >' + data.label + ':</label>';
    for (var i = 0; i < data.item.length; i++) {
        var id = data.id + '' + i;
        text += '<input type="radio" id="' + id + '" name="' + data.id + '"><label for="' + id + '">' + data.item[i] + '</label>';
    }
    text += '</div><span></span>';
    div.innerHTML = text;
    this.box.result_box.insertBefore(div,this.box.submit_form);
},
addCheckWidget : function(data){
    var div = document.createElement("div"),text = "";
    div.className = "radio_box";
    text += '<div id="' + data.id + '"><label className="widgetNameLabel" >' + data.label + ':</label>';
    for (var i = 0; i < data.item.length; i++) {
        var id = data.id + '' + i;
        text += '<input type="checkbox" id="' + id + '" name="' + data.id + '"><label for="' + id + '">' + data.item[i] + '</label>';
    }
    text += '</div><span></span>';
    div.innerHTML = text;
    this.box.result_box.insertBefore(div,this.box.submit_form);
},
addSelectWidget : function(data){
    var div = document.createElement("div"),text = "";
    text += '<label>' + data.label + ':</label><select id="' + data.id + '" >';
    for(var i=0;i<data.item.length;i++){
        text += '<option>' + data.item[i] + '</option>';
    }
    text += '</select><span></span>';
    div.innerHTML = text;
    this.box.result_box.insertBefore(div,this.box.submit_form);
},
addAreaWidget : function(data){
    var div = document.createElement('div');
    div.innerHTML = '<label>' + data.label + ':</label><textarea id="' + data.id + '"></textarea><span></span>';
    this.box.result_box.insertBefore(div, this.box.submit_form);
}

控件工厂完成了,但是还需要给生成的表单设置相应的检验事件,用于提示用户,可以把这部分功能写在initForm中,Form用于事件的绑定,validator对象用于存放所有的检验规则。

//初始化表单验证
function Form(data){
    this.data = data;
    console.log(data.id);
    this.ipt = document.getElementById(data.id);
    console.log(this.ipt);
    this.tip = this.ipt.nextElementSibling;
    this.validator = data.validator;
    this.init();
}
Form.prototype = {
    init : function(){
        addHandler(this.ipt,"focus",this.default_tip.bind(this));
        addHandler(this.ipt,"blur",this.validator.bind(this));
        addHandler(this.ipt,"change",this.validator.bind(this));
    },
    default_tip : function(){
        this.tip.innerHTML = this.data.default_text;
        this.tip.className = "default";
        // this.ipt.className = "default";
    },
    true_tip : function(){
        this.tip.innerHTML = this.data.success_text;
        this.tip.className = "true";
        // this.ipt.className = "true";
    },
    error_tip : function(i){
        this.tip.innerHTML = this.data.fail_text[i];
        this.tip.className = "error";
        // this.ipt.className = "error";
    }
};
var validator = {
     //text password textarea
    'length_control': function () {
        min_length = this.data.min_length;
        max_length = this.data.max_length;
        var text = this.ipt.value;
        if (text == '') {
            if (this.data.necessary)
                this.error_tip(0);
            else {
                this.default_tip();
                return true;
            }
        }
        else {
            var total = (/[\x00-\xff]/.test(text) ? text.match(/[\x00-\xff]/g).length : 0) + (/[^\x00-\xff]/.test(text) ? text.match(/[^\x00-\xff]/g).length * 2 : 0);
            if (total < min_length) {
                this.error_tip(1);
            }
            else if (total > max_length) {
                this.error_tip(2);
            }
            else {
                this.true_tip();
                return true;
            }
        }
        return false;
    },
    'number': function () {
        var text = this.ipt.value;
        if (text == '') {
            if (this.data.necessary)
                this.error_tip(0);
            else {
                this.default_tip();
                return true;
            }
        }
        else {
            if (/^\d*$/.test(text)) {
                this.true_tip();
                return true;
            }
            else {
                this.error_tip(1);
            }
        }
        return false;
    },
    'email': function () {
        var text = this.ipt.value;
        if (text == '') {
            if (this.data.necessary)
                this.error_tip(0);
            else {
                this.default_tip();
                return true;
            }
        }
        else {
            if (/^[0-9a-z]+([._\\-]*[a-z0-9])*@([a-z0-9]+[a-z0-9]+.){1,63}[a-z0-9]+$/.test(text)) {
                this.true_tip();
                return true;
            }
            else {
                this.error_tip(1);
            }
        }
        return false;
    },
    'phone': function () {
        var text = this.ipt.value;
        if (text == '') {
            if (this.data.necessary)
                this.error_tip(0);
            else {
                this.default_tip();
                return true;
            }
        }
        else {
            if (/^1[34578]\d{9}$/.test(text)) {
                this.true_tip();
                return true;
            }
            else {
                this.error_tip(1);
            }
        }
        return false;
    },
    'radio': function () {
        var item = $('#' + this.data.id).getElementsByTagName('input');
        for (var i = 0; i < item.length; i++) {
            if (item[i].checked) {
                this.true_tip();
                return true;
            }
        }
        if (this.data.necessary)
            this.error_tip(0);
        else {
            this.default_tip();
            return true;
        }
        return false;
    },
    'checkbox': function () {
        var children = this.ipt.children;
        for (var i in children) {
            if (children[i].checked) {
                this.true_tip();
                return true;
            }
        }
        if (this.data.necessary)
            this.error_tip(0);
        else {
            this.default_tip();
            return true;
        }
        return false;
    },
    'select': function () {
        this.true_tip();
        return true;
    }
}

最后,直接调用,给页面两个按钮绑定事件。

var data_factory = new Data_factory(data_box),
    tagIpt = new TagIpt(data_box.item_box[0],data_box.item_box[1]),
    widgetArr = [];
data_factory.init();
tagIpt.init();
//绑定添加控件事件
addHandler(data_factory.box.add_btn,"click",function(){
    var data = data_factory.getData();
    if(data != null){
        //向表单中添加相应的控件
        data_factory.addWidget(data);
        //绑定验证函数并放入数组中
        widgetArr.push(new Form(data));
        //在控件为radio和checkbox时直接展示默认的提示
        if (data.type == 'radio' || data.type == 'checkbox') {
            widgetArr[widgetArr.length - 1].default_tip();
        }
    }
});
//给表单提交按钮绑定检验事件
addHandler(data_box.submit_form,"click",function(){
    var text = "";
    for(var i=0;i<widgetArr.length;i++){
        text += !widgetArr[i].validator() ? widgetArr[i].tip.textContent + '\n' : "";
    }
    if(text == ""){
        alert("提交成功");
    }else{
        alert(text);
    }
});

完整代码

完整代码
demo