Синхронизация значений

Разделение отображаемого текста и фактического значения

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

В Awesomplete список может содержать строки или объекты, но базовая реализация ориентирована на строковые значения. При необходимости разделения отображаемого и фактического значения вводится модель «label/value»:

  • label — текст, отображаемый пользователю
  • value — значение, используемое в логике приложения или отправке формы

Проблема синхронизации возникает в момент выбора элемента: input должен показывать label, но хранить value либо параллельно, либо косвенно через скрытое поле.


Базовый механизм подмены значения при выборе

В стандартном поведении Awesomplete при выборе элемента происходит запись строки в input.value. Это удобно для простых списков, но недостаточно для структурированных данных.

Ключевая точка расширения — свойство replace, которое определяет, каким образом выбранный элемент преобразуется в значение поля.

const input = document.querySelector("#city");

new Awesomplete(input, {
  list: [
    "Almaty",
    "Astana",
    "Shymkent"
  ],

  replace: function (suggestion) {
    this.input.value = suggestion;
  }
});

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

Однако при переходе к объектной модели список меняет структуру.


Использование объектов и раздельной модели данных

Расширенная конфигурация использует массив объектов:

const list = [
  { label: "Алматы", value: "ALA" },
  { label: "Астана", value: "NQZ" },
  { label: "Шымкент", value: "CIT" }
];

При этом стандартный механизм Awesomplete не интерпретирует объекты напрямую без дополнительной обработки. Требуется нормализация списка:

new Awesomplete(input, {
  list: list.map(item => item.label)
});

На этом этапе теряется связь с value, поэтому вводится дополнительный слой синхронизации.


Связка input и скрытого поля

Наиболее устойчивый подход — использование скрытого input для хранения фактического значения.

<input id="city-label" />
<input type="hidden" id="city-value" name="city_code" />

Логика синхронизации:

const input = document.querySelector("#city-label");
const hidden = document.querySelector("#city-value");

const data = [
  { label: "Алматы", value: "ALA" },
  { label: "Астана", value: "NQZ" },
  { label: "Шымкент", value: "CIT" }
];

new Awesomplete(input, {
  list: data.map(x => x.label),

  replace: function (text) {
    const selected = data.find(x => x.label === text);

    this.input.value = selected.label;
    hidden.value = selected.value;
  }
});

Здесь синхронизация разделена на два уровня:

  1. UI-слой (input) получает label
  2. Data-слой (hidden input) получает value

Проблема десинхронизации при ручном вводе

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

Решение — сброс значения при изменении input:

input.addEventListener("input", function () {
  hidden.value = "";
});

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


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

Awesomplete предоставляет возможность перехвата момента выбора через awesomplete-selectcomplete.

input.addEventListener("awesomplete-selectcomplete", function (e) {
  const value = e.text.value || e.text.label || e.text;

  const selected = data.find(x => x.label === value);

  hidden.value = selected ? selected.value : "";
});

Этот подход снижает зависимость от replace, концентрируя всю логику синхронизации в одном месте.


Нормализация данных через источник list

Если список формируется динамически, например через API, структура часто уже содержит пары label/value:

fetch("/cities")
  .then(r => r.json())
  .then(items => {
    const awesompleteList = items.map(i => i.label);

    const instance = new Awesomplete(input, {
      list: awesompleteList
    });
  });

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

Оптимальная структура:

const state = {
  items: [],
  index: new Map()
};

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

state.items = data;
state.index = new Map(data.map(i => [i.label, i.value]));

Синхронизация через индексную карту

Использование Map устраняет необходимость поиска через find, что особенно важно при больших списках.

new Awesomplete(input, {
  list: data.map(i => i.label),

  replace: function (text) {
    this.input.value = text;
    hidden.value = state.index.get(text) || "";
  }
});

Здесь синхронизация становится O(1) по сложности.


Потеря синхронизации при кастомном рендеринге

При использовании кастомных элементов списка (через модификацию item или ul) может нарушаться связь между отображением и данными.

new Awesomplete(input, {
  list: data,

  item: function (text, input) {
    const li = document.createElement("li");
    li.textContent = text.label;
    li.setAttribute("data-value", text.value);
    return li;
  }
});

В таком случае стандартный механизм передачи строки перестаёт работать, и синхронизация должна опираться на data-* атрибуты DOM.


Использование data-* атрибутов как слоя синхронизации

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

item: function (data) {
  const li = document.createElement("li");
  li.textContent = data.label;
  li.dataset.value = data.value;
  return li;
}

И затем:

input.addEventListener("awesomplete-selectcomplete", function () {
  const listItem = document.querySelector(".awesomplete li[aria-selected='true']");
  hidden.value = listItem?.dataset.value || "";
});

Этот подход менее прямолинеен, но полезен при сложных UI-модификациях.


Конфликты между replace и событиями

При одновременном использовании replace и обработчиков событий возникает риск двойной синхронизации:

  • replace изменяет input.value
  • событие selectcomplete также может менять значения

Результатом становится перезапись данных в неправильном порядке.

Структурное решение — централизовать логику:

  • либо полностью через replace
  • либо полностью через события

Смешанная модель требует строгого порядка выполнения:

replace: function (text) {
  this.input.value = text;
},

input.addEventListener("awesomplete-selectcomplete", sync);

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

При расширении до мультивыбора проблема усложняется: одно поле input больше не отражает одно значение.

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

input (label buffer) + hidden (serialized values)

Синхронизация:

const values = [];

function sync() {
  hidden.value = JSON.stringify(values);
}

Добавление значения:

input.addEventListener("awesomplete-selectcomplete", function (e) {
  const selected = state.index.get(e.text) || e.text;

  values.push(selected);
  sync();
});

Контроль целостности данных

Основная задача синхронизации — поддержание согласованного состояния между:

  • визуальным текстом
  • внутренними кодами
  • сериализованными данными формы

Нарушение этого баланса проявляется в:

  • отправке некорректных идентификаторов
  • рассинхронизации label/value
  • невозможности восстановить состояние после reload

Для стабилизации состояния используется принцип единого источника данных (single source of truth), где либо:

  • input управляет состоянием полностью
  • либо внешний объект управляет input через явную синхронизацию

Поток данных при выборе элемента

Типовой жизненный цикл синхронизации выглядит следующим образом:

  1. пользователь вводит текст
  2. Awesomplete фильтрует список
  3. пользователь выбирает элемент
  4. вызывается replace
  5. обновляется input.value
  6. извлекается соответствующий value
  7. обновляется hidden input или state

Этот поток должен оставаться детерминированным независимо от способа интеграции и расширений интерфейса.