Перейти к содержимому

Клиентский код в шаблоне - инициализация, делегирование, дубли

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

Что нужно знать заранее

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

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

Данные с сервера отдают отдельным объектом данных, а не готовой разметкой. Разбор атрибутов и текста узлов превращает шаблон в хрупкую конструкцию, которая ломается от любой правки вёрстки.

Шаги

  1. Обернуть объявление класса проверкой: он мог быть уже объявлен предыдущим таким блоком.
  2. Привязывать обработчики к корневому узлу конкретного блока, а не ко всему документу.
  3. Передавать данные компонента одним объектом данных рядом с разметкой самого этого блока.
  4. Использовать делегирование событий для повторяющихся элементов внутри любого длинного списка.
  5. Проверить страницу с двумя блоками того же компонента и с подгрузкой списка.

Решение

Объявляем класс один раз:

if (typeof window.VendorGallery === 'undefined') {
window.VendorGallery = class { // повторный вывод шаблона не переобъявит
constructor(node, params) { this.node = node; this.params = params; }
};
}

Повторное объявление класса перетирает предыдущее и рвёт уже созданные объекты. Проверка перед объявлением стоит одну строку и снимает целый класс плавающих ошибок на страницах с двумя блоками.

Передаём данные компонента объектом:

<div class="vendor-gallery" data-params='<?= \Bitrix\Main\Web\Json::encode([
"iblockId" => $arParams["IBLOCK_ID"], "pageSize" => 12,
]) ?>'>

Создаём по объекту на каждый блок:

document.querySelectorAll('.vendor-gallery').forEach((node) => {
new window.VendorGallery(node, JSON.parse(node.dataset.params)); // свой объект на блок
});

Каждый блок получает свой объект и свои параметры. Так два одинаковых компонента на странице не мешают друг другу, а данные не собираются разбором вёрстки.

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

this.node.addEventListener('click', (event) => {
const button = event.target.closest('.js-add-to-basket');
if (!button) { return; } // клик мимо кнопки игнорируем
this.addToBasket(button.dataset.productId);
});
// один обработчик работает и для элементов, подгруженных позже

Делегирование избавляет от повторной привязки после подгрузки. Список дополнился новыми карточками - обработчик уже на месте, потому что он висит на общем контейнере блока.

Не теряем контекст в колбэках:

const self = this;
BX.ajax.runComponentAction('vendor:gallery', 'load', { mode: 'class' })
.then((response) => self.render(response.data)) // стрелочная функция тоже подходит
.catch((response) => self.showError(response.errors)); // ошибки приходят отдельным полем

Бережём браузер на длинных списках:

const fragment = document.createDocumentFragment();
items.forEach((item) => fragment.append(this.renderItem(item)));
this.node.querySelector('.list').append(fragment); // одна вставка вместо сотни
// перерисовка страницы происходит один раз, а не на каждый элемент списка

Одна вставка вместо цикла заметна на списках от сотни элементов. Там же полезно убрать лишние таймеры: страница с несколькими бесконечными интервалами греет процессор без всякой пользы.

Типичные проблемы

Обработчик срабатывает дважды на одно нажатие.

Шаблон выведен на одной странице дважды и привязал свои обработчики повторно. Обработчики вешают на корневой узел этого блока, а класс объявляют с проверкой.

После подгрузки списка кнопки перестают работать.

Обработчики привязаны к элементам, которых на момент их подгрузки ещё не было. Делегирование событий на общий контейнер решает это без всякой повторной привязки.

В обработчике теряется объект компонента.

Контекст вызова не сохранён при передаче метода объекта в чужой колбэк. Используют стрелочную функцию либо явную привязку контекста к нужному объекту.

Скрипт ломается после правки вёрстки.

Данные собираются разбором текста и классов уже готовой разметки самого блока. Параметры передают отдельным объектом данных рядом с самим блоком его разметки.

Страница с большим списком заметно тормозит.

Элементы вставляются в цикле по одному и каждый раз вызывают перерисовку. Готовят весь набор элементов целиком и вставляют его одной общей операцией вставки.

Частые вопросы

Где размещать скрипт шаблона компонента?

В отдельном файле шаблона, подключённом штатными средствами. Скрипт прямо в разметке дублируется при повторном выводе и не кэшируется браузером.

Как передать в скрипт большой массив данных?

Объектом в данных узла или отдельным запросом действия компонента. Огромный объект в разметке раздувает страницу и замедляет её разбор.

Нужно ли ждать готовности документа?

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

Можно ли обойтись без класса?

Для одного простого блока - да, хватит функции инициализации. Класс окупается там, где у блока есть состояние и несколько действий.

Что делать с двумя одинаковыми блоками на странице?

Создавать по объекту на каждый корневой узел и хранить состояние в нём. Общие переменные на весь скрипт при этом заводить нельзя.

Смежное

Первоисточник