一、前言
铁打的程序员,流水的数据过滤查询表单。hello大家好,下文以element-plus组件库为例,开发一个通过js数组配置虚拟dom模式,解放双手快速生成数据过滤表单,一定程度提升你的开发效率。除此之外,文末还通过这个例子引出reactive响应式api使用时的一些注意点,帮助大家在开发中少踩坑,早点下班~。
二、预期效果
下文以文本、数字、选择框三个控件来表单过滤表单,如有更多控件需求可自行扩展。
最终使用示例:
js
<FilterForm :items="items" v-model="query" @submit="onSearch"></FilterForm>
// 过滤查询参数
const query = ref<{
name?: string,
gender?: number,
age?: number,
phone?: string,
address?: string
}>({})
// 获取过滤数据
const onSearch = () => {
getList(query.value)
}
const items = computed(() => {
return [
{ label: '名称', prop: 'name' },
{ label: '性别', prop: 'gender', type: widget.select, options: genderList.value },
{ label: '年龄', prop: 'age', type: widget.number },
{ label: '手机号码', prop: 'phone' },
{ label: '地址', prop: 'address' },
]
})
如果想要给定默认值,可以直接在query对象中进行直接指定。
三、动态表单封装
1.FilterForm.vue
js
<template>
<el-form
ref="formRef"
:model="form"
label-width="auto"
class="demo-ruleForm"
inline="inline"
@submit.native.prevent
>
<el-form-item
:label="item.label"
:prop="item.prop"
v-for="item in items"
:key="item.prop"
:label-width="item.labelWidth"
>
<el-input
v-if="!item.type || item.type === widget.text"
v-model="form[item.prop]"
:placeholder="item.placeholder"
:disabled="item.disabled"
clearable
/>
<el-input
type="number"
v-else-if="item.type === widget.number"
v-model="form[item.prop]"
:placeholder="item.placeholder"
@input="(value: string) => form[item.prop] = Number(value)"
:disabled="item.disabled"
/>
<el-select
v-else-if="item.type === widget.select"
v-model="form[item.prop]"
:value-key="item.rowKeys ? item.rowKeys.value : 'value'"
:placeholder="item.placeholder"
:disabled="item.disabled"
clearable
>
<el-option
:label="getLabel(item, option)"
:value="getValue(item, option)"
v-for="(option, i) in item.options"
:key="i"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit(formRef)" native-type="submit">
搜索
</el-button>
<el-button @click="reset(formRef)">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import type { FormInstance } from "element-plus";
import type { Option, Item, ModelValue } from "./types";
import { widget } from "@/utils/widget";
const props = defineProps<{
modelValue: ModelValue;
items: Item[];
}>();
const emit = defineEmits<{
(e: "submit"): void;
(e: "update:modelValue"): void;
}>();
// form组件实列
const formRef = ref<FormInstance>();
// 表单数据
const form = computed({
get: () => props.modelValue,
set: (val) => {
emit("update:modelValue", val);
},
});
// 提交表单
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
emit("submit");
};
// 重置表单
const reset = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
emit("submit");
};
const getLabel = (item: Item, option: Option) => {
return item.rowKeys
? option[item.rowKeys.label as keyof Option]
: option.name;
};
const getValue = (item: Item, option: Option) => {
return item.rowKeys ? option[item.rowKeys.value as keyof Option] : option.id;
};
</script>
<style scoped lang="scss"></style>
注意: 以上表单双向数据绑定使用了
computed
的set
和get
进行拦截,当表单单个数据改变时调用emit("update:modelValue", val)
更新父组件的query对象对应的数据,保持单向数据流的原则。
2.组件props参数类型
types.ts
ts
export type ModelValue = { [key: string]: any };
export type Option = {
id: number,
name: string | number
}
export type Item = {
label: string,
prop: string,
type?: number,
placeholder?: string,
disabled?: boolean,
labelWidth?: string,
options?: Option[],
rowKeys?: { label: string, value: string }
}
3.组件控件枚举值
widget.ts
ts
export enum widget {
text = 1,
select,
number
}
组件控件的映射关系建议统一封装,避免使用硬编码的方式,保持可扩展性和灵活性。
四、扩展阅读reactive一些需要注意的点:
以下问题出现在子组件单独维护form表单数据对象时,当用户点击搜索时再一次性更新父组件的form表单对象。
父组件定义query表单对象时选择
reactive
时(const query = reactive({})
)。在子组件执行
emit("update:modelValue", form)
更新父组件数据(相当于@update:modelValue = "query = $event"
)时,会对整个query对象重新赋值。问题1: 父组件使用
const
定义的query表单对象,那么将无法正常更新父组件的值,因为const无法重新赋值,而且子组件执行emit("update:modelValue", form)
更新时,控制台也没有打印对常量重新赋值的错误信息。问题2: 父组件使用
let
定义的query表单对象,那么重新赋值将失去原来响应式,导致一些奇异的bug。当然你说有没有解决方案,也有,但是性价比没有使用
ref
高。解决办法: 使用Object.assign合并对象,
emit('update:modelValue', Object.assign(props.modelValue, form))
个人看法,既然知道存在一些不可预知的问题,何不避免使用它呢。
下图为vue官方文档中说明推荐使用 ref()
作为声明响应式状态的主要 API 的原话,当然接不接受建议还是看个人选择(狗头.jpg):