教你如何基于Element-UI通过配置来生成表单

1,800 阅读1分钟

前言

作为一名sass系统开发的前端工程师,少不了要和表单打交道,我们选择的UI库是Element-UI,但是使用久了就感觉是在复制粘贴,而且特别长,为了解决这种重复枯燥的问题。所以通过配置组件来了。

解决思路

就是根据配置规则,通过vue里面的render函数,把对应的规则渲染出来。好了,直接上代码

代码实现

### index.js
import { deepClone } from '@/libs/util'
import defaultController from './default.controller'
import selectController from './select.controller'
import datePickerController from './datePicker.conrtroller'
export default {
  name: 'v-form',
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    value: {
      type: Object,
      default: () => {}
    },
    rules: {
      type: Object,
      default: () => {}
    },
    schema: {
      type: Array,
      default: () => []
    }
  },
  data () {
    return {
      formData: {}
    }
  },
  created () {
    this.defaultProps = {
      clearable: true,
      multiple: false
    }
  },
  computed: {
    len () {
      return this.schema.length
    },
    mergeProps () {
      return [this.value, this.visible]
    }
  },
  methods: {
    get () {
      this.$emit('input', this.formData)
    },
    validate (fn) {
      this.$refs.form.validate(valid => {
        fn && typeof fn === 'function' && fn(valid)
      })
    },
    resetFields () {
      this.$refs.form.resetFields()
    },
    renderController (h, item) {
      if ('render' in item) {
        return item.render.call(this, h, item)
      }
      switch (item.controller) {
        case 'el-date-picker':
          return datePickerController.call(this, h, item)
        case 'el-select':
          return selectController.call(this, h, item)
        default:
          return defaultController.call(this, h, item)
      }
    },
    renderSpec (h) {
      return h('v-form-item', {
        props: {
          span: '12'
        }
      }, [])
    },
    renderValidate (h, item, isRule) {
      if (isRule) {
        return h('el-form-item', {
          props: {
            prop: item.field,
            rules: this.rules[item.field] || {}
          }
        }, [
          this.renderController(h, item)
        ])
      }
      return this.renderController(h, item)
    },
    renderFormItem (h, item, span = '12') {
      const isRule = item.field && this.rules && item.field in this.rules
      return h('v-form-item', {
        props: {
          span,
          required: isRule || item.required || false,
          label: item.label || '',
          tip: item.tip || ''
        }
      }, [
        this.renderValidate(h, item, isRule)
      ])
    },
    initSchema (h) {
      const children = []
      this.schema.reduce((pre, item, currentIndex) => {
        if ('span' in item && item.span === 24) {
          if (pre.length < 1) {
            children.push(
              h('el-row', {}, [
                this.renderFormItem(h, item, '24')
              ])
            )
            return []
          }
          if (pre.length === 1) {
            pre.push(this.renderSpec(h))
            children.push(h('el-row', {}, pre))
            children.push(
              h('el-row', {}, [
                this.renderFormItem(h, item, '24')
              ])
            )
            return []
          }
        } else {
          pre.push(
            this.renderFormItem(h, item)
          )
          if (pre.length === 2) {
            children.push(h('el-row', {}, pre))
            return []
          }
          if (currentIndex + 1 === this.len) {
            pre.push(this.renderSpec(h))
            children.push(h('el-row', {}, pre))
          }
          return pre
        }
      }, [])
      children.unshift(this.$slots.header)
      children.push(this.$slots.footer)
      return children
    }
  },
  render (h) {
    return h('el-form', {
      ref: 'form',
      props: {
        model: this.formData,
        hideRequiredAsterisk: true,
        labelPosition: 'top',
        rules: this.rules
      }
    }, this.initSchema(h))
  },
  watch: {
    mergeProps: {
      handler ([value, visible]) {
        if (visible) {
          this.formData = deepClone(value)
        } else {
          this.timer = setTimeout(() => {
            this.schema = []
            this.formData = Object.create(null)
            clearTimeout(this.timer)
          }, 300)
        }
      },
      deep: true,
      immediate: true
    }
  }
}

加载下拉组件controller

### select.controller.js
export default function (h, item) {
  const props = Object.assign({}, this.defaultProps, item.props, {
    value: this.formData[item.field]
  })
  const attrs = Object.assign({}, item.attrs)
  const renderOptions = []
  if (item.store && item.store.length) {
    item.store.map(option => {
      renderOptions.push(h('el-option', {
        props: option
      }))
    })
  }
  return h(item.controller, {
    class: 'w100',
    props,
    attrs,
    on: Object.assign({}, item.on || {}, {
      'input': v => {
        this.formData[item.field] = v
        item.on && item.on.input && item.on.input.call(this, v, item)
      }
    })
  }, renderOptions)
}

加载日期型的controller

### datePicker.conrtroller.js
import { formatDate } from '@/libs/util'
export default function (h, item) {
  const props = Object.assign({}, this.defaultProps, item.props, {
    value: this.formData[item.field]
  })
  const attrs = Object.assign({}, item.attrs)
  return h(item.controller, {
    class: 'w100',
    props,
    attrs,
    on: Object.assign({}, item.on || {}, {
      'input': v => {
        if (v) {
          this.formData[item.field] = formatDate(v)
        } else {
          this.formData[item.field] = v
        }
        item.on && item.on.input && item.on.input.call(this, v, item)
      }
    })
  })
}

加载默认的controller

### default.controller.js
export default function (h, item) {
  const props = Object.assign({}, this.defaultProps, item.props, {
    value: this.formData[item.field]
  })
  const attrs = Object.assign({}, item.attrs)
  return h(item.controller, {
    props,
    attrs,
    on: Object.assign({}, item.on || {}, {
      'input': v => {
        this.formData[item.field] = v
        item.on && item.on.input && item.on.input.call(this, v, item)
      }
    })
  })
}

util工具提供两个方法

### util.js
/**
 * 对象拷贝方法
 * @param { any } data
 */
export const deepClone = (data) => {
  if (typeOf(data) === 'array') {
    return data.map(deepClone)
  } else if (data && typeof data === 'object') {
    const obj = Object.assign({}, data)
    for (const key in obj) {
      if (!obj.hasOwnProperty(key)) {
        continue
      }
      if (typeof obj[key] === 'object') {
        obj[key] = deepClone(obj[key])
      }
    }
    return obj
  } else {
    return data
  }
}

/**
 * 日期格式化
 * @param { Date } date
 * @param { String } fmt
 */
export const formatDate = (date, fmt = 'YYYY-MM-DD HH:mm:ss') => {
  return dayjs(date).format(fmt)
}

还涉及到了一个排版组件v-form-item

<template>
  <el-col
    :class="['v-form-item', required && 'v-form-item__required']"
    :span="span"
  >
    <el-form-item :label="label" :prop="prop">
      <slot></slot>
      <div class="v-form-item__tip line-clamp fs12">{{ tip }}</div>
    </el-form-item>
  </el-col>
</template>
<script>
export default {
  name: 'v-form-item',
  props: {
    span: {
      type: String,
      default: '12'
    },
    required: {
      type: Boolean,
      default: false
    },
    prop: {
      type: String
    },
    label: {
      type: String,
      default: ''
    },
    tip: {
      type: String,
      default: ''
    }
  }
}
</script>
<style lang="scss">
.el-form--label-top .el-form-item__label {
  padding: 0 !important;
}
.v-form-item__required {
  .el-form-item__label {
    &::after {
      content: '*';
      position: relative;
      left: 5px;
    }
  }
}
.el-form-item--small.el-form-item {
  margin-bottom: 0;
}
.el-form-item__error {
  padding-top: 6px !important;
  background: #fff;
  width: 100%;
  z-index: 1;
}
.v-form-item {
  margin-bottom: 24px;
  & +.v-form-item {
    padding-left: 10px;
  }
  &:nth-last-child(2):first-child {
    padding-right: 10px;
  }
  &__tip {
    width: 100%;
    height: 20px;
    line-height: 24px;
    position: absolute;
    color: #999;
    background: #fff;
    z-index: 0;
  }
}
</style>

最后看看怎么来写配置

<template>
<section>
  <v-form
    :schema="schema"
    v-model="formData"
    :rules="rules"
    ref="form"
  />
  <el-button @click="handleValidate">验证表单</el-button>
  <el-button @click="handleSave">获取数据</el-button>
  <el-button @click="handleReset">重置表单</el-button>
</section>
</template>
<script>
export default {
  data () {
    return {
      rules: {
        name: [
          { required: true, message: '请输入员工名称', trigger: 'blur' },
          { min: 3, max: 5, message: '长度在 2 到 20 个字符', trigger: 'blur' }
        ],
        mobile: [
          { required: true, message: '手机号不能为空', trigger: 'blur' }
        ]
      },
      schema: [
        {
          field: 'name',
          label: '员工',
          tip: '员工的全名,会出现在企业通讯录中',
          required: true,
          controller: 'el-input',
          attrs: {
            placeholder: '请输入'
          }
        },
        {
          field: 'mobile',
          label: '手机',
          tip: '需要员工用手机号绑定的微信激活账号,激活后手机号不可编辑',
          required: true,
          controller: 'el-input',
          attrs: {
            placeholder: '请输入'
          }
        },
        {
          field: 'deptIds',
          label: '部门',
          tip: '选择的第一个部门将作为该员工的主部门',
          controller: 'v-select-dept',
          props: {
            multiple: true,
            store: []
          }
        },
        {
          field: 'roleIds',
          label: '角色',
          controller: 'el-select',
          store: [
            {
              label: '管理员',
              value: '0'
            }
          ],
          attrs: {
            placeholder: '请选择角色'
          },
          props: {
            multiple: true
          }
        },
        {
          field: 'position',
          label: '职位',
          controller: 'el-input',
          attrs: {
            placeholder: '请输入职位'
          }
        },
        {
          field: 'jobNumber',
          label: '工号',
          controller: 'el-input',
          attrs: {
            placeholder: '请输入工号'
          }
        },
        {
          field: 'email',
          label: '邮箱',
          controller: 'el-input',
          attrs: {
            placeholder: '请输入邮箱地址'
          }
        },
        {
          field: 'tel',
          label: '座机',
          controller: 'el-input',
          attrs: {
            placeholder: '请输入电话号码'
          }
        },
        {
          field: 'hiredDate',
          label: '入职时间',
          controller: 'el-date-picker',
          attrs: {
            placeholder: '请选择入职时间'
          },
          props: {
            type: 'date'
          }
        },
        {
          field: 'workPlace',
          label: '办公地点',
          controller: 'el-input',
          attrs: {
            placeholder: '请输入办公地点'
          }
        },
        {
          field: 'bySort',
          label: '排序权重',
          span: 12,
          tip: '权重值越大,排序越靠前',
          type: 'number',
          max: 9999999999,
          precision: 4,
          controller: 'el-input',
          attrs: {
            placeholder: '请输入权重数值'
          }
        },
        {
          field: 'remark',
          label: '备注',
          controller: 'el-input',
          span: 24,
          attrs: {
            placeholder: '请输入内容',
            rows: 2
          },
          props: {
            type: 'textarea'
          }
        }
      ],
      formData: {
        bySort: null,
        name: null,
        email: null,
        hiredDate: null,
        jobNumber: null,
        leaveDate: null,
        mobile: null,
        workPlace: null,
        position: null,
        remark: null,
        roles: null,
        depts: null,
        tel: null
      }
    }
  },
  methods: {
    handleSave () {
      this.$refs.form.get()
    },
    handleReset () {
      this.$refs.form.resetFields()
    },
    handleValidate () {
      this.$refs.form.validate(res => {
      })
    }
  }
}
</script>

最终效果

这样写配置是不是更简单

  • field 对应表单字段。
  • label 表单头部。
  • span 表单是否占一行还是半行 使用的el-row, el-cel控制布局。
  • tip 表单下面的提示。
  • controller 下面的就是对应组件以及组件自身的props, attrs, events等等。
  • rules其实就跟element-ui是一样的没什么好说的。