Обработка создания на сервере

Механизм create и его роль в серверной архитектуре

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

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

Основные задачи серверной обработки создания:

  • генерация уникального идентификатора записи;
  • проверка дубликатов;
  • валидация пользовательского ввода;
  • возврат структурированного объекта в формате, понятном Tom Select;
  • обработка конкурентных запросов.

Базовая активация режима создания

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

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

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


Серверный сценарий через create с AJAX

Для подключения серверной логики используется комбинация create и событийного перехвата:

new TomSelect("#select", {
  create: true,

  onOptionAdd: function(value, data) {
    // локальный хук, не предназначенный для async-логики
  }
});

Но для полноценной серверной обработки используется паттерн с перехватом создания через create как функцию:

new TomSelect("#select", {
  create: function(input, callback) {
    fetch("/api/tags/create", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({ name: input })
    })
    .then(res => res.json())
    .then(data => {
      callback({
        value: data.id,
        text: data.name
      });
    })
    .catch(() => {
      callback(false);
    });
  }
});

В этой модели:

  • input — строка, введённая пользователем;
  • callback — функция завершения создания;
  • сервер возвращает объект с id и name.

Формат ответа сервера

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

Пример ответа:

{
  "id": 125,
  "name": "Новая категория"
}

Клиент преобразует его в:

{
  value: 125,
  text: "Новая категория"
}

Реализация на Node.js (Express)

Серверная часть должна обеспечивать проверку и сохранение данных:

app.post("/api/tags/create", async (req, res) => {
  const name = req.body.name.trim();

  if (!name) {
    return res.status(400).json({ error: "Empty value" });
  }

  const exists = await db.tags.findOne({ name });

  if (exists) {
    return res.json({
      id: exists.id,
      name: exists.name
    });
  }

  const created = await db.tags.insert({
    name
  });

  res.json({
    id: created.id,
    name: created.name
  });
});

Обработка дубликатов на сервере

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

Используются стратегии:

1. Уникальный индекс

CREATE UNIQUE INDEX unique_tag_name ON tags(name);

2. Проверка перед вставкой

const existing = await db.tags.findOne({ name });
if (existing) return existing;

3. UPSERT (предпочтительный вариант)

const result = await db.tags.upsert({
  where: { name },
  update: {},
  create: { name }
});

Асинхронное создание и UX-поведение

Tom Select ожидает завершения операции создания через callback. Это означает, что интерфейс блокируется до получения ответа сервера.

Важно учитывать:

  • задержки сети;
  • возможные ошибки;
  • отмену операции пользователем.

Пример обработки ошибок:

create: function(input, callback) {
  fetch("/api/tags/create", {
    method: "POST",
    body: JSON.stringify({ name: input })
  })
  .then(r => {
    if (!r.ok) throw new Error("Server error");
    return r.json();
  })
  .then(data => {
    callback({ value: data.id, text: data.name });
  })
  .catch(() => {
    callback(false);
  });
}

Если передать false, Tom Select отменяет создание элемента.


Использование debounce при частом создании

При быстром вводе пользователь может инициировать множественные запросы. Для оптимизации применяется debounce-логика:

function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

Использование:

const createRemote = debounce((input, callback) => {
  fetch("/api/create", {
    method: "POST",
    body: JSON.stringify({ name: input })
  })
  .then(r => r.json())
  .then(data => callback({ value: data.id, text: data.name }));
}, 300);

Интеграция с load и create одновременно

В сложных сценариях создание новых элементов сочетается с удалённой загрузкой:

new TomSelect("#select", {
  load: function(query, callback) {
    fetch(`/api/search?q=${encodeURIComponent(query)}`)
      .then(res => res.json())
      .then(callback);
  },

  create: function(input, callback) {
    fetch("/api/create", {
      method: "POST",
      body: JSON.stringify({ name: input })
    })
    .then(res => res.json())
    .then(data => {
      callback({ value: data.id, text: data.name });
    });
  }
});

В этом случае система работает как гибрид:

  • load отвечает за поиск существующих значений;
  • create отвечает за добавление новых.

Форматирование данных для корректной интеграции

Tom Select требует строго определённый формат:

{
  value: "unique_id",
  text: "Отображаемое имя"
}

Дополнительные поля допускаются, но не используются ядром:

{
  value: 42,
  text: "Категория",
  meta: {
    created_at: "2026-01-01"
  }
}

Обработка состояния гонки (race condition)

При серверном создании возможна ситуация, когда:

  • пользователь отправляет запрос;
  • сервер обрабатывает дольше обычного;
  • пользователь создаёт тот же элемент повторно.

Решение заключается в:

  • блокировке повторного ввода;
  • проверке существования записи;
  • идемпотентности API.

Пример защиты:

let creating = false;

create: function(input, callback) {
  if (creating) return callback(false);

  creating = true;

  fetch("/api/create", {
    method: "POST",
    body: JSON.stringify({ name: input })
  })
  .then(r => r.json())
  .then(data => {
    creating = false;
    callback({ value: data.id, text: data.name });
  })
  .catch(() => {
    creating = false;
    callback(false);
  });
}

Возврат уже существующих сущностей

Часто сервер возвращает уже существующую запись вместо создания новой. Это позволяет поддерживать консистентность данных:

if (existing) {
  return res.json({
    id: existing.id,
    name: existing.name,
    created: false
  });
}

Клиент может игнорировать флаг created, но он полезен для аналитики и UI-логики.


Ограничения и контроль ввода

Серверная часть обязана фильтровать ввод:

  • минимальная длина строки;
  • запрещённые символы;
  • лимиты на количество создаваемых элементов;
  • нормализация регистра.

Пример:

const normalized = name
  .trim()
  .toLowerCase()
  .replace(/\s+/g, " ");

Расширенная интеграция с авторизацией

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

fetch("/api/tags/create", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ name: input })
});

На сервере это позволяет:

  • привязывать записи к пользователю;
  • ограничивать права создания;
  • вести аудит изменений.

Итоговая модель поведения

Серверная обработка создания в связке с Tom Select формирует следующую архитектурную цепочку:

  • ввод пользователя;
  • инициирование create функции;
  • отправка запроса на сервер;
  • валидация и проверка дубликатов;
  • сохранение в базе данных;
  • возврат структурированного объекта;
  • интеграция нового значения в список без перезагрузки интерфейса.