Синхронизация созданных элементов

В Tom Select создание новых значений реализуется через встроенный механизм create, позволяющий пользователю добавлять элементы, отсутствующие в текущем списке опций. При активации этого режима введённая строка преобразуется в объект опции и одновременно попадает в список выбранных значений (items) и в набор доступных опций (options), если это предусмотрено конфигурацией.

Базовая конфигурация создания:

new TomSelect('#select', {
  create: true
});

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

{
  value: "новое_значение",
  text: "новое_значение"
}

и добавляет его в коллекцию опций.

Ключевой момент заключается в том, что созданный элемент не является «временным» — он сразу становится частью состояния компонента, а значит участвует в фильтрации, отображении и сериализации.


Жизненный цикл созданного элемента

Жизненный цикл созданного элемента в Tom Select проходит несколько стадий:

  1. Ввод текста в input-компонент
  2. Срабатывание логики createFilter
  3. Генерация новой опции через create callback или встроенный механизм
  4. Добавление в options
  5. Добавление в items
  6. Генерация событий item_add и change

В момент создания Tom Select синхронизирует внутренние структуры:

  • this.options — полный набор доступных опций
  • this.items — выбранные значения
  • DOM-слой — визуальные теги

Любое несоответствие между этими слоями приводит к рассинхронизации отображения и состояния.


Конфигурация поведения создания элементов

Создание элементов управляется несколькими параметрами:

create

Определяет возможность добавления новых элементов.

create: true

Также допускается функция:

create: function(input) {
  return {
    value: input.toLowerCase(),
    text: input
  };
}

Функция позволяет нормализовать данные до попадания в модель.


createFilter

Определяет допустимость создания элемента на основе введённого значения.

createFilter: function(input) {
  return input.length > 2;
}

Фильтр часто используется для предотвращения создания пустых или некорректных значений.


createOnBlur

Автоматическое создание элемента при потере фокуса:

createOnBlur: true

При включении значение из input может быть преобразовано в элемент без явного подтверждения.


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

Внутренняя модель Tom Select разделяет данные на два слоя:

  • options — справочник всех известных элементов
  • items — активные выбранные значения

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

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


Синхронизация с сервером

Создание элементов почти всегда требует интеграции с backend. Основная задача — связать локально созданный элемент с серверной сущностью.

Типовой сценарий:

  1. Пользователь вводит значение
  2. Tom Select создаёт временный объект
  3. Выполняется запрос на сервер
  4. Сервер возвращает нормализованный объект с ID
  5. Локальный элемент обновляется

Пример базовой синхронизации

const ts = new TomSelect('#select', {
  create: function(input) {
    return {
      value: input,
      text: input,
      temp: true
    };
  },

  onItemAdd: function(value) {
    const option = this.options[value];

    if (option && option.temp) {
      fetch('/api/tags', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: option.text })
      })
      .then(res => res.json())
      .then(data => {
        this.updateOption(value, {
          value: data.id,
          text: data.name
        });

        this.removeItem(value);
        this.addItem(data.id);
      });
    }
  }
});

Оптимистическое обновление состояния

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

Преимущества:

  • мгновенный отклик интерфейса
  • отсутствие блокировки ввода

Недостатки:

  • необходимость отката при ошибке
  • возможные конфликты ID

Пример:

onItemAdd: function(value) {
  const option = this.options[value];

  if (!option || !option.temp) return;

  const tempId = value;

  fetch('/api/tags', {
    method: 'POST',
    body: JSON.stringify({ name: option.text })
  })
  .then(res => res.json())
  .then(data => {
    this.updateOption(tempId, {
      value: data.id,
      text: data.name
    });

    this.refreshItems();
  })
  .catch(() => {
    this.removeItem(tempId);
  });
}

Дедупликация и нормализация

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

Типовые стратегии:

Нормализация регистра

create: function(input) {
  const normalized = input.trim().toLowerCase();

  if (this.options[normalized]) return false;

  return {
    value: normalized,
    text: input.trim()
  };
}

Проверка существующих значений

createFilter: function(input) {
  const exists = Object.values(this.options)
    .some(opt => opt.text.toLowerCase() === input.toLowerCase());

  return !exists;
}

Обновление созданных элементов

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

Tom Select не имеет прямого «patch»-API, поэтому используется комбинация операций:

  • updateOption
  • removeItem
  • addItem
this.updateOption(oldValue, {
  value: newValue,
  text: newText
});

this.removeItem(oldValue);
this.addItem(newValue);

Важно учитывать, что изменение value требует пересборки items, так как идентификатор используется как ключ.


Восстановление состояния

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

Типовой сценарий:

const initialItems = [
  { id: 1, name: 'JavaScript' },
  { id: 2, name: 'Frontend' }
];

new TomSelect('#select', {
  options: initialItems.reduce((acc, item) => {
    acc[item.id] = {
      value: item.id,
      text: item.name
    };
    return acc;
  }, {}),

  items: initialItems.map(i => i.id)
});

Если среди сохранённых значений есть пользовательские элементы, не входящие в справочник, их необходимо предварительно добавить в options, иначе Tom Select не сможет корректно отобразить их в списке выбранных элементов.


Асинхронная загрузка и создание элементов

При использовании remote data (например, через load), создание элементов требует учёта гонок состояний.

Проблема возникает, когда:

  • пользователь создаёт элемент
  • одновременно выполняется загрузка опций с сервера
  • сервер возвращает список, не содержащий созданный элемент

Решение заключается в принудительном сохранении локально созданных значений:

load: function(query, callback) {
  fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(data => {
      const options = {};

      data.forEach(item => {
        options[item.id] = {
          value: item.id,
          text: item.name
        };
      });

      Object.assign(this.options, options);

      callback(data);
    });
}

Конфликты идентификаторов

Одним из критичных аспектов синхронизации является различие между временными и серверными идентификаторами.

Типовая модель:

  • временный ID: tmp_123
  • серверный ID: 42

При обновлении необходимо гарантировать:

  • перенос всех ссылок items
  • обновление options
  • отсутствие старого ключа

Игнорирование этого приводит к ситуации, когда визуально элемент существует, но логически недоступен для операций поиска или удаления.


Поведение при ошибках сети

При сбоях синхронизации возможны три стратегии:

  1. Удаление элемента (жёсткий откат)
  2. Сохранение локального состояния до повторной синхронизации
  3. Пометка элемента как несинхронизированного

Пример мягкого отката:

.catch(() => {
  this.updateOption(tempId, {
    value: tempId,
    text: option.text + ' (не сохранено)'
  });
});

Особенности работы с кастомными значениями

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

create: function(input) {
  return {
    value: crypto.randomUUID(),
    text: input,
    raw: {
      label: input,
      createdAt: Date.now()
    }
  };
}

Такая структура позволяет сохранять метаданные, но требует внешнего контроля при отправке на сервер, так как Tom Select оперирует только value и text.


Поведение событий при создании

Создание элемента вызывает последовательность событий:

  • option_add
  • item_add
  • change

При синхронизации важно различать их назначение:

  • option_add — изменение справочника
  • item_add — изменение выбранных значений
  • change — итоговое состояние

Неправильная реакция на option_add часто приводит к повторной отправке данных на сервер и циклическим запросам.


Управление пересозданием элементов

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

Ключевая стратегия — предварительная нормализация options:

  • сначала загружаются все известные серверные значения
  • затем добавляются локальные пользовательские
  • затем устанавливаются items

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