Vue компонент

Использование Tom Sel ect внутри Vue.js требует учета реактивной модели фреймворка и жизненного цикла DOM-элементов. Библиотека напрямую манипулирует DOM, тогда как Vue опирается на виртуальный DOM, что создает необходимость синхронизации состояния и корректного управления инициализацией/уничтожением инстанса.

Ключевая задача интеграции заключается в том, чтобы:

  • инициализировать Tom Sel ect только после монтирования элемента;
  • синхронизировать выбранные значения с состоянием компонента;
  • корректно обновлять список опций при изменении данных;
  • уничтожать экземпляр при размонтировании компонента.

Базовый компонент-обертка

Интеграция строится вокруг обертки над нативным <select> элементом. Vue управляет данными, Tom Select управляет отображением.

<template>
  <select ref="select">
    <option
      v-for="option in options"
      :key="option.value"
      :value="option.value"
    >
      {{ option.label }}
    </option>
  </select>
</template>
import TomSelect fr om "tom-select";

export default {
  name: "TomSelectWrapper",

  props: {
    modelValue: {
      type: [String, Array, Number],
      default: null
    },
    options: {
      type: Array,
      default: () => []
    },
    settings: {
      type: Object,
      default: () => ({})
    }
  },

  emits: ["update:modelValue"],

  data() {
    return {
      instance: null
    };
  }
};

Инициализация в жизненном цикле компонента

Инициализация выполняется в mounted, поскольку DOM-элемент должен существовать до создания экземпляра.

mounted() {
  this.initTomSelect();
}
methods: {
  initTomSelect() {
    if (this.instance) return;

    this.instance = new TomSelect(this.$refs.select, {
      ...this.settings,

      onChange: (value) => {
        this.$emit("update:modelValue", value);
      }
    });

    if (this.modelValue !== undefined && this.modelValue !== null) {
      this.instance.setValue(this.modelValue, true);
    }
  }
}

Особенность поведения заключается в том, что setValue должен вызываться после полной инициализации, иначе возможна потеря синхронизации.


Реактивная синхронизация v-model

Vue-компонент должен реагировать на изменения входного значения извне. Для этого используется watcher:

watch: {
  modelValue(newValue) {
    if (!this.instance) return;

    const current = this.instance.getValue();
    if (String(current) !== String(newValue)) {
      this.instance.setValue(newValue, true);
    }
  }
}

Флаг silent (второй аргумент true) предотвращает повторный вызов события onChange, что исключает цикл обновлений.


Обновление списка опций

При динамическом изменении массива options необходимо пересоздавать или обновлять содержимое Tom Sel ect. Прямое изменение DOM нежелательно, поэтому используется API библиотеки.

watch: {
  options: {
    deep: true,
    handler(newOptions) {
      if (!this.instance) return;

      this.instance.clearOptions();

      newOptions.forEach(opt => {
        this.instance.addOption(opt);
      });

      this.instance.refreshOptions(false);
    }
  }
}

Подход через clearOptions и addOption обеспечивает согласованность внутреннего состояния.


Уничтожение экземпляра

При размонтировании компонента важно освободить ресурсы и удалить обработчики событий.

beforeUnmount() {
  this.destroyTomSelect();
}
methods: {
  destroyTomSelect() {
    if (!this.instance) return;

    this.instance.destroy();
    this.instance = null;
  }
}

Отсутствие корректного уничтожения приводит к утечкам памяти и дублированию обработчиков при повторном монтировании.


Асинхронная загрузка данных

Tom Select поддерживает асинхронный режим через load callback, что особенно важно при работе с API.

initTomSelect() {
  this.instance = new TomSelect(this.$refs.select, {
    ...this.settings,

    load: (query, callback) => {
      if (!query.length) return callback();

      fetch(`/api/search?q=${encodeURIComponent(query)}`)
        .then(res => res.json())
        .then(data => {
          callback(data.items);
        })
        .catch(() => callback());
    }
  });
}

Vue-компонент при этом не должен хранить промежуточное состояние загрузки, если это не требуется UI-логикой. Ответственность делится: Vue управляет данными формы, Tom Select — поисковым UI.


Интеграция с v-model в Vue 3 (Composition API)

В Composition API контроль состояния становится более явным через ref.

import { ref, onMounted, onBeforeUnmount, watch } fr om "vue";
import TomSelect fr om "tom-select";

export default {
  props: {
    modelValue: [String, Array, Number],
    options: Array
  },

  emits: ["update:modelValue"],

  setup(props, { emit }) {
    const selectRef = ref(null);
    let instance = null;

    onMounted(() => {
      instance = new TomSelect(selectRef.value, {
        onChange(value) {
          emit("update:modelValue", value);
        }
      });

      if (props.modelValue) {
        instance.setValue(props.modelValue, true);
      }
    });

    watch(() => props.modelValue, (val) => {
      if (instance && val !== instance.getValue()) {
        instance.setValue(val, true);
      }
    });

    watch(() => props.options, (opts) => {
      if (!instance) return;

      instance.clearOptions();
      opts.forEach(o => instance.addOption(o));
      instance.refreshOptions(false);
    }, { deep: true });

    onBeforeUnmount(() => {
      if (instance) instance.destroy();
    });

    return {
      selectRef
    };
  }
};

Обработка множественного выбора

В режиме plugins: ['remove_button'] и maxItems > 1 поведение значения становится массивом.

this.instance = new TomSelect(this.$refs.select, {
  maxItems: 5,
  plugins: ["remove_button"],
  onChange: (values) => {
    this.$emit("update:modelValue", values);
  }
});

Особое внимание требуется при синхронизации: Vue может передавать строку или массив, поэтому нормализация данных обязательна.


Работа с асинхронными props и задержками обновления

При загрузке данных с сервера возможно состояние, при котором options приходят позже, чем происходит инициализация инстанса. В таких случаях требуется отложенное заполнение:

mounted() {
  this.initTomSelect();

  if (this.options.length) {
    this.syncOptions(this.options);
  }
}
methods: {
  syncOptions(options) {
    if (!this.instance) return;

    this.instance.clearOptions();
    this.instance.addOptions(options);
    this.instance.refreshOptions(false);
  }
}

Кастомизация отображения элементов

Tom Select позволяет переопределять шаблоны рендера элементов через render.

this.instance = new TomSelect(this.$refs.select, {
  render: {
    option: (data, escape) => {
      return `<div class="option-item">
        <span class="label">${escape(data.label)}</span>
      </div>`;
    },

    item: (data, escape) => {
      return `<div class="selected-item">
        ${escape(data.label)}
      </div>`;
    }
  }
});

При использовании в Vue важно избегать конфликта между виртуальным DOM и HTML-строками: шаблоны Tom Select полностью изолированы от Vue render pipeline.


Обработка внешнего сброса состояния

При необходимости полного сброса компонента требуется последовательность действий:

reset() {
  if (!this.instance) return;

  this.instance.clear(true);
  this.$emit("update:modelValue", null);
}

Флаг true предотвращает лишние события изменения.


Типовые ошибки интеграции

Основные проблемы возникают из-за несоответствия жизненных циклов Vue и Tom Select:

  • инициализация до mount приводит к отсутствию DOM-узла;
  • отсутствие watcher на modelValue вызывает десинхронизацию состояния;
  • прямое изменение <option> через Vue без обновления Tom Select приводит к рассинхронизации UI;
  • отсутствие destroy() вызывает накопление экземпляров при повторном входе на страницу.

Поведение в SSR-окружении

В серверном рендеринге экземпляр Tom Select должен инициализироваться только на клиенте. Проверка окружения обязательна:

if (typeof window !== "undefined") {
  this.initTomSelect();
}

SSR-рендер ограничивается выводом стандартного <select>, который затем гидратируется на клиенте.


Управление фокусом и UX-аспекты

При использовании в формах важно учитывать поведение фокуса:

this.instance.on("focus", () => {
  // кастомная логика при необходимости
});

this.instance.on("blur", () => {
  this.$emit("blur");
});

Vue-компонент может пробрасывать эти события дальше, сохраняя единый интерфейс формы.


Синхронизация с внешними стореджами состояния

При использовании Vuex или Pinia состояние может быть источником истины. В этом случае Tom Select становится представлением:

  • store обновляет modelValue;
  • watcher синхронизирует значение;
  • onChange обновляет store через emit.

Такой подход исключает расхождения состояния между UI и глобальным хранилищем.