封装一个动态表单吧

经常做后台的小伙伴儿可能经常要遭遇各种表单结构,input,select,时间、日期插件等等,各种属性配置到吐,今天就带大家来封装一个可复用表单。表单参考了网上的一些例子,再结合自己的需求,封装了一个自己的业务表单组件。
先上图:


QQ截图20201118163834.png

首先给大家看一下结构布局:
我们现实放了一个form表单。然后利用el-form-item来循环遍历,给每一个el-form-item设置动态label,动态rules(校验规则)以及相对应的prop。
然后我们给每一种类型的表单元素添加动态disabled的属性,控制可编辑状态。
给属性绑定动态表单值,并且添加placeholder提示语;
对于select,checkbox等多值域绑定的表单元素,配置项中提供options属性。
然后我们的图片上传用的是我们之前封装的图片上传组件,传递一个绑定值和一个可编辑状态即可。
最后我们给每个表单元素添加了一个tooltip,用于提示用户每一个表单元素要填写的内容或者提示语。对于每一个label标签,我们也做了最基本的处理,大于四个字我们就给变成4字加省略号,通过tooltip来查看标签名称。

<template>
    <div>
        <el-form :label-position="labelPosition" :inline="inline" :label-width="labelWidth" :model="myForm" ref="customForm">
          <el-form-item v-for="(item,index) in formData" :label="returnLable(item.label)+':'" :rules="item.rules" :key="index" :prop="item.prop">
                <el-tooltip v-if="item.type == 'input'" :effect="effect" :content="'请填写'+item.label" placement="top">
                    <!-- prefix-icon 首部图标--><!-- suffix-icon 尾部图标-->
                    <el-input 
                        v-model="myForm[item.prop]" clearable :suffix-icon="item.sufIcon" :show-password ="item.password"
                        :prefix-icon="item.preIcon" :placeholder="'选择'+item.label" :disabled="item.disabled">
                    </el-input>
                </el-tooltip>
                <el-tooltip v-if="item.type == 'select'" :effect="effect" :content="'请选择'+item.label" placement="top">
                    <el-select v-model="myForm[item.prop]" filterable clearable :multiple="item.multiple" filterable :placeholder="'选择'+item.label" :disabled="item.disabled">
                        <el-option v-for="o in item.options" :key="o.value" :label="o.label" :value="o.value">
                        </el-option>
                    </el-select>
                </el-tooltip>
                <el-tooltip v-if="item.type == 'radio'" :effect="effect" :content="'请选择'+item.label" placement="top">
                    <el-radio-group v-model="myForm[item.prop]" :disabled="item.disabled">
                        <el-radio v-for="o in item.options" :label="o.value" :placeholder="'选择'+item.label">{{o.label}}</el-radio>
                    </el-radio-group>
                </el-tooltip>
                <el-tooltip v-if="item.type == 'checkbox'" :effect="effect" :content="'请选择'+item.label" placement="top">
                    <el-checkbox-group v-model="myForm[item.prop]" :disabled="item.disabled">
                        <el-checkbox v-for="o in item.options" :label="o.value" :placeholder="'选择'+item.label">{{o.label}}</el-checkbox>
                    </el-checkbox-group>
                </el-tooltip>
                <el-tooltip v-if="item.type == 'date'" :effect="effect" :content="'请选择'+item.label" placement="top">
                    <el-date-picker
                        v-model="myForm[item.prop]" type="date" 
                        value-format="yyyy-MM-dd" 
                        :picker-options="pickerOptions" 
                        :placeholder="'选择'+item.label" 
                        :disabled="item.disabled">
                    </el-date-picker>
                </el-tooltip>
                <el-tooltip v-if="item.type == 'datetime'" :effect="effect" :content="'请选择'+item.label" placement="top">
                    <el-date-picker
                        v-model="myForm[item.prop]"
                        type="datetime"
                        value-format="yyyy-MM-dd HH:mm:ss"
                        :picker-options="pickerOptions"
                        :placeholder="'选择'+item.label"
                        default-time="12:00:00"
                        :disabled="item.disabled">
                    </el-date-picker>
                </el-tooltip>
                <el-tooltip v-if="item.type == 'image'" :effect="effect" :content="'请选择'+item.label" placement="top">
                    <upload-image v-model="myForm[item.prop]" :disabled="item.disabled"></upload-image>
                </el-tooltip>
          </el-form-item>
        </el-form>
        <el-form v-if="isHandle" style="padding-left: 20px;">
            <el-form-item>
              <slot name="handle"></slot>
            </el-form-item>
        </el-form>
    </div>
</template>

下面是组件内的方法及属性定义

<script>
    import uploadImage from '@/mycomponents/UploadImage/index.vue'
    export default {
        components:{uploadImage},
        props:{
            //ref命名
            formName:{
                type:String,
                default:'right'
            },
            //操作栏
            isHandle:{
                type:Boolean,
                default:true
            },
            //表单标签位置
            labelPosition:{
                type:String,
                default:'right'
            },
            //tooltip风格是dark还是light
            effect:{
                type:String,
                default:'light'
            },
            //表单元素是并列同行还是独占一行
            inline:{
                type:Boolean,
                default:true
            },
            //表单标签宽度
            labelWidth:{
                type:String,
                default:'100px'
            },
            //绑定的表单
            myForm:{
                type:Object,
                default:()=>{}
            },
            //表单配置数据
            formData:{
                type:Array,
                default:()=>[]
            }
        },
        data(){
            return {
                pickerOptions : {
                  shortcuts: [{
                    text: '今天',
                    onClick(picker) {
                      picker.$emit('pick', new Date())
                    }
                  }, {
                    text: '昨天',
                    onClick(picker) {
                      const date = new Date()
                      date.setTime(date.getTime() - 3600 * 1000 * 24)
                      picker.$emit('pick', date)
                    }
                  }, {
                    text: '一周前',
                    onClick(picker) {
                      const date = new Date()
                      date.setTime(date.getTime() - 3600 * 1000 * 24 * 7)
                      picker.$emit('pick', date)
                    }
                  }]
                }
            }
        },
        methods:{
            //过滤超长标签
            returnLable(label){
                if(label.length>4){
                    return label.substring(0,4)+'...'
                }
                return label
            },
            // 表单验证
            async submitForm(prop) {
                try {
                    return await this.$refs["customForm"].validate();
                } catch (error) {
                    return error;
                }
            },
        }
    }
</script>

我们在页面里使用时,只需要调用一下校验方法,看是否通过校验即可,这里的表单校验方法返回的是一个promise,所以 这里用async 和 await搭配使用来renturn校验flag。
再来看我们的组件应用页面;(这里的具体属性可根据element文档自行查看。)

<template>
    <div class="pages-container">
        <dynamic-form :myForm="myForm" :formData="formData" ref="dynamic">
            <div slot="handle">
                <el-button type="primary" @click="onSubmit()">{{submitText}}</el-button>
                <el-button type="warning">取消</el-button>
            </div>
        </dynamic-form>
        <div>提交状态:{{text}}</div>
        <!-- 表单使用说明 -->
        <p>基本使用定义一个myForm(表单绑定字段的对象集合),再定义一个配置项formData(表单需要配置的验证方法、元素类型、元素标签名称等)</p>
        <p>label:'用户名',//表单元素的标签名称</p>
        <p>prop:'name',//表单元素需要绑定的字段</p>
        <p>type:'textInput',//表单元素类型,类型在下方会有说明具体是什么元素</p>
        <p>preIcon:'',//input输入框等元素首部图标,写空就是不显示</p>
        <p>sufIcon:'',//input输入框等元素尾部图标,写空就是不显示</p>
        <p>password:'',//input输入框是否为密码输入</p>
        <p>disabled:'',//表单元素可编辑状态</p>
        <p>rules:Check(true, null, 'blur', '用户名不能为空'),//校验规则,借鉴的是网上一个大神的封装方法。Check中填写的四个参数分别是:是否必填、正则方法名称、触发动作、校验提示语
        </p>
        <p>
            <span>配置项属性type值释义:</span>
            <span>input : 普通输入框</span>
            <span>select : 下拉框</span>
            <span>radio : 单选</span>
            <span>checkbox : 复选</span>
            <span>date : 日期</span>
            <span>dateTime : 日期时间</span>
            <span>image : 图片</span>
        </p>
    </div>
</template>
<script>
    import DynamicForm from '@/mycomponents/Form/dynamic.vue'
    import { Check } from '@/utils/rule.js'
    export default {
        components:{DynamicForm},
        data(){
            const options = [{
          value: '1',
          label: '黄金糕'
        }, {
          value: '2',
          label: '双皮奶'
        }, {
          value: '3',
          label: '蚵仔煎'
        }, {
          value: '4',
          label: '龙须面'
        }, {
          value: '5',
          label: '北京烤鸭'
        }]
            return {
                text:'',
                submitText:'编辑',
                //表单值绑定
                myForm:{
                    name:'',
                    vscode:'',
                    prodType:'',
                    prodp:'2',
                    commissions:['1','5'],
                    commdate:'',
                    commdatetime:'',
                    images:['https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80'],
                },
                //基本的表单配置项示例、有需要可自行添加其他属性
                formData:[
                    {
                        label:'用户名',//标签名称
                        prop:'name',//字段值
                        type:'input',//表单元素类型
                        preIcon:'el-icon-search',//首部标签,input元素独有
                        rules:Check(true, null, 'blur', '用户名不能为空'),//校验规则
                        disabled:true,
                    },
                    {
                        label:'管理员密码',
                        prop:'vscode',
                        type:'input',
                        sufIcon:'',//尾部标签,input元素独有
                        password:true,//input是否是密码类型
                        rules:Check(true, null, 'blur', '密码不能为空'),
                        disabled:true,
                    },
                    {
                        label:'产品类型',
                        prop:'prodType',
                        type:'select',
                        rules:Check(true, null, 'change', '请选择产品类型'),
                        options:options,
                        multiple:false,//select是否可以多选,多选绑定值要对应变为数组
                        disabled:true,
                    },
                    {
                        label:'产品分类',
                        prop:'prodp',
                        type:'radio',
                        rules:Check(true, null, 'change', '请选择产品分类'),
                        options:options,//属性选项值
                        disabled:true,
                    },
                    {
                        label:'订单选择',
                        prop:'commissions',
                        type:'checkbox',
                        rules:Check(true, null, 'change', '请订单选择'),
                        options:options,//属性选项值
                        disabled:true,
                    },
                    {
                        label:'订单日期',
                        prop:'commdate',
                        type:'date',
                        rules:Check(true, null, 'change', '请选择订单日期'),
                        disabled:true,
                    },
                    {
                        label:'订单日期时间',
                        prop:'commdatetime',
                        type:'datetime',
                        rules:Check(true, null, 'change', '请选择订单日期时间'),
                        disabled:true,
                    },
                    {
                        label:'订单图标',
                        prop:'images',
                        type:'image',
                        rules:Check(true, null, 'change', '请选择订单图标'),
                        disabled:true,
                    },
                ]
            }
        },
        mounted() {
            
        },
        methods:{

            //点击提交,校验表单,进行业务操作
            async onSubmit(){
                if(this.submitText == '编辑'){
                    this.formData.forEach(item=>{
                        item.disabled = false;
                    })
                    this.submitText = '提交'
                }else{
                    this.formData.forEach(item=>{
                        item.disabled = true;
                        this.submitText = '编辑'
                    })
                    let flag = await this.$refs.dynamic.submitForm();
                    if(flag){
                        this.text = "success submit"
                    }else{
                        this.text = "fail submit"
                    }
                }
                
            },
        }
    }
</script>

我的rules校验方法是在网上看到的一个大神的校验方法,了解详细,拿来即用,给大家看一下。

//Check方法
//校验规则列表(可扩展)
const rules = {
  URL(url) {
    const regex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?"\\+&%$#=~_-]+))*$/
    return valid(url, regex, "URL格式不正确")
  },

  LowerCase(str) {
    const regex = /^[a-z]+$/
    return valid(str, regex, "只能输入小写字母")
  },

  UpperCase(str) {
    const regex = /^[A-Z]+$/
    return valid(str, regex, "只能输入大写字母")
  },

  Alphabets(str) {
    const regex = /^[A-Za-z]+$/
    return valid(str, regex, "只能输入字母")
  },

  Email(email) {
    const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
    return valid(email, regex, "邮箱地址格式不正确")
  },

  Mobile(mobile) {
    const regex = /^1\d{10}$/
    return valid(mobile, regex, "手机号格式不正确")
  },

  Phone(phone) {
    const regex = /^(0\d{2,3})?-?\d{7,8}$/
    return valid(phone, regex, "电话号码格式不正确")
  },

  Postcode(postcode) {
    const regex = /^[0-9][0-9]{5}$/
    return valid(postcode, regex, "邮编格式不正确")
  },

  Number(num) {
    const regex = /^\d+$/
    return valid(num, regex, "只能输入纯数字")
  },

  Fax(fax) {
    const regex = /^(\d{3,4}-)?\d{7,8}$/
    return valid(fax, regex, "传真格式不正确")
  },

  Int(num) {
    const regex = /^((0)|([1-9]\d*))$/
    return valid(num, regex, "只能输入非负整数")
  },

  IntPlus(num){
    const regex = /^[1-9]\d*$/
    return valid(num, regex, "只能输入正整数")
  },

  Float1(num){
    const regex = /^-?\d+(\.\d)?$/
    return valid(num, regex, "只能输入数字,最多一位小数")
  },

  Float2(num){
    const regex = /^-?\d+(\.\d{1,2})?$/
    return valid(num, regex, "只能输入数字,最多两位小数")
  },

  Float3(num){
    const regex = /^-?\d+(\.\d{1,3})?$/
    return valid(num, regex, "只能输入数字,最多三位小数")
  },
  
  FloatPlus3(num){
    const regex = /^\d+(\.\d{1,3})?$/
    return valid(num, regex, "只能输入数字,最多三位小数")
  },

  Encode(code){
    const regex = /^(_|-|[a-zA-Z0-9])+$/
    return valid(code, regex, "编码只能使用字母、数字、下划线、中划线")
  },

  Encode2(code){
    const regex = /^[a-zA-Z0-9]+$/
    return valid(code, regex, "编码只能使用字母、数字")
  },

  Encode3(code){
    const regex = /^(_|[a-zA-Z0-9])+$/
    return valid(code, regex, "编码只能使用字母、数字、下划线")
  },

  IdCard(code){
    const regex = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
    return valid(code, regex, "请输入正确的身份证号码")
  },
  
  USCC(code){
    const regex = /^[0-9A-Z]{18}/
    return valid(code, regex, "请输入正确的社会信用号")
  },
  
  CarNum(code){
    const regex = /^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[DF])|([DF]([A-HJ-NP-Z0-9])[0-9]{4})))|([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领]))$/i
    return valid(code, regex, "请输入正确的车牌号")
  },
  
  CNandEN(code){
    const regex = /^[a-zA-Z\u4e00-\u9fa5]+$/
    return valid(code, regex, "只能使用中文、英文")
  },
  
  MobileOrPhone(val){
    const result = /^1\d{10}$/.test(val) || /^(0\d{2,3})?-?\d{7,8}$/.test(val)
    return valid(result, null, "手机或电话号格式不正确")
  }
}

//val:String 要校验的值
//regex:RegExp 校验正则,不是正则时val作为result的值
//msg:String 校验不通过的错误信息
function valid(val, regex, msg){
  return {result: regex instanceof RegExp? regex.test(val) : !!val, errMsg: msg}
}

//required:Boolean 是否必填项,选填,默认"true"
//type:String/Function 校验类型,选填,
//     String时必须是上面rules中存在的函数名,
//     Function时只接收一个参数(输入值),返回格式: {result:Boolean, errMsg:String}
//trigger:String 触发动作,选填,默认"blur"
//nullMsg:String 未输入的提示语,选填,required=true时有效
export function Check(required=true, type, trigger="blur", nullMsg="该字段为必填项"){
  const rule = { required: !!required, trigger}

  let check = null
  if(typeof type === "function"){
    check = type
  }else{
    check = type ? rules[type+""] : null
  }

  if(check){//存在规则时添加规则
    rule.validator = (r, v, c) => {
      const {result, errMsg} = check(v)
      if(required){
        //必填项: null,undefined,"","  " 都算无输入内容
        return (v==null || (v+"").trim()==="") ? c(new Error(nullMsg)) : result ? c() : c(new Error(errMsg))
      }
      //选填项: null,undefined,"" 都算无输入内容,"  "会被校验
      return (v==null || (v+"")==="" || result) ? c() : c(new Error(errMsg))
    }
  }else{
    rule.message = nullMsg
  }

  return [rule]
}

校验方法很完美。


QQ截图20201118170100.png

写到这里,我们的一个简单的form表单封装就完成了。有需要的小伙伴儿可以添加更多的元素进去,例如开关,例如radio-button等等,这里的封装元素也只添加了最基本的元素,想要的更多可以自行添加,示例都在上面。大家觉得对你有帮助的,可以点个赞!!!谢谢!!

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容