注册
web

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的值时,发现值全没给上。


微信截图_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 去更新整个对象的时候失败,常量不能更改,也没有给出任何报错或提示!


唉!今年太难了。前端路漫漫其修远兮,还需

作者:Pluto5280
来源:juejin.cn/post/7253453908039123005
更加卷地而行!😵😵😵

0 个评论

要回复文章请先登录注册