Vue3中多TomSelect组件dropdown_close/item_remove事件失效求助
问题描述
在Vue3中使用自定义封装的TomSelect组件时,同一组件内多个实例绑定dropdown_close和item_remove事件后,未被操作的实例会失去这些事件的响应能力,无法触发报表查询逻辑,影响用户交互流畅度。
问题代码
业务组件代码
<template> <TomSelect v-model="filtroEmpPedido" class="w-full" refKey="tomSelectEmpPedido" id="tomSelectEmpPedido" multiple :options="{ plugins: { remove_button: { title: 'Remover', }, clear_button: { title: 'Remover Todos', }, }, }" > <option v-for="item in empresapedido" :key="item" :value="item.cd_emppedido"> {{ item.ds_empresa }} </option> </TomSelect> </template> <script setup> const tomSelectTransp = ref(); provide('bind[tomSelectTransp]', (el) => { tomSelectTransp.value = el; }); const tomSelectEmpPedido = ref(); provide('bind[tomSelectEmpPedido]', (el) => { tomSelectEmpPedido.value = el; }); onMounted(async () => { tomSelectTransp.value.TomSelect.on('dropdown_close', () => { rastreio(); }); tomSelectTransp.value.TomSelect.on('item_remove', () => { rastreio(); }); tomSelectEmpPedido.value.TomSelect.on('dropdown_close', () => { rastreio(); }); tomSelectEmpPedido.value.TomSelect.on('item_remove', () => { rastreio(); }); }); </script>
TomSelect组件代码(Main.vue)
<template> <select v-select-directive="{ props, emit, computedOptions }" class="tom-select" > <slot></slot> </select> </template> <script setup> import { computed, watch, toRaw, ref, onMounted, inject } from "vue"; import { setValue, init, updateValue } from "./index"; import dom from "@left4code/tw-starter/dist/js/dom"; const vSelectDirective = { mounted(el, { value }) { const clonedEl = dom(el).clone().insertAfter(el)[0]; dom(el).attr("hidden", true); setValue(clonedEl, value.props); init(el, clonedEl, value.props, value.emit, value.computedOptions); tomSelectRef.value = clonedEl; }, updated(el, { value }) { const clonedEl = dom(el).next()[0]; const modelValue = toRaw(value.props.modelValue); updateValue( el, clonedEl, modelValue, value.props, value.emit, value.computedOptions ); tomSelectRef.value = clonedEl; }, }; const props = defineProps({ options: { type: Object, default() { return {}; }, }, modelValue: { type: [String, Number, Array], default: "", }, refKey: { type: String, default: null, }, }); const emit = defineEmits(); const tomSelectRef = ref(); const bindInstance = () => { if (props.refKey) { const bind = inject(`bind[${props.refKey}]`); if (bind) { bind(tomSelectRef.value); } } }; const computedOptions = computed(() => { let options = { ...props.options, plugins: { dropdown_input: {}, ...props.options.plugins, }, }; if (Array.isArray(props.modelValue)) { options = { persist: false, create: true, onDelete: function (values) { return confirm( values.length > 1 ? "Are you sure you want to remove these " + values.length + " items?" : 'Are you sure you want to remove "' + values[0] + '"?' ); }, ...options, plugins: { remove_button: { title: "Remove this item", }, ...options.plugins, }, }; } return options; }); watch( computed(() => props.modelValue), () => { emit("change"); } ); onMounted(() => { bindInstance(); }); </script>
index.js代码
import dom from "@left4code/tw-starter/dist/js/dom"; import TomSelect from "tom-select"; import _, { clone } from "lodash"; const setValue = (el, props) => { if (props.modelValue.length) { dom(el).val(props.modelValue); } }; const init = (originalEl, clonedEl, props, emit, computedOptions) => { if (Array.isArray(props.modelValue)) { computedOptions = { onOptionAdd: function (value) { const newOption = document.createElement("option"); newOption.value = value; newOption.text = value; originalEl.add(newOption); emit("optionAdd", value); }, ...computedOptions, }; } clonedEl.TomSelect = new TomSelect(clonedEl, computedOptions); clonedEl.TomSelect.on("change", function (selectedItems) { emit( "update:modelValue", Array.isArray(selectedItems) ? [...selectedItems] : selectedItems ); }); }; const getOptions = (options, tempOptions = []) => { options.each(function (optionKey, optionEl) { if (optionEl instanceof HTMLOptGroupElement) { getOptions(dom(optionEl).children(), tempOptions); } else { tempOptions.push(optionEl); } }); return tempOptions; }; const updateValue = ( originalEl, clonedEl, modelValue, props, emit, computedOptions ) => { for (const [optionKey, option] of Object.entries( clonedEl.TomSelect.options )) { if ( !getOptions(dom(clonedEl).prev().children()).filter((optionEl) => { return optionEl.value === option.value; }).length ) { clonedEl.TomSelect.removeOption(option.value); } } dom(clonedEl.TomSelect.wrapper).css( "border-color", dom(originalEl).css("border-color") ); dom(clonedEl) .prev() .children() .each(function () { clonedEl.TomSelect.addOption({ text: dom(this).text(), value: dom(this).attr("value"), }); }); clonedEl.TomSelect.refreshOptions(false); if ( (!Array.isArray(modelValue) && modelValue !== clonedEl.TomSelect.getValue()) || (Array.isArray(modelValue) && !_.isEqual(modelValue, clonedEl.TomSelect.getValue())) ) { clonedEl.TomSelect.destroy(); dom(clonedEl).html(dom(clonedEl).prev().html()); setValue(clonedEl, props); init(originalEl, clonedEl, props, emit, computedOptions); } }; export { setValue, init, updateValue };
问题根源
- 事件绑定一次性失效:业务组件仅在
onMounted中绑定一次事件,但封装组件的updateValue逻辑会在值变化时销毁并重建TomSelect实例(clonedEl.TomSelect.destroy()后重新init),重建后的新实例未重新绑定dropdown_close和item_remove事件,导致事件响应丢失。 - 实例引用被覆盖:封装组件用单个
tomSelectRef存储实例,多个组件实例会互相覆盖这个引用,导致业务组件拿到的实例可能不是最新的。
解决方案
方案1:封装组件时暴露目标事件(推荐)
修改封装组件,将TomSelect原生事件通过Vue的emit暴露给父组件,业务组件直接监听组件事件,避免手动绑定丢失的问题。
步骤1:修改index.js,添加事件emit
在init函数中,新增对dropdown_close和item_remove事件的监听,并通过emit传递给父组件:
const init = (originalEl, clonedEl, props, emit, computedOptions) => { // ... 原有代码 clonedEl.TomSelect = new TomSelect(clonedEl, computedOptions); // On change clonedEl.TomSelect.on("change", function (selectedItems) { emit( "update:modelValue", Array.isArray(selectedItems) ? [...selectedItems] : selectedItems ); }); // 新增事件监听并emit clonedEl.TomSelect.on('dropdown_close', () => { emit('dropdown-close'); }); clonedEl.TomSelect.on('item_remove', () => { emit('item-remove'); }); };
步骤2:业务组件直接监听事件
移除手动绑定事件的代码,直接通过组件的@语法监听事件:
<template> <TomSelect v-model="filtroEmpPedido" class="w-full" refKey="tomSelectEmpPedido" id="tomSelectEmpPedido" multiple :options="{ plugins: { remove_button: { title: 'Remover', }, clear_button: { title: 'Remover Todos', }, }, }" @dropdown-close="rastreio" @item-remove="rastreio" > <option v-for="item in empresapedido" :key="item" :value="item.cd_emppedido"> {{ item.ds_empresa }} </option> </TomSelect> <!-- 另一个TomSelect实例同样处理 --> <TomSelect v-model="filtroTransp" class="w-full" refKey="tomSelectTransp" id="tomSelectTransp" multiple :options="{ plugins: { remove_button: { title: 'Remover', }, clear_button: { title: 'Remover Todos', }, }, }" @dropdown-close="rastreio" @item-remove="rastreio" > <!-- 选项内容 --> </TomSelect> </template> <script setup> import { ref } from 'vue'; const filtroEmpPedido = ref([]); const filtroTransp = ref([]); const rastreio = () => { // 报表查询逻辑 }; // 移除原有provide和onMounted中的事件绑定代码 </script>
方案2:修复实例引用与事件重绑定(兼容现有代码)
如果不想修改组件封装逻辑,可通过监听实例变化,在实例重建后重新绑定事件。
步骤1:封装组件修改实例存储
将单个tomSelectRef改为对象存储多个实例,并在updated时重新执行绑定:
<script setup> // ... 原有代码 // 替换单个ref为对象,存储多个实例 const tomSelectRefs = ref({}); const bindInstance = () => { if (props.refKey) { const bind = inject(`bind[${props.refKey}]`); if (bind) { tomSelectRefs.value[props.refKey] = tomSelectRef.value; bind(tomSelectRef.value); } } }; // 在updated后重新绑定实例 onUpdated(() => { bindInstance(); }); // ... 原有代码 </script>
步骤2:业务组件监听实例变化重绑事件
使用watch监听实例引用,当实例更新时重新绑定事件:
<script setup> import { ref, watch, onMounted } from 'vue'; const tomSelectTransp = ref(); provide('bind[tomSelectTransp]', (el) => { tomSelectTransp.value = el; }); const tomSelectEmpPedido = ref(); provide('bind[tomSelectEmpPedido]', (el) => { tomSelectEmpPedido.value = el; }); // 封装事件绑定逻辑 const bindEvents = (instance) => { if (!instance?.TomSelect) return; // 先移除原有绑定避免重复触发 instance.TomSelect.off('dropdown_close'); instance.TomSelect.off('item_remove'); // 重新绑定事件 instance.TomSelect.on('dropdown_close', rastreio); instance.TomSelect.on('item_remove', rastreio); }; onMounted(() => { bindEvents(tomSelectTransp.value); bindEvents(tomSelectEmpPedido.value); }); // 监听实例变化,重新绑定事件 watch(tomSelectTransp, (newEl) => { bindEvents(newEl); }); watch(tomSelectEmpPedido, (newEl) => { bindEvents(newEl); }); const rastreio = () => { // 报表查询逻辑 }; </script>
内容的提问来源于stack exchange,提问作者Robinho de Morais
相关产品推荐
相关产品推荐

