You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue自定义输入组件:Watcher触发emit后modelValue未更新问题排查

问题排查与修复:Vue contenteditable组件双向绑定失效

核心问题原因

你的组件存在两个关键问题,导致父组件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>

关键调整说明

  1. 移除模板渲染表达式:删掉span内的{{ inputValue }},改用updateSpanContent手动控制DOM文本,彻底避免Vue虚拟DOM和用户原生编辑的冲突。

  2. 添加props监听:新增对props.modelValue的监听,确保父组件外部修改值时,子组件能同步更新内部状态和显示内容。

  3. 使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 05:24:57