Vue自定义输入组件:Watcher触发emit后modelValue未更新问题排查
核心问题原因
你的组件存在两个关键问题,导致父组件modelValue无法正确更新:
Vue模板渲染与原生DOM编辑冲突:你在
contenteditable的<span>中用{{ inputValue }}渲染内容,Vue的虚拟DOM会持续控制该元素的文本。当用户手动编辑时,Vue在下一次响应式更新会强制把span内容重置为inputValue的值,既会覆盖用户操作,也可能让innerText获取的值不准确,导致emit的更新无效。未同步父组件
modelValue到子组件内部状态:仅初始化时将props.modelValue赋值给inputValue,但没有监听props.modelValue的变化。父组件外部修改值时,子组件内部状态不会同步,双向绑定链路断裂。
修复后的完整代码
<template> <div class="col-span-12"> <div class="flex"> <span contenteditable="true" class="editable" :class="[fontBold ? 'font-bold' : '']" :placeholder="placeholder" @input="handleInput" @focusout="$emit('savingValue')" ref="editableSpan" ></span> </div> </div> </template> <script setup> import { ref, nextTick, onMounted, watch } from "vue"; const emit = defineEmits(["update:modelValue", "savingValue"]); const props = defineProps({ modelValue: { type: String, required: true, }, name: { type: String, default: "", }, type: { type: String, default: "text", }, required: { type: Boolean, default: false, }, disabled: { type: Boolean, default: false, }, label: { type: String, default: "", }, placeholder: { type: String, default: "", }, fontBold: { type: Boolean, default: false, }, }); const inputValue = ref(props.modelValue); const editableSpan = ref(null); // 监听父组件modelValue变化,同步到子组件内部状态和DOM watch(() => props.modelValue, (newVal) => { inputValue.value = newVal; updateSpanContent(); }); // 监听内部状态变化,通知父组件更新 watch(inputValue, (newValue) => { emit("update:modelValue", newValue); }); // 手动更新span内容,避免Vue模板渲染冲突 const updateSpanContent = async () => { await nextTick(); if (editableSpan.value) { editableSpan.value.innerText = inputValue.value; } }; // 组件挂载时初始化DOM内容 onMounted(() => { updateSpanContent(); }); const handleInput = () => { if (editableSpan.value) { inputValue.value = editableSpan.value.innerText; } }; </script> <style scoped> [contenteditable="true"]:empty { color: #bbb; } .editable { display: inline-block; padding: 3px 2px; outline: none; cursor: text; background-color: #edf3f1; white-space: nowrap; overflow: hidden; } [contenteditable="true"]:empty:before { content: attr(placeholder); } </style>
关键调整说明
移除模板渲染表达式:删掉span内的
{{ inputValue }},改用updateSpanContent手动控制DOM文本,彻底避免Vue虚拟DOM和用户原生编辑的冲突。添加props监听:新增对
props.modelValue的监听,确保父组件外部修改值时,子组件能同步更新内部状态和显示内容。使用nextTick确保DOM就绪:更新span内容前等待
nextTick,保证操作的是最新渲染完成的DOM元素。
额外注意事项
若需要保留用户输入的换行或空格格式,可将
innerText替换为textContent(仅保留纯文本格式)或innerHTML(注意防范XSS风险)。父组件需正确使用
v-model绑定,或手动指定:modelValue和@update:modelValue回调:<!-- 父组件正确用法 --> <CustomInput v-model="parentValue" /> <!-- 等价写法 --> <CustomInput :modelValue="parentValue" @update:modelValue="parentValue = $event" />
内容的提问来源于stack exchange,提问作者Emile Junior ADA

