Организация кода

При использовании Slim Sel ect в реальных проектах ключевым фактором становится не сама инициализация компонента, а организация кода вокруг неё. Библиотека предоставляет простой API, но при масштабировании интерфейсов с десятками селектов хаотичное создание экземпляров быстро приводит к дублированию логики, утечкам состояния и сложностям при сопровождении.

Базовый принцип организации заключается в разделении ответственности: конфигурация, инициализация, управление жизненным циклом и взаимодействие с DOM должны быть изолированы друг от друга.

import SlimSelect fr om 'slim-select';

const createSelect = (selector, config = {}) => {
  return new SlimSelect({
    select: selector,
    ...config
  });
};

export default createSelect;

Такой подход позволяет централизовать создание экземпляров и избежать прямых вызовов new SlimSelect по всему проекту.


Централизованное управление экземплярами

При наличии нескольких селектов необходимо хранить ссылки на их экземпляры. Это обеспечивает возможность обновления, уничтожения и повторной инициализации без пересоздания DOM-структуры.

const selects = new Map();

export const registerSelect = (id, instance) => {
  selects.set(id, instance);
};

export const getSelect = (id) => selects.get(id);

export const destroySelect = (id) => {
  const instance = selects.get(id);
  if (instance) {
    instance.destroy();
    selects.delete(id);
  }
};

Использование структуры Map предпочтительнее обычного объекта, поскольку позволяет использовать любые ключи и обеспечивает более предсказуемую работу при динамических ключах.


Инициализация через фабрики компонентов

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

export const initCountrySelect = (selector) => {
  return new SlimSelect({
    select: selector,
    placeholder: 'Выбор страны',
    allowDeselect: true
  });
};

export const initCitySelect = (selector, countryId) => {
  return new SlimSelect({
    select: selector,
    placeholder: 'Выбор города',
    ajax: (search, callback) => {
      fetch(`/api/cities?country=${countryId}&q=${search}`)
        .then(res => res.json())
        .then(data => {
          callback(data);
        });
    }
  });
};

Разделение на специализированные фабрики снижает связность кода и упрощает тестирование отдельных конфигураций.


Изоляция конфигурации

Конфигурационные параметры Slim Select часто разрастаются: placeholder, search, ajax, events, styling hooks. Вынос конфигурации в отдельные объекты позволяет избежать “размазывания” логики по проекту.

export const selectConfig = {
  base: {
    showSearch: true,
    allowDeselect: false
  },

  searchable: {
    searchText: 'Поиск...',
    searchPlaceholder: 'Введите текст'
  },

  single: {
    allowDeselect: false,
    hideSelectedOption: true
  }
};

Далее конфигурации комбинируются при инициализации:

const config = {
  ...selectConfig.base,
  ...selectConfig.searchable
};

new SlimSelect({
  select: '#mySelect',
  ...config
});

При необходимости можно использовать функцию глубокого объединения конфигураций для более сложных структур.


Управление жизненным циклом

Slim Select создаёт собственную обёртку над <select> элементом, поэтому важно корректно управлять его жизненным циклом, особенно в SPA-приложениях.

Типичный цикл включает:

  • инициализацию при монтировании компонента;
  • обновление при изменении данных;
  • уничтожение при размонтировании.
let instance = null;

export const mountSelect = (selector, options) => {
  if (instance) {
    instance.destroy();
  }

  instance = new SlimSelect({
    select: selector,
    ...options
  });

  return instance;
};

export const unmountSelect = () => {
  if (instance) {
    instance.destroy();
    instance = null;
  }
};

Игнорирование этапа destroy() приводит к накоплению DOM-обработчиков и дублированию событий.


Организация событий и обработчиков

События Slim Select часто используются для синхронизации состояния с внешними системами. При неправильной организации они быстро превращаются в неконтролируемые цепочки вызовов.

Рекомендуется централизовать обработчики:

const handlers = {
  onChange: (info) => {
    console.log('Selected:', info);
  },

  onOpen: () => {
    console.log('Select opened');
  }
};

export const createSelectWithEvents = (selector) => {
  return new SlimSelect({
    select: selector,
    events: {
      afterChange: handlers.onChange,
      open: handlers.onOpen
    }
  });
};

Вынос логики обработчиков позволяет повторно использовать их между различными экземплярами.


Динамическое обновление данных

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

const updateOptions = (instance, data) => {
  instance.setData(
    data.map(item => ({
      text: item.name,
      value: item.id
    }))
  );
};

Организация слоя обновления данных отдельно от UI-инициализации позволяет избежать смешивания бизнес-логики и представления.


Модульная структура проекта

При росте проекта логично разделить Slim Select-логику на отдельные слои:

/selects
  /core
    createSelect.js
    destroySelect.js
  /configs
    baseConfig.js
    selectConfig.js
  /factories
    countrySelect.js
    citySelect.js
  /services
    updateOptions.js
    fetchOptions.js

Такое разделение упрощает масштабирование и поддержку, особенно в командах, где разные разработчики работают над разными типами селектов.


Использование Slim Select в рамках компонентов UI

При интеграции в компонентный подход (React-подобный или vanilla components) важно отделять DOM-узел от логики библиотеки.

class SelectComponent {
  constructor(selector) {
    this.selector = selector;
    this.instance = null;
  }

  mount() {
    this.instance = new SlimSelect({
      select: this.selector
    });
  }

  update(data) {
    this.instance.setData(data);
  }

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

Такая инкапсуляция позволяет управлять экземпляром как единым объектом, не распространяя API библиотеки по всему приложению.


Поддержка нескольких экземпляров на странице

При работе с формами, содержащими повторяющиеся блоки (например, динамические строки таблицы), важно избегать конфликтов идентификаторов и состояния.

const initMultipleSelects = (selectors) => {
  return selectors.map(selector => {
    return new SlimSelect({
      select: selector
    });
  });
};

Управление коллекцией экземпляров позволяет централизованно выполнять массовые операции:

instances.forEach(instance => instance.destroy());

Отложенная инициализация

В интерфейсах с табами или модальными окнами часто используется ленивое создание экземпляров для оптимизации производительности.

const lazyInitSelect = (selector) => {
  let instance = null;

  const init = () => {
    if (!instance) {
      instance = new SlimSelect({ select: selector });
    }
  };

  return { init };
};

Инициализация выполняется только при первом обращении к элементу, что снижает начальную нагрузку на DOM.


Работа с повторной инициализацией

Повторное создание Slim Select без уничтожения предыдущего экземпляра приводит к конфликтам отображения. Поэтому стандартный паттерн включает защиту от дублирования:

const safeInit = (selector, config) => {
  const el = document.querySelector(selector);

  if (el && el.slim) {
    el.slim.destroy();
  }

  return new SlimSelect({
    select: selector,
    ...config
  });
};

Такой подход особенно важен при работе с динамическими формами и AJAX-обновлениями интерфейса.


Разделение бизнес-логики и UI-слоя

Организация кода вокруг Slim Select должна исключать смешивание бизнес-правил (фильтрация, зависимости, вычисления) с визуальной частью.

Плохая практика — выполнение API-запросов прямо внутри конфигурации компонента без промежуточного слоя.

Правильная архитектура использует сервис:

export const fetchCountries = async () => {
  const res = await fetch('/api/countries');
  return res.json();
};

export const mapToOptions = (items) => {
  return items.map(i => ({
    text: i.name,
    value: i.id
  }));
};

И отдельно UI-инициализацию:

fetchCountries().then(data => {
  new SlimSelect({
    select: '#country',
    data: mapToOptions(data)
  });
});

Такое разделение делает систему предсказуемой и тестируемой без зависимости от UI-слоя.