В типичном сценарии автодополнения визуальный текст, который виден в выпадающем списке, не совпадает с тем значением, которое должно быть отправлено на сервер. Это ключевая особенность большинства интерфейсов autocomplete: пользователь взаимодействует с человекочитаемыми метками, тогда как форма оперирует структурированными идентификаторами.
В Awesomplete список может содержать строки или объекты, но базовая реализация ориентирована на строковые значения. При необходимости разделения отображаемого и фактического значения вводится модель «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 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;
}
});
Здесь синхронизация разделена на два уровня:
Ключевая сложность возникает, когда пользователь вводит значение вручную, не выбирая элемент из списка. В этом случае 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, концентрируя
всю логику синхронизации в одном месте.
Если список формируется динамически, например через 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.
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 изменяет input.valueРезультатом становится перезапись данных в неправильном порядке.
Структурное решение — централизовать логику:
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();
});
Основная задача синхронизации — поддержание согласованного состояния между:
Нарушение этого баланса проявляется в:
Для стабилизации состояния используется принцип единого источника данных (single source of truth), где либо:
Типовой жизненный цикл синхронизации выглядит следующим образом:
Этот поток должен оставаться детерминированным независимо от способа интеграции и расширений интерфейса.