Удаление выбранных элементов

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

Основой является массив, в котором фиксируется текущее состояние выбранных элементов:

let selectedItems = [];

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

Ключевой принцип: DOM не является источником истины, он лишь отражает состояние массива selectedItems.


Связка Awesomplete с пользовательским состоянием

Awesomplete предоставляет событие выбора элемента awesomplete-selectcomplete, которое используется как точка добавления значения в список:

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

    if (!selectedItems.includes(value)) {
        selectedItems.push(value);
        renderTags();
    }

    input.value = "";
});

Здесь важно, что Awesomplete завершает ввод, но не управляет коллекцией — это полностью внешняя ответственность.


Визуальное представление выбранных элементов

Для отображения используется контейнер, в который добавляются «теги»:

<div id="tags"></div>
<input id="input" />

Функция отрисовки полностью пересобирает DOM на основе массива:

function renderTags() {
    const container = document.getElementById("tags");
    container.innerHTML = "";

    selectedItems.forEach(item => {
        const tag = document.createElement("span");
        tag.className = "tag";

        const text = document.createElement("span");
        text.className = "tag-text";
        text.textContent = item;

        const removeBtn = document.createElement("button");
        removeBtn.className = "tag-remove";
        removeBtn.textContent = "×";

        removeBtn.addEventListener("click", () => removeItem(item));

        tag.appendChild(text);
        tag.appendChild(removeBtn);
        container.appendChild(tag);
    });
}

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


Удаление элемента по клику

Удаление из списка реализуется через фильтрацию массива:

function removeItem(value) {
    selectedItems = selectedItems.filter(item => item !== value);
    renderTags();
}

Фильтрация обеспечивает иммутабельное обновление состояния, что упрощает контроль изменений.

При необходимости допускается более строгая модель с индексами, особенно если значения могут повторяться:

function removeItemByIndex(index) {
    selectedItems.splice(index, 1);
    renderTags();
}

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

selectedItems.forEach((item, index) => {
    removeBtn.addEventListener("click", () => removeItemByIndex(index));
});

Удаление через клавиатуру

Типичный сценарий — удаление последнего элемента при нажатии Backspace, когда поле ввода пустое.

input.addEventListener("keydown", function (e) {
    if (e.key === "Backspace" && input.value === "") {
        selectedItems.pop();
        renderTags();
    }
});

Поведение повторяет логику многих tag-input компонентов: пользователь интуитивно ожидает удаление последнего добавленного элемента.


Синхронизация с Awesomplete

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

  1. удалённое значение должно снова становиться доступным в списке;
  2. дубликаты должны контролироваться на уровне источника данных.

Если список источника статический:

const list = ["Apple", "Banana", "Orange", "Mango"];

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

function getFilteredList() {
    return list.filter(item => !selectedItems.includes(item));
}

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

const awesomplete = new Awesomplete(input, {
    list: getFilteredList()
});

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

function refreshAwesomplete() {
    awesomplete.list = getFilteredList();
}

И вызов вместе с рендером:

function removeItem(value) {
    selectedItems = selectedItems.filter(item => item !== value);
    renderTags();
    refreshAwesomplete();
}

Предотвращение дублирования значений

Удаление тесно связано с защитой от повторного добавления. Проверка выполняется до вставки:

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

    if (selectedItems.indexOf(value) === -1) {
        selectedItems.push(value);
        renderTags();
        refreshAwesomplete();
    }

    input.value = "";
});

Это гарантирует консистентность списка.


Удаление при взаимодействии с фокусом

Расширенный сценарий предполагает удаление последнего элемента при потере фокуса или очистке поля:

input.addEventListener("blur", function () {
    if (input.value === "" && selectedItems.length === 0) return;
});

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


Оптимизация рендеринга

При большом количестве выбранных элементов пересоздание DOM может становиться затратным. Альтернативный подход — точечное удаление узлов:

function removeItem(value) {
    const index = selectedItems.indexOf(value);
    if (index === -1) return;

    selectedItems.splice(index, 1);

    const container = document.getElementById("tags");
    const tagElements = container.querySelectorAll(".tag");

    tagElements[index].remove();
}

Однако такой подход требует строгого соответствия порядка массива и DOM, что усложняет поддержку.


Поддержка сложных объектов вместо строк

Awesomplete допускает использование объектов:

const list = [
    { label: "Apple", value: "apple" },
    { label: "Banana", value: "banana" }
];

В этом случае удаление должно учитывать поле value:

function removeItem(value) {
    selectedItems = selectedItems.filter(item => item.value !== value);
    renderTags();
}

И добавление:

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

    if (!selectedItems.find(i => i.value === value)) {
        selectedItems.push({ value, label });
        renderTags();
    }
});

Обработка edge cases

Удаление элементов требует учёта нескольких ситуаций:

Пустой список

Операции удаления должны быть безопасными:

if (selectedItems.length === 0) return;

Несуществующий элемент

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

Повторяющиеся значения

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


Интеграционная модель состояния

Наиболее устойчивый подход включает три слоя:

  1. Awesomplete — отвечает за подсказки
  2. Массив состояния — хранит выбранные значения
  3. DOM слой тегов — визуализирует состояние

Удаление всегда инициирует цепочку:

  • изменение массива
  • перерисовка UI
  • обновление списка подсказок
function removeItem(value) {
    selectedItems = selectedItems.filter(item => item !== value);

    renderTags();
    refreshAwesomplete();
    syncHiddenInput();
}

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

function syncHiddenInput() {
    document.getElementById("hiddenInput").value =
        JSON.stringify(selectedItems);
}

Стабильность поведения при быстром вводе

При интенсивном вводе и удалении важно избегать гонок состояния. Решение заключается в том, чтобы не смешивать прямые DOM-операции и изменение массива без единого источника обновления.

Рекомендуемая модель:

function updateState(mutator) {
    mutator();
    renderTags();
    refreshAwesomplete();
    syncHiddenInput();
}

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

function removeItem(value) {
    updateState(() => {
        selectedItems = selectedItems.filter(item => item !== value);
    });
}

Поведенческая консистентность интерфейса

Удаление выбранных элементов в связке с Awesomplete требует строгого соответствия между:

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

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