Отправка данных на сервер

В интерфейсах с использованием Choices.js ключевая задача отправки данных на сервер заключается в корректной сериализации выбранных значений и их согласовании с форматом API. Элемент <select> или <input> под управлением Choices.js продолжает оставаться стандартным DOM-элементом формы, что позволяет использовать нативную отправку формы или перехват данных через JavaScript.

Базовая структура данных зависит от режима работы компонента:

  • одиночный выбор: строка или число
  • множественный выбор: массив значений
  • объекты (при кастомных вариантах): массив объектов с value и label

Choices.js хранит внутреннее представление в виде массива объектов выбора:

[
  { value: '1', label: 'JavaScript', selected: true },
  { value: '2', label: 'TypeScript', selected: true }
]

Для отправки на сервер обычно извлекается только поле value.


Получение выбранных значений

Choices.js предоставляет API экземпляра, через который можно получить текущее состояние выбора:

const choices = new Choices('#skills');

const selected = choices.getValue(true);

Передача аргумента true возвращает упрощённый массив значений:

['1', '2']

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


Отправка через стандартную HTML-форму

В простейшем случае Choices.js не требует дополнительной логики отправки. Значение синхронизируется с исходным <select>:

<form action="/api/skills" method="POST">
  <select id="skills" name="skills[]" multiple>
    <option value="1">JavaScript</option>
    <option value="2">TypeScript</option>
  </select>

  <button type="submit">Send</button>
</form>

Инициализация:

new Choices('#skills');

При отправке формы браузер автоматически сформирует payload:

skills[]=1&skills[]=2

Перехват отправки формы и использование Fetch API

В современных SPA-подходах отправка часто выполняется вручную через fetch, что позволяет контролировать формат данных и обработку ответа.

const form = document.querySelector('#form');
const choices = new Choices('#skills');

form.addEventListener('submit', async (e) => {
  e.preventDefault();

  const payload = {
    skills: choices.getValue(true)
  };

  const response = await fetch('/api/skills', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(payload)
  });

  const result = await response.json();
});

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

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

Для этого используется событие change:

const choices = new Choices('#skills');

const sendUpdate = async (values) => {
  await fetch('/api/skills/update', {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ skills: values })
  });
};

document.querySelector('#skills').addEventListener('change', () => {
  sendUpdate(choices.getValue(true));
});

Дебаунс запросов при частых изменениях

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

const debounce = (fn, delay) => {
  let timeout;
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), delay);
  };
};

const sendUpdate = debounce((values) => {
  fetch('/api/skills/update', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ skills: values })
  });
}, 300);

Отправка данных в формате form-data

Некоторые серверные API ожидают multipart/form-data. В этом случае используется FormData.

const formData = new FormData();
const values = choices.getValue(true);

values.forEach(v => {
  formData.append('skills[]', v);
});

fetch('/api/skills', {
  method: 'POST',
  body: formData
});

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


Работа с объектными значениями

Choices.js позволяет хранить не только простые значения, но и сложные структуры:

new Choices('#users', {
  choices: [
    { value: '1', label: 'Alex', customProperties: { role: 'admin' } },
    { value: '2', label: 'Maria', customProperties: { role: 'editor' } }
  ]
});

При отправке можно включать дополнительные поля:

const data = choices.getValue().map(item => ({
  id: item.value,
  role: item.customProperties.role
}));

fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ users: data })
});

Интеграция с серверным поиском (AJAX choices)

В сценариях с динамическими данными Choices.js часто работает совместно с API поиска:

const choices = new Choices('#city', {
  searchEnabled: true
});

document.querySelector('#city').addEventListener('search', async (e) => {
  const query = e.detail.value;

  const response = await fetch(`/api/cities?q=${query}`);
  const data = await response.json();

  choices.setChoices(data, 'id', 'name', true);
});

Здесь сервер возвращает данные в формате:

[
  { "id": "10", "name": "Almaty" },
  { "id": "11", "name": "Astana" }
]

Согласование формата данных с backend

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

REST JSON API

{
  "skills": ["js", "ts"]
}

PHP / Laravel style

skills[]=js&skills[]=ts

GraphQL переменные

{
  "input": {
    "skills": ["js", "ts"]
  }
}

Choices.js не навязывает формат, поэтому преобразование выполняется на уровне JavaScript перед отправкой.


Обработка пустых значений

При отсутствии выбора важно контролировать поведение payload:

const values = choices.getValue(true);

const payload = {
  skills: values.length ? values : null
};

Альтернативный вариант — отправка пустого массива:

skills: []

Выбор стратегии зависит от логики серверной валидации.


Обновление состояния после ответа сервера

После успешной отправки сервер может возвращать актуализированные данные, которые синхронизируются с интерфейсом:

const response = await fetch('/api/skills', {
  method: 'POST',
  body: JSON.stringify({ skills: choices.getValue(true) })
});

const result = await response.json();

choices.clearStore();
choices.setValue(result.skills);

Валидация перед отправкой

Choices.js не выполняет строгую бизнес-валидацию, поэтому проверка выполняется вручную:

const values = choices.getValue(true);

if (values.length < 2) {
  throw new Error('Минимум два значения');
}

Дополнительно может проверяться наличие запрещённых значений:

const forbidden = ['admin'];

const valid = values.filter(v => !forbidden.includes(v));

Отправка данных в многокомпонентных формах

В сложных формах Choices.js часто используется совместно с другими input-элементами:

const payload = {
  username: document.querySelector('#username').value,
  skills: choices.getValue(true),
  role: document.querySelector('#role').value
};

fetch('/api/user', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(payload)
});

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


Обработка CSRF и авторизационных заголовков

В защищённых приложениях добавляются токены:

fetch('/api/skills', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': document.querySelector('meta[name="csrf"]').content
  },
  body: JSON.stringify({
    skills: choices.getValue(true)
  })
});

Для JWT используется заголовок Authorization:

headers: {
  'Authorization': `Bearer ${token}`
}

Оптимизация сетевого взаимодействия

При интенсивной работе с Choices.js важна минимизация количества запросов:

  • использование debounce при поиске
  • батчинг изменений
  • отправка только diff-значений
  • кеширование результатов поиска

Пример отправки только изменений:

let previous = [];

const diff = (current) =>
  current.filter(x => !previous.includes(x));

document.querySelector('#skills').addEventListener('change', () => {
  const current = choices.getValue(true);
  const changed = diff(current);

  previous = current;

  fetch('/api/skills/diff', {
    method: 'POST',
    body: JSON.stringify({ changed })
  });
});