vue3 表单封装遇到的一个有意思的问题
前言
最近在用 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 的值时,发现值全没给上。
原因分析
这里经过两年半的尝试,终于发现在定义 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 拆解一下,此时还看不出来问题。
换成:modelValue="model" @update:model-value="update(e)"
问题立马出现了,ts 已经提示了 model 是常量!
这样问题就非常明了了,这就解释了 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 去更新整个对象的时候失败,常量不能更改,也没有给出任何报错或提示!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性