Checkbox Options

Checkbox Options в Tom Select реализуются через механизм кастомного рендеринга опций и расширения поведения multi-select режима, при котором каждый элемент списка отображается с интерактивным чекбоксом. Этот режим не является отдельной встроенной сущностью ядра, а формируется через комбинацию настроек plugins, кастомных шаблонов render и управления состоянием выбранных значений.

Внутренняя модель выбора в библиотеке основана на двух слоях состояния:

  • raw value layer — хранит выбранные значения (обычно массив id)
  • UI layer — отображает выбранные элементы и список опций

Checkbox-поведение возникает, когда UI layer перестраивает каждую опцию как элемент, содержащий визуальный индикатор состояния выбора.

Ключевым фактором становится не изменение модели данных, а расширение представления каждой опции.

Включение множественного выбора

Checkbox-режим всегда опирается на multi-select поведение:

new TomSelect("#select", {
  maxItems: null,
  plugins: ['remove_button']
});

Однако сам факт multi-select не создаёт чекбоксы. Он лишь позволяет хранить массив значений, что необходимо для синхронизации состояния чекбокса и выбранных элементов.

Кастомизация render.option

Основной механизм реализации чекбоксов — переопределение render.option.

new TomSelect("#select", {
  plugins: ['remove_button'],
  render: {
    option: function(data, escape) {
      const selected = this.items.includes(data.value);

      return `
        <div class="ts-option">
          <label class="ts-checkbox">
            <input type="checkbox" ${selected ? 'checked' : ''} disabled />
            <span class="ts-label">${escape(data.text)}</span>
          </label>
        </div>
      `;
    }
  }
});

Здесь чекбокс не является управляемым input-элементом в классическом смысле. Он служит визуальным индикатором, а управление выбором остаётся за внутренней логикой селекта.

Синхронизация состояния чекбоксов

Важный аспект — синхронизация UI и состояния this.items. При изменении выбора библиотека пересоздаёт список отображаемых опций, что приводит к обновлению состояния чекбоксов.

Механизм работает следующим образом:

  1. Пользователь кликает по опции
  2. Вызывается внутренний метод addItem или removeItem
  3. Обновляется массив items
  4. UI перерисовывается через refreshOptions
  5. render.option заново вычисляет checked

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

Обработка кликов и предотвращение конфликтов

Если чекбокс внутри option остаётся интерактивным, возникает конфликт событий: клик по input может не совпадать с логикой выбора Tom Select. Поэтому используется один из двух подходов:

1. Отключённый input

<input type="checkbox" disabled />

Такой подход полностью делегирует управление библиотеке и исключает рассинхронизацию событий.

2. Перехват событий

render: {
  option: function(data, escape) {
    return `
      <div class="ts-option" data-value="${data.value}">
        <input type="checkbox" />
        <span>${escape(data.text)}</span>
      </div>
    `;
  }
},
onDropdownOpen: function() {
  this.dropdown.addEventListener('click', (e) => {
    const option = e.target.closest('.ts-option');
    if (!option) return;

    this.setActiveOption(option);
    this.onOptionSelect(option, e);
  });
}

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

Стилизация чекбоксов

Визуальная часть checkbox-mode обычно строится поверх стандартных CSS-селекторов:

.ts-option {
  display: flex;
  align-items: center;
  padding: 6px 10px;
}

.ts-checkbox {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
}

.ts-checkbox input[type="checkbox"] {
  pointer-events: none;
}

Ключевой момент — отключение pointer-events у input, чтобы клик всегда обрабатывался контейнером опции.

Интеграция с plugins системы

Tom Select использует систему плагинов, где checkbox-логика может быть оформлена как расширение.

Пример кастомного плагина:

TomSelect.define('checkbox_options', function(options) {
  const self = this;

  self.hook('render', 'option', function(data, html) {
    const checked = self.items.includes(data.value);

    return `
      <div class="ts-option">
        <input type="checkbox" ${checked ? 'checked' : ''} disabled>
        <span>${data.text}</span>
      </div>
    `;
  });
});

И подключение:

new TomSelect("#select", {
  plugins: ['checkbox_options']
});

Такой подход позволяет вынести визуальную логику из конфигурации и использовать её повторно.

Обновление состояния при динамических данных

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

new TomSelect("#select", {
  valueField: "id",
  labelField: "title",
  searchField: "title",
  load: function(query, callback) {
    fetch(`/api/items?q=${query}`)
      .then(res => res.json())
      .then(data => callback(data));
  },
  render: {
    option: function(data, escape) {
      const checked = this.items.includes(data.id);

      return `
        <div>
          <input type="checkbox" ${checked ? 'checked' : ''} disabled />
          <span>${escape(data.title)}</span>
        </div>
      `;
    }
  }
});

Здесь важно, что при каждой загрузке данных render-метод получает актуальное состояние items, что гарантирует корректное отображение чекбоксов даже при асинхронном обновлении списка.

Работа с удалением элементов

Checkbox UI тесно связан с механизмом удаления выбранных элементов через remove_button plugin. При удалении:

  • элемент удаляется из items
  • вызывается refreshItems()
  • обновляются все чекбоксы в списке

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

Оптимизация перерисовки

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

  • кэширование this.items в локальную переменную внутри render
  • минимизация DOM-строк в render.option
  • использование render.item отдельно от render.option
  • отключение лишних плагинов, влияющих на refresh cycle
render: {
  option: function(data, escape) {
    const isSelected = this.items.indexOf(data.value) !== -1;

    return `<div class="opt">
      <input type="checkbox" ${isSelected ? 'checked' : ''} disabled>
      <span>${escape(data.text)}</span>
    </div>`;
  }
}

Поведение при keyboard navigation

Checkbox UI не изменяет стандартную клавиатурную модель:

  • ArrowUp / ArrowDown — смена активной опции
  • Enter — выбор/снятие выбора
  • Space — альтернативный триггер выбора

Чекбоксы при этом не получают фокус, чтобы не ломать единый event flow внутри компонента.

Ограничения checkbox-подхода

Несмотря на визуальную простоту, подход имеет ограничения:

  • невозможность нативного двухстороннего binding input
  • необходимость полной синхронизации через render
  • потенциальные проблемы с accessibility при неправильной разметке
  • зависимость от перерисовки dropdown для актуализации состояния

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

Поведенческая модель выбора

В checkbox-режиме выбор элементов соответствует множественной конечной множественной структуре:

  • множество A — все доступные опции
  • подмножество B ⊆ A — выбранные элементы

Каждое действие пользователя выполняет операцию симметрической разности:

  • выбор → B = B ∪ {x}
  • снятие → B = B  {x}

UI чекбокса отображает принадлежность элемента множеству B, но не управляет им напрямую.