Добавление HTML в элементы

Библиотека Awesomplete по умолчанию формирует элементы выпадающего списка как обычный текст. Каждый пункт представляет собой элемент <li>, содержимое которого вставляется в DOM без дополнительной HTML-разметки. Такое поведение безопасно и подходит для простых сценариев автодополнения, однако в реальных интерфейсах часто требуется:

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

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

  • item
  • replace
  • filter
  • sort

Ключевую роль при вставке HTML играет именно метод item, отвечающий за создание DOM-элемента списка.


Базовое переопределение item

Стандартный item принимает строку и возвращает элемент <li>.

Простейший вариант кастомизации выглядит так:

new Awesomplete(input, {
    list: ["JavaScript", "TypeScript", "Python"],

    item: function(text, input) {
        const li = document.createElement("li");

        li.innerHTML = `
            <strong>${text}</strong>
        `;

        return li;
    }
});

В результате каждый элемент списка будет содержать HTML-тег <strong>.


Разница между textContent и innerHTML

При генерации элементов можно использовать два подхода:

Без HTML

li.textContent = text;

Текст вставляется безопасно. HTML-теги экранируются.

С HTML

li.innerHTML = `<strong>${text}</strong>`;

Строка интерпретируется как HTML.

Именно innerHTML позволяет внедрять полноценную разметку.


Добавление выделения совпадений

Одно из наиболее популярных применений HTML в Awesomplete — подсветка совпавшей части строки.

Пример

new Awesomplete(input, {
    list: [
        "JavaScript",
        "Java",
        "TypeScript"
    ],

    item: function(text, input) {

        const li = document.createElement("li");

        const regex = new RegExp(input, "gi");

        li.innerHTML = text.replace(regex, function(match) {
            return `<mark>${match}</mark>`;
        });

        return li;
    }
});

Использование mark для подсветки

Тег <mark> особенно удобен для визуального выделения совпадений.

mark {
    background: gold;
    color: black;
}

Такой подход позволяет реализовать поведение, аналогичное поисковым системам.


Структурированные элементы списка

Awesomplete способен отображать не только строки, но и сложные объекты.

Исходные данные

const languages = [
    {
        name: "JavaScript",
        type: "Frontend"
    },
    {
        name: "Node.js",
        type: "Backend"
    }
];

Отображение нескольких полей

new Awesomplete(input, {

    list: languages,

    item: function(item, input) {

        const li = document.createElement("li");

        li.innerHTML = `
            <div class="title">${item.name}</div>
            <div class="subtitle">${item.type}</div>
        `;

        return li;
    },

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

Стилизация составных элементов

.title {
    font-weight: bold;
}

.subtitle {
    font-size: 12px;
    color: gray;
}

Теперь элементы выпадающего списка становятся похожими на карточки.


Добавление иконок

HTML внутри элементов позволяет внедрять иконки несколькими способами.

Unicode-иконки

li.innerHTML = `
    <span>?</span>
    <span>${text}</span>
`;

SVG-иконки

li.innerHTML = `
    <svg width="16" height="16">
        <circle cx="8" cy="8" r="6" fill="green"></circle>
    </svg>

    <span>${text}</span>
`;

Иконки через CSS-классы

li.innerHTML = `
    <i class="icon-search"></i>
    <span>${text}</span>
`;

Такой подход особенно удобен при использовании:

  • Font Awesome;
  • Material Icons;
  • Bootstrap Icons.

Добавление изображений

Awesomplete можно использовать как компонент поиска пользователей, товаров или медиафайлов.

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

const users = [
    {
        name: "Alex",
        avatar: "avatar1.jpg"
    },

    {
        name: "Maria",
        avatar: "avatar2.jpg"
    }
];

Генерация HTML с изображениями

new Awesomplete(input, {

    list: users,

    item: function(user) {

        const li = document.createElement("li");

        li.innerHTML = `
            <img src="${user.avatar}" class="avatar">

            <span>${user.name}</span>
        `;

        return li;
    },

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

Стилизация изображений

.avatar {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    margin-right: 10px;
}

li {
    display: flex;
    align-items: center;
}

Использование HTML-шаблонов

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

Пример

function renderItem(product) {

    return `
        <div class="product">
            <div class="name">${product.name}</div>

            <div class="price">
                $${product.price}
            </div>
        </div>
    `;
}

Интеграция шаблона в item

item: function(product) {

    const li = document.createElement("li");

    li.innerHTML = renderItem(product);

    return li;
}

Такой подход делает код более читаемым.


Добавление HTML через createElement

Использование innerHTML удобно, но иногда предпочтительнее создавать элементы вручную.

Пример

item: function(text) {

    const li = document.createElement("li");

    const strong = document.createElement("strong");

    strong.textContent = text;

    li.appendChild(strong);

    return li;
}

Преимущества createElement

Безопасность

Снижается вероятность XSS-уязвимостей.

Контроль DOM

Можно точечно управлять:

  • атрибутами;
  • классами;
  • обработчиками событий;
  • вложенностью элементов.

Производительность

При сложных интерфейсах иногда уменьшается количество перерасчётов DOM.


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

HTML-разметка может содержать дополнительные данные.

Пример

item: function(product) {

    const li = document.createElement("li");

    li.dataset.id = product.id;

    li.innerHTML = `
        <span>${product.name}</span>
    `;

    return li;
}

Получение dataset после выбора

input.addEventListener("awesomplete-selectcomplete", function(event) {

    console.log(event.text.dataset.id);
});

HTML и replace

Метод replace определяет, что именно попадёт в поле ввода после выбора.

Даже если элемент содержит сложную HTML-разметку, в input обычно вставляется только текст.

Пример

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

Разделение отображения и значения

Это важная архитектурная особенность.

Отображение

Используется для визуального интерфейса.

Значение

Используется для логики приложения.

Например:

Отображение Значение
Карточка товара ID товара
Пользователь с аватаром username
Элемент меню slug

HTML в списках категорий

Awesomplete можно превратить в визуальный каталог.

Пример

li.innerHTML = `
    <div class="category">

        <span class="icon">
            ${item.icon}
        </span>

        <span class="title">
            ${item.title}
        </span>

    </div>
`;

Использование ARIA-атрибутов

При вставке HTML важно не ухудшить доступность интерфейса.

Пример

li.setAttribute("role", "option");

Добавление скрытого текста

li.innerHTML = `
    <span class="visually-hidden">
        Результат поиска:
    </span>

    <span>${text}</span>
`;

HTML и безопасность

Использование innerHTML требует осторожности.

Опасный пример

li.innerHTML = userInput;

Если строка содержит:

<script>alert(1)</script>

код будет выполнен.


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

Никогда не вставлять пользовательский ввод напрямую

Плохо:

innerHTML = userData;

Использовать экранирование

function escapeHTML(str) {

    return str
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;");
}

Использовать textContent

element.textContent = userData;

Комбинированный безопасный вариант

item: function(text) {

    const li = document.createElement("li");

    const span = document.createElement("span");

    span.textContent = text;

    li.appendChild(span);

    return li;
}

HTML и производительность

Большое количество сложной HTML-разметки может замедлять:

  • рендеринг;
  • открытие списка;
  • фильтрацию;
  • навигацию клавиатурой.

Потенциально тяжёлые элементы

Особенно затратны:

  • изображения;
  • SVG;
  • тени;
  • вложенные контейнеры;
  • анимации;
  • сложные flex/grid-структуры.

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

Минимизация DOM

Плохо:

<div>
    <div>
        <div>
            <span>Item</span>
        </div>
    </div>
</div>

Лучше:

<span>Item</span>

Ограничение количества результатов

maxItems: 5

Lazy loading изображений

<img loading="lazy">

Кастомные HTML-компоненты

Awesomplete совместим с современными подходами компонентной архитектуры.

Можно интегрировать:

  • Web Components;
  • Shadow DOM;
  • шаблоны <template>;
  • JSX;
  • React/Vue-генерацию HTML.

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

<template id="item-template">

    <div class="user">
        <img class="avatar">
        <span class="name"></span>
    </div>

</template>

Клонирование шаблона

item: function(user) {

    const template =
        document.querySelector("#item-template");

    const clone =
        template.content.cloneNode(true);

    clone.querySelector(".avatar").src =
        user.avatar;

    clone.querySelector(".name").textContent =
        user.name;

    const li = document.createElement("li");

    li.appendChild(clone);

    return li;
}

HTML и события

Элементы списка могут содержать интерактивные элементы:

  • кнопки;
  • ссылки;
  • чекбоксы;
  • переключатели.

Пример кнопки внутри элемента

li.innerHTML = `
    <span>${item.name}</span>

    <button class="remove">
        Удалить
    </button>
`;

Конфликт событий

Следует учитывать, что Awesomplete управляет:

  • выбором;
  • hover;
  • клавиатурной навигацией;
  • focus-состояниями.

Из-за этого вложенные элементы могут конфликтовать с основной логикой списка.


Предотвращение всплытия

button.addEventListener("click", function(event) {

    event.stopPropagation();
});

Добавление ссылок

li.innerHTML = `
    <a href="/profile/${user.id}">
        ${user.name}
    </a>
`;

Проблема навигации

Клик по ссылке может:

  • закрывать список;
  • вызывать выбор элемента;
  • менять значение input.

Подобные сценарии требуют дополнительной настройки событий.


Использование flex-layout

HTML-разметка внутри Awesomplete отлично сочетается с Flexbox.

Пример

li {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

Добавление вторичного контента

li.innerHTML = `
    <span>${item.title}</span>

    <small>${item.description}</small>
`;

Группировка элементов

HTML позволяет визуально разделять результаты.

Пример

li.innerHTML = `
    <div class="group">
        ${item.group}
    </div>

    <div class="value">
        ${item.value}
    </div>
`;

Визуальные разделители

.group {
    font-size: 11px;
    text-transform: uppercase;
    opacity: 0.6;
}

Динамическое изменение классов

if (item.featured) {
    li.classList.add("featured");
}

HTML и кастомные состояния

.featured {
    background: #fff8d9;
}

Использование badge-элементов

li.innerHTML = `
    <span>${item.name}</span>

    <span class="badge">
        NEW
    </span>
`;

Итоговая архитектура сложного элемента

Типичная структура кастомного HTML-элемента в Awesomplete включает:

  • контейнер;
  • иконку;
  • основной текст;
  • вторичный текст;
  • служебные данные;
  • состояния;
  • кастомные атрибуты;
  • обработчики событий;
  • стилизацию.

Именно переопределение item превращает стандартный autocomplete в полноценный UI-компонент с гибкой HTML-разметкой и сложным визуальным поведением.