Изменение функции item

Функция item в библиотеке Awesomplete отвечает за создание HTML-элемента пункта списка автодополнения. Именно она определяет, каким образом каждая подсказка будет отображаться внутри выпадающего списка.

По умолчанию библиотека генерирует элемент <li>, содержащий текст найденного совпадения с выделением совпадающих символов через тег <mark>. Однако стандартное поведение подходит не для всех сценариев. Во многих проектах требуется:

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

Для решения этих задач используется переопределение функции item.


Стандартное поведение item

Внутри библиотеки функция имеет примерно следующий вид:

Awesomplete.ITEM = function (text, input) {
    return Awesomplete.$.create("li", {
        innerHTML: text.replace(
            new RegExp(Awesomplete.REGEX_ESCAPE(input.trim()), "gi"),
            "<mark>$&</mark>"
        ),
        "aria-selected": "false"
    });
};

Функция:

  1. Создаёт элемент <li>;
  2. Выполняет подсветку совпадения;
  3. Возвращает DOM-элемент.

Переопределение функции

Собственная функция item передаётся через конфигурацию экземпляра.

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

        li.textContent = text;

        return li;
    }
});

Аргументы функции

text

Содержимое элемента списка.

item: function(text, input) {
    console.log(text);
}

При использовании простого массива строк:

[
    "JavaScript",
    "TypeScript",
    "Python"
]

значением text будет строка.


input

Текущее значение поля ввода.

item: function(text, input) {
    console.log(input);
}

Используется для:

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

Создание собственного HTML

Наиболее распространённый сценарий — изменение структуры элемента.

Простой пример

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

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

        return li;
    }
});

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

Часто требуется показывать дополнительную информацию.

const list = [
    {
        label: "JavaScript",
        description: "Язык программирования"
    },
    {
        label: "HTML",
        description: "Язык разметки"
    }
];

new Awesomplete(input, {
    list,

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

        li.innerHTML = `
            <div class="title">${item.label}</div>
            <div class="description">
                ${item.description}
            </div>
        `;

        return li;
    }
});

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

Если список содержит объекты, функция item получает полный объект.

[
    {
        label: "VS Code",
        type: "Редактор"
    }
]

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

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

    li.innerHTML = `
        <span>${item.label}</span>
        <small>${item.type}</small>
    `;

    return li;
}

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

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

    li.innerHTML = `
        <div class="autocomplete-item">
            <span class="icon">?</span>
            <span>${item.label}</span>
        </div>
    `;

    return li;
}

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

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

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

            <span>${item.label}</span>
        </div>
    `;

    return li;
}

Создание карточек

Функция item может возвращать полноценные карточки.

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

    li.classList.add("card-item");

    li.innerHTML = `
        <div class="card-title">
            ${item.name}
        </div>

        <div class="card-meta">
            ${item.category}
        </div>

        <div class="card-price">
            ${item.price}
        </div>
    `;

    return li;
}

Подсветка совпадений вручную

При переопределении item стандартная подсветка исчезает. Её приходится реализовывать самостоятельно.

Пример через replace

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

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

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

    return li;
}

Экранирование спецсимволов

Без экранирования регулярное выражение может работать некорректно.

Проблемный пример:

input = "c++"

Правильный подход:

function escapeRegex(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

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

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

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

    li.innerHTML = text.replace(regex, "<mark>$&</mark>");

    return li;
}

Работа с изображениями

item: function(user) {
    const li = document.createElement("li");

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

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

    return li;
}

Добавление data-атрибутов

Иногда необходимо сохранять служебные данные.

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

    li.dataset.id = item.id;
    li.dataset.type = item.type;

    li.textContent = item.label;

    return li;
}

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

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

    li.classList.add("autocomplete-item");

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

    li.textContent = item.label;

    return li;
}

Динамические стили

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

    li.style.color = item.color;

    li.textContent = item.label;

    return li;
}

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

Категории

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

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

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

    return li;
}

Разделители

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

    if (item.separator) {
        li.classList.add("separator");
        li.textContent = item.title;

        return li;
    }

    li.textContent = item.label;

    return li;
}

Использование шаблонных функций

Для крупных шаблонов код удобно выносить отдельно.

function renderItem(item) {
    return `
        <div class="title">
            ${item.label}
        </div>

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

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

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

    li.innerHTML = renderItem(item);

    return li;
}

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

При сложной структуре можно избегать большого количества innerHTML.

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

    const title = document.createElement("div");
    title.textContent = item.label;

    const meta = document.createElement("small");
    meta.textContent = item.type;

    li.append(title);
    li.append(meta);

    return li;
}

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

Сложная функция item может замедлять отображение списка.

На производительность влияют:

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

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

Кэширование регулярного выражения

Плохо:

item: function(text, input) {
    const regex = new RegExp(input, "gi");
}

Лучше:

const cache = {};

item: function(text, input) {
    if (!cache[input]) {
        cache[input] = new RegExp(input, "gi");
    }

    const regex = cache[input];
}

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

Большие HTML-строки увеличивают нагрузку.

Более эффективный подход:

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

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

    span.textContent = item.label;

    li.append(span);

    return li;
}

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

Использование innerHTML может привести к XSS-уязвимостям.

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

li.innerHTML = item.label;

Если данные приходят с сервера:

<script>alert(1)</script>

код выполнится.


Безопасный вариант

li.textContent = item.label;

Санитизация HTML

Если HTML необходим:

function sanitize(value) {
    return value
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;");
}

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

li.innerHTML = sanitize(item.label);

Работа с ARIA

Для доступности желательно сохранять необходимые атрибуты.

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

    li.setAttribute(
        "aria-selected",
        "false"
    );

    li.textContent = item.label;

    return li;
}

Добавление состояния загрузки

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

    if (item.loading) {
        li.textContent = "Загрузка...";
        li.classList.add("loading");

        return li;
    }

    li.textContent = item.label;

    return li;
}

Использование кастомных элементов

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

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

    badge.className = "badge";
    badge.textContent = item.type;

    li.append(item.label);
    li.append(badge);

    return li;
}

Интеграция с CSS-анимациями

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

    li.classList.add("fade-item");

    li.textContent = item.label;

    return li;
}
.fade-item {
    animation: fade .2s ease;
}

@keyframes fade {
    from {
        opacity: 0;
        transform: translateY(4px);
    }

    to {
        opacity: 1;
        transform: translateY(0);
    }
}

Полное переопределение отображения

new Awesomplete(input, {
    list: users,

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

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

        const highlighted = user.name.replace(
            regex,
            "<mark>$&</mark>"
        );

        li.className = "user-row";

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

            <div class="content">
                <div class="name">
                    ${highlighted}
                </div>

                <div class="email">
                    ${user.email}
                </div>
            </div>
        `;

        li.setAttribute(
            "aria-selected",
            "false"
        );

        return li;
    }
});

Типичные ошибки

Отсутствие возврата

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

Без return элемент не появится.


Возврат строки вместо DOM-элемента

Неправильно:

item: function(text) {
    return `<li>${text}</li>`;
}

Правильно:

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

    li.textContent = text;

    return li;
}

Потеря подсветки

После переопределения item встроенная подсветка больше не используется автоматически.


Отсутствие aria-selected

Без атрибута могут возникать проблемы доступности.


Использование тяжёлых компонентов

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