vue2封装动态表单组件
封装组件注意点
- 不要为了封装而封装
- 只封装不变的部分,将变化的部分通过slot或其他方式,暴露出去,交给调用者实现
- 为了提供封装组件的复用率,优先封装为UI组件,而不是封装为业务组件(即:封装组件内部使用到的数据,都通过prop获取,而不是直接通过ajax请求或vuex中获取)
动态表单动态控制的是什么?
已知的需要动态控制的场景:
- 在A界面显示:A, B, C三个表单项,B界面显示:A,B,E,F三个表单项
- 在A界面默认显示:A,B两个表单项,当A项的值为x时,C表单项才显示出来
- 在A界面默认显示:A,B,C三个表单项,A,B默认可用,C默认禁用,当A项的值为x时,C表单项才可用
- 在A界面B表单项可选择的值,依赖于A表单项的选择/填写结果
- 根据不同的分辨率每行可显示的表单项数量不同
动态表单封装
请优先考虑风格二的封装方式
风格一
特点简介
将el-form, el-form-item, el-input等等完全封装到动态表单组件内,通过for循环来刷表单项,外部通过传json配置对象的方式,完成字段的动态显示/隐藏控制。
优点:
- 减少了
<el-form-item><el-input></el-input></el-form-item>
这类代码的重复编写 - 可以轻易的改变表单项的先后顺序
缺点:
- 如果所有用到表单的地方,都用这一个表单组件实现,那么这个组件后期一定会变得非常庞大,充斥大量的逻辑控制代码,导致后期难以维护
- 要想最大程度的保留原始表单的配置和事件,那就需要使用
v-bind=attrs
和v-on=evts
方式进行配置,这相对于template语法来说,json对象的配置方式,就没那么让vue开发者习惯了。 - 通过
v-bind=attrs
方式设置原组件的属性,那么如果想设置一些默认值就变得麻烦起来,因为vue2中,设置了v-bind=attrs
之后,没法再在模板中设置默认属性,必须在这个封装的组件中,在获取到配置对象时,进行一些比较繁琐的初始值设置。 - 如果表单model对象是通过prop传入,那么表单内部修改这个表单对象需要做特殊处理,来规避eslint对单项数据流的检查报错
代码实现示例
动态表单组件
<template>
<el-form :model="formModel" v-bind="elFormAttrs">
<el-form-item
v-for="(formItemConfig, index) in formItemConfigArr"
:key="`${formItemConfig.prop}-${index}`"
:prop="formItemConfig.prop"
:rules="formItemConfig.rules"
>
<template #label>
<div class="o-custom-label">{{ formItemConfig.label }}</div>
</template>
<el-input
v-if="formItemConfig.itemType === 'input'"
v-model="formModel[formItemConfig.prop]"
></el-input>
<el-select
v-else-if="formItemConfig.itemType === 'select'"
v-model="formModel[formItemConfig.prop]"
>
<template v-if="formItemConfig.optionArr">
<el-option
v-for="(option, pos) in formItemConfig.optionArr"
:key="`${option.value}-${pos}`"
:value="option.value"
:label="option.label"
></el-option>
</template>
</el-select>
</el-form-item>
</el-form>
</template>
<script>
export default {
name: "DynForm",
model: {
event: "change",
prop: "formData",
},
props: {
formData: {
type: Object,
},
/**
* prop: 唯一标识
* itemType: 表单项类型
* rules: 表单验证规则
* label: 显示标签
* optionArr: 下拉值
*/
formItemConfigArr: {
type: Array,
default: () => [],
},
// el-form支持的所有属性
elFormAttrs: {
type: Object,
},
},
data() {
return {
formModel: this.formData ? this.formData : {},
};
},
watch: {
// 监听formData改变,将formData的值设置给组件内部的formModel,规避eslint对单项数据流的规则检测报错
formData: {
handler(newVal) {
this.formModel = newVal;
},
},
},
methods: {},
};
</script>
<style scoped></style>
调用动态表单组件
<template>
<div class="demo-form">
<DynForm
v-model="formData"
:form-item-config-arr="formItemConfigArr"
:el-form-attrs="elFormAttrs"
></DynForm>
</div>
</template>
<script>
import DynForm from "@/components/form/dyn/DynForm";
export default {
name: "FormTemplate3",
components: {
DynForm,
},
props: {
// input文本框数量
count: {
type: Number,
default: 50,
},
},
data() {
// 表单项配置
const formItemConfigArr = [
{
prop: "sex",
label: "成语故",
itemType: "select",
optionArr: [],
},
{
prop: "name",
label: "姓名",
itemType: "input",
},
{
prop: "three",
label: "成语故事",
itemType: "select",
optionArr: [],
},
{
prop: "four",
label: "一二三四五",
itemType: "select",
optionArr: [],
},
{
prop: "five",
label: "一二三四五六",
itemType: "select",
optionArr: [],
},
{
prop: "six",
label: "一二三四五六七",
itemType: "select",
optionArr: [],
},
{
prop: "seven",
label: "一二三四五六七八",
itemType: "select",
optionArr: [],
},
{
prop: "eight",
label: "一二三四五六七八九",
itemType: "select",
optionArr: [],
},
{
prop: "ten",
label: "ten",
itemType: "select",
optionArr: [],
},
{
prop: "zero",
label: "zero",
itemType: "select",
optionArr: [],
},
{
prop: "a",
label: "hello",
itemType: "select",
optionArr: [],
},
{
prop: "b",
label: "hello world",
itemType: "select",
optionArr: [],
},
{
prop: "c",
label: "good idea thank",
itemType: "select",
optionArr: [],
},
{
prop: "d",
label: "good configuration",
itemType: "select",
optionArr: [],
},
{
prop: "d",
label: "good idea thank configuration",
itemType: "select",
optionArr: [],
},
];
return {
elFormAttrs: {
inline: true,
labelWidth: "81px",
},
formData: null,
formItemConfigArr,
};
},
created() {
this.loadFormData();
this.loadSexOptions();
},
methods: {
loadFormData() {
setTimeout(() => {
this.formData = {
name: "张三",
sex: 1,
};
}, 1000);
},
loadSexOptions() {
setTimeout(() => {
const item = this.formItemConfigArr.find((item) => item.prop === "sex");
if (item) {
const sexOptionArr = [
{ value: 1, label: "选项1" },
{ value: 2, label: "选项2" },
{ value: 3, label: "选项3" },
];
item.optionArr = sexOptionArr;
}
}, 1500);
},
},
};
</script>
<style scoped lang="scss">
.js-validate-form ::v-deep(.is-error .o-show-data) {
color: red;
}
.demo-form ::v-deep(.el-form-item__label) {
line-height: 1.6;
display: inline-flex;
height: 40px;
justify-items: right;
justify-content: flex-end;
align-items: center;
}
.demo-form ::v-deep(.el-form-item__label .o-custom-label) {
word-break: break-word;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2; /* 这里是超出几行省略 */
overflow: hidden;
}
</style>
风格二 (推荐优先采用此风格
)
特点介绍
通过函数组件,以jsx语法对el-form-item进行封装,仅封装模板代码部分,动态部分全部由调用者自行实现
优点:
- 能够针对界面特点,仅封装不变的模板代码部分,能够最大程度保留template编程风格
- 因为表单项都是通过slot实现,不会导致这个表单组件随着应用的场景增多和变得越来越复杂,复杂度会基本保持不变
缺点:
- 需要掌握jsx语法(jsx仅用于封装组件,调用这个组件使用template语法即可)
代码实现示例
表单项组件封装
<script>
export default {
functional: true,
name: "DynFormItemsFunction",
props: {
formItemConfigArr: {
type: Array,
required: true,
},
},
render(h, context) {
// console.log(context);
const { props, scopedSlots } = context;
const { formItemConfigArr } = props;
// 获得插槽里的 vNodes
const vNodes = scopedSlots.default();
return vNodes.map((node, idx) => {
const formItemConfig = formItemConfigArr[idx];
return (
<el-form-item prop={formItemConfig.prop} rules={formItemConfig.rules}>
<span slot="label" className="o-custom-label">
{formItemConfig.label}
</span>
{node}
</el-form-item>
);
});
},
};
</script>
<style scoped>
.o-custom-label {
color: blue;
}
</style>
代码调用示例
<template>
<div>
<el-form :model="formData" inline :validate-on-rule-change="false">
<!-- DynFormItemsFunction 这个组件,仅封装了el-form-item的逻辑 -->
<DynFormItemsFunction :form-item-config-arr="formItemConfigArrComp">
<!--
注意组件的循环是在slot中进行的,el-form-item的包装逻辑,
交给了DynFormItemsFunction组件实现
-->
<template v-for="(formItemConfig, idx) in formItemConfigArrComp">
<el-input
v-if="formItemConfig.itemType === 'input'"
v-model="formData[formItemConfig.prop]"
:key="idx"
></el-input>
<el-select
v-else-if="formItemConfig.itemType === 'select'"
v-model="formData.sex"
:key="idx"
>
<template v-if="formItemConfig.optionArr">
<el-option
v-for="option in formItemConfig.optionArr"
:key="option.value"
:value="option.value"
:label="option.label"
></el-option>
</template>
</el-select>
</template>
</DynFormItemsFunction>
</el-form>
</div>
</template>
<script>
import DynFormItemsFunction from "@/components/form/dyn/DynFormItemsFunction";
export default {
name: "FormTemplate",
components: {
DynFormItemsFunction,
},
props: {
count: {
type: Number,
default: 50,
},
},
data() {
/*
完整的表单配置放在data中,数据中的元素个数不会变,
动态值也填充到formItemConfigArr对应元素中(如性别的下拉选项值),
formItemConfigArr只需要关注完整表单需要哪些表单项字段,
以及下拉选项数据的填充,无需考虑表单项字段的显示/隐藏逻辑控制
*/
const formItemConfigArr = [
{
prop: "sex",
label: "性别",
itemType: "select",
optionArr: [],
},
{
prop: "name",
label: "姓名",
itemType: "input",
rules: [{ required: true, message: "该项必填", trigger: "change" }],
},
{
prop: "school",
label: "学校",
itemType: "input",
rules: [],
},
];
// for (let i = 0; i < this.count; i++) {
// formItemConfigArr.push({
// prop: "name" + i,
// label: "姓名" + i,
// itemType: "input",
// });
// }
return {
formData: {
name: null,
sex: null,
school: null,
},
formItemConfigArr,
};
},
computed: {
/*
表单项的显示/隐藏通过计算属性实现,可以认为计算属性就只关注控制哪些表单项需要显示,
哪些需要隐藏,就可以了。算是一种职责分离
*/
formItemConfigArrComp() {
return this.formItemConfigArr.filter((item) => {
if (this.formData.sex === 3 && item.prop === "name") {
return null;
}
return item;
});
},
},
created() {
this.loadOptions(1500);
},
methods: {
loadOptions(timeout) {
setTimeout(() => {
const item = this.formItemConfigArr.find((item) => item.prop === "sex");
if (item) {
const sexOptionArr = new Array(10)
.fill(true)
.map((item, idx) => ({ value: idx, label: "选项:" + idx }));
item.optionArr = sexOptionArr;
}
}, timeout);
},
},
};
</script>
<style scoped lang="scss">
.js-validate-form ::v-deep(.is-error .o-show-data) {
color: red;
}
</style>
参考文章
转载自:https://juejin.cn/post/7131386390981869598