铁打的程序员,流水的数据过滤查询表单(vue3+reactive踩坑)

一、前言

铁打的程序员,流水的数据过滤查询表单。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>

注意: 以上表单双向数据绑定使用了computedsetget进行拦截,当表单单个数据改变时调用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):

往期文章回顾

table表格自适应浏览器窗口变化解决方案

一文学会vue3如何自定义hook钩子函数和封装组件

一文学会请求中断、请求重发、请求排队、请求并发

相关推荐
吕彬-前端9 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱11 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai20 分钟前
uniapp
前端·javascript·vue.js·uni-app
bysking1 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205872 小时前
web端手机录音
前端
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb