vue3 表单封装遇到的一个有意思的问题

发布时间 2024-01-04 15:01:18作者: 柯基与佩奇

前言

最近在用 vue3 封装 element 的表单时遇到的一个小问题,这里就简单记录一下过程。话不多说直接上代码!!!

正文

部分核心代码

import { ref, defineComponent, renderSlot, type PropType, type SetupContext } from 'vue';
import { ElForm, ElFormItem, ElRow, ElCol } from 'element-plus';
import type { RowProps, FormItemProps, LabelPosition } from './types';
import formItemRender from './CusomFormItem';
import { pick } from 'lodash-es';

const props = {
  formRef: {
    type: String,
    default: 'customFormRef',
  },
  modelValue: {
    type: Object as PropType<Record<string, unknown>>,
    default: () => ({}),
  },
  rowProps: {
    type: Object as PropType<RowProps>,
    default: () => ({
      gutter: 24,
    }),
  },
  formData: {
    type: Array as PropType<FormItemProps[]>,
    default: () => [],
  },
  labelPosition: {
    type: String as PropType<LabelPosition>,
    default: 'right',
  },
  labelWidth: {
    type: String,
    default: '150px',
  },
};

const elFormItemPropsKeys = [
  'prop',
  'label',
  'labelWidth',
  'required',
  'rules',
  // 'error',
  // 'showMessage',
  // 'inlineMessage',
  // 'size',
  // 'for',
  // 'validateStatus',
];

export default defineComponent({
  name: 'CustomForm',
  props,
  emits: ['update:modelValue'],
  setup(props, { slots, emit, expose }: SetupContext) {
    const customFormRef = ref();

    const mValue = ref({ ...props.modelValue });

    watch(
      mValue,
      (newVal) => {
        emit('update:modelValue', newVal);
      },
      {
        immediate: true,
        deep: true,
      },
    );

    // 表单校验
    const validate = async () => {
      if (!customFormRef.value) return;
      return await customFormRef.value.validate();
    };

    // 表单重置
    const resetFields = () => {
      if (!customFormRef.value) return;
      customFormRef.value.resetFields();
    };

    // 暴漏方法
    expose({ validate, resetFields });

    // col 渲染
    const colRender = () => {
      return props.formData.map((i: FormItemProps) => {
        const formItemProps = { labelWidth: props.labelWidth, ...pick(i, elFormItemPropsKeys) };
        return (
          <ElCol {...i.colProps}>
            <ElFormItem {...formItemProps}>
              {i.formItemType === 'slot'
                ? renderSlot(slots, i.prop, { text: mValue.value[i.prop], props: { ...i } })
                : formItemRender(i, mValue.value)}
            </ElFormItem>
          </ElCol>
        );
      });
    };

    return () => (
      <ElForm ref={customFormRef} model={mValue} labelPosition={props.labelPosition}>
        <ElRow {...props.rowProps}>
          {colRender()}
          <ElCol>
            <ElFormItem labelWidth={props.labelWidth}>{renderSlot(slots, 'action')}</ElFormItem>
          </ElCol>
        </ElRow>
      </ElForm>
    );
  },
});
<script setup lang="ts">
import CustomerForm from "/@/components/CustomForm";
const data = ref([
  {
    formItemType: "input",
    prop: "name",
    label: "Activity name",
    placeholder: "Activity name",
    rules: [
      {
        required: true,
        message: "Please input Activity name",
        trigger: "blur",
      },
      { min: 3, max: 5, message: "Length should be 3 to 5", trigger: "blur" },
    ],
  },
  {
    formItemType: "select",
    prop: "region",
    label: "Activity zone",
    placeholder: "Activity zone",
    options: [
      {
        label: "Zone one",
        value: "shanghai",
      },
      {
        label: "Zone two",
        value: "beijing",
      },
    ],
  },
  {
    formItemType: "inputNumber",
    prop: "count",
    label: "Activity count",
    placeholder: "Activity count",
  },
  {
    formItemType: "date",
    prop: "date",
    label: "Activity date",
    type: "datetime",
    placeholder: "Activity date",
  },
  {
    formItemType: "radio",
    prop: "resource",
    label: "Resources",
    options: [
      { label: "Sponsorship", value: "1" },
      { label: "Venue", value: "2" },
    ],
  },
  {
    formItemType: "checkbox",
    prop: "type",
    label: "Activity type",
    options: [
      { label: "Online activities", value: "1", disabled: true },
      { label: "Promotion activities", value: "2" },
      { label: "Offline activities", value: "3" },
      { label: "Promotion activities", value: "4" },
      { label: "Simple brand exposure", value: "5" },
    ],
  },
  {
    formItemType: "input",
    prop: "desc",
    type: "textarea",
    label: "Activity form",
    placeholder: "Activity form",
  },
  {
    formItemType: "slot",
    prop: "test",
    label: "slot",
  },
]);
const model = reactive({
  name: "",
  region: "",
  count: 0,
  date: "",
  resource: "",
  type: [],
  desc: "",
  test: "1111",
});
const formRef = ref();
const submitForm = () => {
  const valid = formRef.value.validate();
  if (valid) {
    console.log(model);
  } else {
    return false;
  }
};

const resetForm = () => {
  formRef.value.resetFields();
};
</script>

<template>
  <div class="wrap">
    <CustomerForm ref="formRef" :v-model="model" :formData="data">
      <template #test="scope">
        {{ scope.text }}
      </template>
      <template #action>
        <el-button type="primary" @click="submitForm()">Create</el-button>
        <el-button @click="resetForm()">Reset</el-button>
      </template>
    </CustomerForm>
  </div>
</template>

<style scoped>
.wrap {
  margin: 30px auto;
  width: 600px;
  height: auto;
}
</style>

问题现象

代码其实非常简单,运行起来也很正常很流畅 ???,但是当我填写完表单后点击提交按钮,打印 model 的值时,发现值全没给上。

微信截图_20230709120015.png

原因分析

这里经过两年半的尝试,终于发现在定义 model 时,将const model = reactive({xxx}) 改为 const model = ref({xxx}) 后就正常了。思考了一下 ref 定义的对象,源码上最后通过 toReactive 还是被转化为 reactive,ref 用法上需要 .value, 数据上这两者应该没有什么不同。然后我就去把 reactive、ref 又看了看也没发现问题。在emit('update:modelValue', newVal) 处打印也是正常的。

watch(
  mValue,
  (newVal) => {
    console.log("newVal>>>", newVal);
    emit("update:modelValue", newVal);
  },
  { immediate: true, deep: true }
);

最后有意思的是,我把 const model 改成 let model tmd 居然也正常了,这就让我百思不得其解了 ???

解决

其实上面 debugger 后,就确定了方向 肯定是emit('update:modelValue', newVal)这里出问题了,回到使用组件,把 v-model 拆解一下,此时还看不出来问题。

1688879457596.jpg

换成:modelValue="model" @update:model-value="update(e)"问题立马出现了,ts 已经提示了 model 是常量!

微信截图_20230709131250.png

这样问题就非常明了了,这就解释了 let 可以 const 不行,但你好歹报个错啊 ??? 坑死人不偿命,可见即使在 template 里面这样写@update:model-value="model = $event" ts 也无能为力! 回过头再来看看 ref 为啥可行呢?当改成 ref 时,

const update = (e) => {
  model.value = e;
};

update 是要.value 的,修改常量对象里面属性是正常的。再想想 ref 的变量在 template 中 vue 已经帮我们解过包了,v-model 语法糖拿着属性直接赋值并不会产生问题。而常量 reactive 则不能修改,也可以在在里面再包裹一层对象,但这样就有点冗余了。

总结

总结起来就是,const 定义的 reactive 对象,v-model 去更新整个对象的时候失败,常量不能更改,也没有给出任何报错或提示!