element-plus form表单的二次封装

1. 首先,我们需要在components新建一个文件夹CustomForm,然后新建一个index.vue的文件
// element-plus form表单的二次封装 自定义form表单
<template>
  <el-form :model="model" v-bind="_options" ref="formRef">
    <template v-for="(item, index) in fieldList" :key="index">
      <!-- 单选框 -->
      <el-form-item :label="item.label" v-if="item.type === 'radio'" :rules="item.rules" :prop="[item.field]">
        <el-radio-group v-model="model[item.field]" :disabled="item.disabled">
          <el-radio :label="val[item.options?.valueKey || 'value']" size="large" v-for="val in item.options?.data"
            :key="val[item.options?.valueKey || 'value']">
            {{ val[item.options?.labelkey || 'label'] }}
          </el-radio>
        </el-radio-group>
      </el-form-item>
      <!-- 复选框 -->
      <el-form-item :label="item.label" v-else-if="item.type === 'checkbox'" :rules="item.rules" :prop="[item.field]">
        <el-checkbox-group v-model="model[item.field]" :disabled="item.disabled">
          <el-checkbox v-for="c in item.options?.data" :key="c[item.options?.valueKey || 'value']"
            :label="c[item.options?.valueKey || 'value']">{{ c[item.options?.labelkey || 'label'] }}</el-checkbox>
        </el-checkbox-group>
      </el-form-item>
      <!-- 下拉框 -->
      <el-form-item :label="item.label" v-else-if="item.type === 'select'" :rules="item.rules" :prop="[item.field]">
        <el-select v-model="model[item.field]" :placeholder="item.options?.placeholder || '请选择'" clearable>
          <el-option v-for="s in item.options?.data" :key="s[item.options?.valueKey || 'value']"
            :label="s[item.options?.labelkey || 'label']" :value="s[item.options?.valueKey || 'value']" />
        </el-select>
      </el-form-item>
      <!-- 默认输入框 -->
      <el-form-item :label="item.label" :rules="item.rules" :prop="[item.field]" v-else>
        <el-input v-model="model[item.field]" :readonly="item.readonly" :type="item.type ?? 'text'"
          :placeholder="item.label" :disabled="item.disabled" />
      </el-form-item>
    </template>
    <el-form-item>
      <slot name="buttons" :model="model" :formRef="formRef">
        <el-button type="primary" @click="onSubmit(formRef)">{{ _options.submitButtonText }}</el-button>
        <el-button v-if="_options.showResetButton" type="info" @click="resetForm(formRef)">
          {{ _options.resetButtonText }}
        </el-button>
        <el-button v-if="_options.showCancelButton" @click="emit('cancel')">
          {{ _options.cancelButtonText }}
        </el-button>
      </slot>
    </el-form-item>
  </el-form>
</template>
<script lang="ts" setup>
import type { FormInstance } from "element-plus";
import { ComputedRef, ref, computed } from "vue";
// 父组件传递的值
interface Props {
  fieldList: Form.FieldItem[];
  model?: Record<string, any>;
  options?: Form.Options;
}
// 表单的数据
const model = ref<Record<string, any>>({});
const formRef = ref<FormInstance>();
const props = defineProps<Props>();
// 设置option默认值,如果传入自定义的配置则合并option配置项
const _options: ComputedRef<Form.Options> = computed(() => {
  const option = {
    labelWidth: 120,
    labelPosition: "right",
    disabled: false,
    submitButtonText: "提交",
    resetButtonText: "重置",
    cancelButtonText: "取消",
    showResetButton: false,
    showCancelButton: false,
  };
  return Object.assign(option, props?.options);
});
interface EmitEvent {
  (e: "submit", params: any): void;
  (e: "reset"): void;
  (e: "cancel"): void;
}
const emit = defineEmits<EmitEvent>();
defineExpose({
  formRef,
});
// 根据fieldList初始化model, 如果model有传值就用传递的model数据模型,否则就给上面声明的model设置相应的(key,value) [item.field], item.value是表单的默认值(选填)
props.fieldList.map((item: Form.FieldItem) => {
  // 如果类型为checkbox,默认值需要设置一个空数组
  const value = item.type === "checkbox" ? [] : "";
  props.model
    ? (model.value = props.model)
    : (model.value[item.field] = item.value || value);
});
// 提交按钮
const onSubmit = (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  formEl.validate((valid) => {
    if (valid) {
      emit("submit", model);
    } else {
      return false;
    }
  });
};
// 重置按钮
const resetForm = (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  formEl.resetFields();
};
</script>
<style lang="less" scoped></style>

写完Form组件的代码后,会报红线,Form.XXXXX 找不到,这个是Form表单的全局类型声明。

声明文件在下方,直接复制进项目中, 红色警告自然消失。

声明文件可以直接放在src下即可。(因为后续我们项目可能需要二次封装多个组件,例如table, pagination, date-picker等,所以在此我们新建一个type文件夹,里面再创建各个组件的声明文件)

// src/type/form/index.d.ts
declare namespace Form {
  type ItemType = 'password' | 'text' | 'textarea' | 'radio' | 'checkbox' | 'select'
  // 当FiledItem的type === 'radio' | 'checkbox'时,options的参数类型
  interface IFieldOptions {
    labelkey?: string,
    valueKey?: string,
    placeholder?: string,
    data: Recode<string, any>[]
  }
  interface Options {
    labelWidth?: string | number,
    labelPosition?: 'left' | 'right' | 'top',
    disabled?: boolean,
    size?: 'large' | 'small' | 'default',
    showResetButton?: boolean, // 是否展示重置按钮
    showCancelButton?: boolean, // 是否展示取消按钮
    submitButtonText?: string,
    resetButtonText?: string,
    cancelButtonText?: string
  }
  interface FieldItem {
    label: string,
    field: string,
    type?: ItemType,
    value?: any,
    placeholder?: string,
    disabled?: boolean,
    readonly?: boolean,
    options?: IFieldOptions,
    rules?: RuleItem[]
  }
  interface RuleItem {
    type?: RuleType;
    required?: boolean;
    pattern?: RegExp | string;
    min?: number;
    max?: number;
    len?: number;
    enum?: Array<string | number | boolean | null | undefined>;
    whitespace?: boolean;
    fields?: Record<string, Rule>;
    options?: ValidateOption;
    defaultField?: Rule;
    transform?: (value: Value) => Value;
    message?: string | ((a?: string) => string);
    asyncValidator?: (rule: InternalRuleItem, value: Value, callback: (error?: string | Error) => void, source: Values, options: ValidateOption) => void | Promise<void>;
    validator?: (rule: InternalRuleItem, value: Value, callback: (error?: string | Error) => void, source: Values, options: ValidateOption) => SyncValidateResult | void;
    trigger?: 'blur' | 'change'
  }
}
2. 然后我们需要配置form的基本信息(基本表单信息,验证规则,Options等)
// 自定义验证邮箱方法
const checkEmail = (rule: any, value: any, callback: any) => {
  if (!value) callback(new Error('Please input the email'))
  const regExp = /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.(com|cn|net)$/
  regExp.test(value) ? callback() : callback(new Error('Please input the correct email address'))
}
// // 自定义验证表单配置数据
// export const validationFormFieldList = [
//     { label: '姓名', field: 'name', rules: [{ required: true, message: 'name is required' }] },
//     { label: '邮箱', field: 'email', rules: [{ required: true, validator: checkEmail }] },
// ] as Form.FieldItem[]

// 表单配置示例
export const exampleForm = {
  base: [
    { label: '姓名', field: 'name', disabled: false },
    { label: '性别', field: 'gender', type: 'radio', options: { data: [{ label: '男', value: 1 }, { label: '女', value: 0 }] } },
    {
      label: '爱好',
      field: 'hobbies', type: 'checkbox',
      options: {
        data: [
          { label: '吃饭', value: 1 },
          { label: '睡觉', value: 2 },
          { label: '写代码', value: 3 }
        ]
      }
    },
    {
      label: '工作', field: 'job', type: 'select',
      options: {
        data: [{ label: '吃饭', value: 1 }, { label: '睡觉', value: 2 }, { label: '写代码', value: 3 }]
      }
    },
    { label: '密码', field: 'password', type: 'password', placeholder: '这是一个密码输入框' },
    { label: '只读', field: 'readonly', readonly: true, placeholder: '这是一个只读输入框' },
    { label: '留言板', field: 'summary', type: 'textarea', placeholder: '留言板' },
  ],
  customkeyForm: [
    { label: '标题', field: 'name' },
    { label: '性别', field: 'gender', type: 'radio', options: { labelkey: 'title', valueKey: 'val', data: [{ title: '男', val: 1 }, { title: '女', val: 0 }] } },
  ],
  ruleForm: [
    { label: '姓名', field: 'name', rules: [{ required: true, message: 'name is required' }] },
    { label: '邮箱', field: 'email', rules: [{ required: true, validator: checkEmail }] },
  ]
} as Record<string, Form.FieldItem[]>

export const Options = {
  // 自定义form1表单
  form1: {
    showResetButton: true,
    showCancelButton: false,
    resetButtonText: "重置1212",
  }
}

3. 接下来,我们就到了使用环节
// src/views/form/index.vue
<template>
    <el-card class="mb-5">
        <template #header> 基本表单 </template>
        <custom-form :fieldList="fieldList" :model="model" @submit="handleBaseSubmit">
            <!-- 如果不使用默认的按钮可以使用插槽自定义内容, 插槽返回的model就是当前表单的数据 -->
            <!-- <template #buttons="{ model }">
                    <el-button">提交</el-button>
                </template> -->
        </custom-form>
    </el-card>
</template>
<script lang="ts" setup>
    import { exampleForm } from '@/config/form' 
    import { ref } from 'vue'
    // 本项目EasyForm组件自动引入,如复制此代码,需根据路径引入Form组件后使用
    const fieldList: Form.FieldItem[] = exampleForm.base
    const model = ref<Record<string, any>>({
        name: '张三',
        gender: 1,
        hobbies: [1],
        job: 3,
        readonly: '只读输入框',
        summary: '尤雨溪懂个锤子vue是什么梗'
    })
    /**
     * 注意: model数据模型非必填项,如果仅仅是用于数据收集,model参数可以不用填,表单的submit事件会返回所有搜集的数据对象
     *       如果是编辑的情况下,页面需要回显数据,则model数据模型必须要填写
     */
    const handleBaseSubmit = (model: Record<string, any>) => {
        console.log(model.value)
    }
</script>
<style lang="less" scoped></style>

此时运行项目,我们可以得到的界面

基础表单
image.png

自定义key

// src/views/form/index.vue
<template>
    <el-card class="mb-5">
        <template #header> 自定义key </template>
        <custom-form :fieldList="customKeyFieldList" :model="model2" />
    </el-card>
</template>
<script lang="ts" setup>
    import { exampleForm } from '@/config/form'
    import { ref } from 'vue'
    // import EasyForm from '@/components/EasyForm/index.vue'
    // 本项目EasyForm组件自动引入,如复制此代码,需根据路径引入Form组件后使用
    const customKeyFieldList: Form.FieldItem[] = exampleForm.customkeyForm
    const model2 = ref<Record<string, any>>({
        name: '自定义key',
        gender: 1
    })
    /**
     * 注意: 如果使用到checkbox,radio,或者select等组件,需要传入组件额外需要的数据,本组件默认设定的读取数据的字段是 label, value
     *       可参考下方声明文件 FiledItem options的参数类型描述
     *       比如,当前传入的data数据字段名和label、value不匹配,可使用预留的参数 labelkey, valueKey指定字段名
     *         customkeyForm: [
                    { label: '标题', field: 'name' },
                    { label: '性别', field: 'gender', type: 'radio', options: { labelkey: 'title', valueKey: 'val', data: [{ title: '男', val: 1 }, { title: '女', val: 0 }] } },
                ],
     */
    const handleBaseSubmit = (model: Record<string, any>) => {
        console.log(model.value)
    }
</script>
<style lang="less" scoped></style>

界面效果如下
image.png

自定义表单验证

// src/views/form/index.vue
<template>
    <el-card class="mb-5">
        <template #header> 自定义验证的表单 (使用slot自定义按钮) </template>
        <custom-form :fieldList="ruleFieldList">
            <!-- 如果不使用默认的按钮可以使用插槽自定义内容, 插槽返回的model就是当前表单的数据, formRef是当前表单的FormInstance -->
            <template #buttons="{ model, formRef }">
                <el-button @click="handleSubmit(model, formRef)">保存</el-button>
            </template>
        </custom-form>
    </el-card>
</template>
<script lang="ts" setup>
    import type { FormInstance } from 'element-plus'
    import { exampleForm } from '@/config/form' 
    // import EasyForm from '@/components/EasyForm/index.vue'
    // 本项目EasyForm组件自动引入,如复制此代码,需根据路径引入Form组件后使用
    const ruleFieldList: Form.FieldItem[] = exampleForm.ruleForm
    /**
     *  如果用到了表单验证,又使用slot自定义按钮的话,需要自行实现验证逻辑
     *  组件内部已经集成验证,及重置逻辑。表单验证建议使用内置的提交按钮。当通过验证规则,内置提交按钮才会出发submit事件
     */
    // 下方是使用slot自定义按钮,需要自己实现验证逻辑
    const handleSubmit = (model: any, formEl: FormInstance | undefined) => {
        if (!formEl) return
        formEl.validate((valid) => {
            if (valid) {
                console.log('submit!', model)
            } else {
                console.log('error submit!')
                return false
            }
        })
    }
</script>
<style lang="less" scoped></style>

页面效果如下
image.png

4. 如果我们需要根据不同表单,展示不一样的效果,我们可以通过options去设置

比如,由于我们在customForm中,默认是不展示重置和取消按钮的

// 改变这两个值的属性,可显示隐藏按钮
showResetButton: false,
showCancelButton: false,

从下面参数介绍,我们可以看到options是一个对象,所以我们可以这样写

<template>
  <div class="home">
    <el-card class="mb-5">
      <template #header> 基本表单 </template>
      <custom-form :fieldList="ruleFieldList" :options="options" @submit="handleSubmit">
        <!-- 如果不使用默认的按钮可以使用插槽自定义内容, 插槽返回的model就是当前表单的数据 -->
        <!-- <template #buttons="{ model, formRef }">
          <el-button @click="handleSubmit(model, formRef)">保存</el-button>
        </template> -->
      </custom-form>
    </el-card>
  </div>
</template>

<script lang="ts" setup>
import type { FormInstance } from "element-plus";
import { exampleForm, Options } from "@/config/form";
import { ref } from "vue";
// 本项目EasyForm组件自动引入,如复制此代码,需根据路径引入Form组件后使用
const options: Form.Options = Options.form1;
const ruleFieldList: Form.FieldItem[] = exampleForm.ruleForm;
console.log("options", options);
/**
 * 注意: model数据模型非必填项,如果仅仅是用于数据收集,model参数可以不用填,表单的submit事件会返回所有搜集的数据对象
 *       如果是编辑的情况下,页面需要回显数据,则model数据模型必须要填写
 */
const handleSubmit = (model: Record<string, any>) => {
  console.log(model.value)
}
</script>

页面效果如下
image.png

参数介绍

Form 属性

参数 说明 类型 是否必填 默认值
model 表单数据对象 Record<string, any>
options 自定义配置 object
fieldList formItem 配置数组 Array<object>

Options 配置项

参数 说明 类型 是否必填 默认值
labelWidth 标签的长度,例如 ‘50px’。 作为 Form 直接子元素的 form-item 会继承该值。 可以使用 auto。 string / number
labelPosition 表单域标签的位置, 当设置为 left 或 right 时,则也需要设置 label-width 属性‘left’ / ‘right’ / ‘top’ right
size 用于控制该表单内组件的尺寸 large / default /small
disabled 是否禁用该表单内的所有组件。 如果设置为 true, 它将覆盖内部组件的 disabled 属性。 boolean false
submitButtonText 提交按钮默认显示的文本内容 string 提交
resetButtonText 重置按钮默认显示的文本内容 string 重置
cancelButtonText 取消按钮默认显示的文本内容 string 取消
showResetButton 是否显示重置按钮 boolean
showCancelButton 是否显示取消按钮 boolean

fieldItem 配置项

参数 说明 类型 是否必填 默认值
field model 的键名 string
label 标签文本 string
type 当前 fieldItem 的类型 ‘password’ / ‘text’ / ‘textarea’ / ‘radio’ / ‘checkbox’ / ‘select’ text
value 默认显示的值 any
placeholder 输入框占位文本 string
disabled 是否禁用 boolean false
options 如果 type=‘checkbox’ / ‘radio’ / 'select’时,需传入此配置项。格式参考 fieldItem options 配置项 object -
rules 表单验证规则。格式参考element-plus form 表单 或者参数类型声明 Array<RuleItem> -

fieldItem options 配置项

参数 说明 类型 是否必填 默认值
labelkey label 自定义字段名 string label
value value 自定义字段名 string value
placeholder 当 fieldItem type= 'select’时,选择框的提示语 string -
data type=‘checkbox’ / ‘radio’ / 'select’时, 需要的数据 Array<object> -

Form 插槽

插槽名 说明 插槽作用域
buttons 自定义按钮区域的内容 { model, formRef }

Form 事件

事件名 说明 回调参数
submit 点击默认的提交按钮触发 model
cancel 点击取消按钮触发 -
reset 重置该表单项,将其值重置为初始值,并移除校验结果 -
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容