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

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 };

问题根源

  1. 事件绑定一次性失效:业务组件仅在onMounted中绑定一次事件,但封装组件的updateValue逻辑会在值变化时销毁并重建TomSelect实例(clonedEl.TomSelect.destroy()后重新init),重建后的新实例未重新绑定dropdown_close和item_remove事件,导致事件响应丢失。
  2. 实例引用被覆盖:封装组件用单个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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 10:57:00