Делегування подій JavaScript для динамічних кнопок

Практичні поради команди AriZone

У навчальному каталозі кнопка «Позначити» працює після відкриття сторінки, але нові картки, додані пізніше, ігнорують натискання. Повторне завантаження ненадовго допомагає. Причина може бути не в мережі: обробники приєднали тільки до кнопок, які існували під час запуску скрипту. Делегування подій JavaScript дозволяє перенести обробку на стабільний контейнер і працювати з елементами, що з’являються згодом.

Цей приклад не відправляє запити, не змінює замовлення та не зберігає персональні дані. Він показує механіку на локальній HTML-сторінці. Для справжнього магазину додатково потрібні серверна перевірка прав, обробка помилок і захист від повторних операцій. Спершу відтворіть поведінку окремо від продуктивного сайту, щоб не змішувати проблему подій із роботою кошика чи стороннього модуля.

Де зникає зв’язок між кнопкою та обробником

Поширений початковий підхід — знайти кнопки через querySelectorAll і додати кожній слухач click. Такий список не поповнюється автоматично, коли після запиту або фільтрації з’являються нові вузли. Новий елемент може виглядати ідентично старому, але його зовнішній вигляд нічого не говорить про наявність обробника. Перевіряйте саме момент створення вузла та момент реєстрації слухача.

Подія click зазвичай спливає від натиснутого елемента через його предків. Тому один слухач на списку може приймати натискання від вкладених кнопок. event.target означає початкову ціль, а event.currentTarget — елемент, на якому зареєстрований поточний слухач. Ця різниця важлива: натискання на текст або значок усередині кнопки не обов’язково має саму кнопку за ціль.

Не починайте ремонт із глобального перехоплення всіх натискань на document. Виберіть найближчий стабільний контейнер потрібного компонента. Так простіше визначити межі відповідальності й уникнути випадкового реагування на чужу форму. Якщо фільтр замінює весь контейнер, делегування на старому вузлі не допоможе: або залишайте контейнер, або керуйте повторним підключенням у життєвому циклі компонента.

Мінімальна сторінка без додаткових бібліотек

Створіть новий файл delegation-demo.html у тестовому каталозі та відкрийте його в сучасному браузері. Адміністративні права, пакети й сервер не потрібні. Приклади ідентифікаторів catalog, add-demo та item-1 умовні: у проєкті замініть їх узгоджено в розмітці й скрипті. Не вставляйте цей код одразу в редактор продуктивної теми; окремий файл легко прибрати після перевірки.

Почніть із такої розмітки. Використано справжні кнопки, а не клікабельні div: стандартна кнопка підтримує клавіатурну активацію. type="button" запобігає випадковому відправленню форми, якщо компонент пізніше опиниться всередині неї. Атрибут aria-pressed передає стан перемикача допоміжним технологіям. Текст усередині span навмисно створює вкладену ціль для перевірки.

<!doctype html>
<html lang="uk">
<meta charset="utf-8">
<title>Delegation demo</title>
<body>
  <button id="add-demo" type="button">Add card</button>
  <div id="catalog">
    <button type="button" data-action="toggle"
            data-item-id="item-1" aria-pressed="false">
      <span>Mark</span>
    </button>
  </div>
</body>
</html>

Додайте наведений нижче script після розмітки, перед закривальним body. Так каталог уже існуватиме під час пошуку. Обробник знаходить найближчу відповідну кнопку методом closest, перевіряє належність контейнеру і змінює тільки її стан. Новий текст встановлюється через textContent: цей навчальний сценарій не потребує інтерпретації довільного HTML. Перевірка instanceof Element захищає виклик closest від невідповідного типу цілі.

const catalog = document.querySelector('#catalog');

catalog.addEventListener('click', (event) => {
  if (!(event.target instanceof Element)) return;
  const button = event.target.closest(
    'button[data-action="toggle"]'
  );
  if (!button || !catalog.contains(button)) return;
  if (button.disabled) return;

  const pressed = button.getAttribute('aria-pressed') === 'true';
  button.setAttribute('aria-pressed', String(!pressed));
  button.textContent = pressed ? 'Mark' : 'Marked';
});

Після натискання має змінитися aria-pressed і підпис кнопки; жодна інша картка не повинна переключитися. Це очікувана поведінка прикладу, а не результат вимірювання на вашому сайті. Метод closest перевіряє сам елемент і рухається вгору до предків, тому натискання на вкладений span теж знаходить кнопку. Документація MDN пояснює спливання подій та поведінку closest.

Додайте новий елемент після запуску

Наступний фрагмент поставте в тому самому script після реєстрації слухача. Кнопка «Додати картку» створить ще одну кнопку без окремого слухача click. Тут немає зовнішніх даних: ідентифікатор та текст задаються самим прикладом. Для даних API не використовуйте отриманий рядок як готову HTML-розмітку; створюйте вузли й заповнюйте текстові властивості або застосовуйте належну перевірену обробку.

let nextItem = 2;
document.querySelector('#add-demo').addEventListener('click', () => {
  const button = document.createElement('button');
  button.type = 'button';
  button.dataset.action = 'toggle';
  button.dataset.itemId = `item-${nextItem++}`;
  button.setAttribute('aria-pressed', 'false');
  button.textContent = 'Mark';
  catalog.append(button);
});

Порядок тесту має значення. Спочатку активуйте початкову кнопку, потім додайте нову картку й натисніть її. Далі додайте ще кілька, перемикаючи лише одну з них. Нарешті клацніть порожнє місце каталогу: нічого змінюватися не повинно. Повторіть перевірку клавішами Tab і Space або Enter. Якщо мишею все працює, а клавіатурою ні, перевірте, чи не замінили стандартну кнопку іншою розміткою.

Обмеження, які варто перевірити до релізу

Делегування не виправляє помилки бізнес-логіки. Якщо натискання запускає оплату або видалення, потрібні окремі серверні гарантії та підтвердження там, де вони доречні. Не довіряйте data-item-id як доказу прав користувача: атрибут можна змінити в браузері. Наш приклад лише переключає локальний стан і не є реалізацією збереження товару в особистому кабінеті.

Не всі події спливають однаково. Для іншого типу події спершу перевірте документацію; механічна заміна click на focus може дати іншу поведінку. Також з’ясуйте, чи сторонній код не викликає stopPropagation до досягнення контейнера. Не прибирайте такий виклик навмання: він може захищати інший компонент від небажаної реакції. Shadow DOM потребує окремого розгляду шляху та перенаправлення подій.

Перед публікацією перевірте повторну ініціалізацію: один клік не повинен запускати два однакові обробники. Збережіть попередній варіант скрипту в Git і перевірте зміни перед відправленням. Для відкату поверніть попередню реалізацію, а не вимикайте весь JavaScript сайту. Корисно зафіксувати тест додавання картки як регресійний сценарій для наступного оновлення каталогу.

Висновок: стабільний контейнер, перевірена ціль і невеликий набір тестів роблять поведінку динамічних кнопок зрозумілою. Якщо взаємодія все одно ламається після оновлення теми або модуля, AriZone може допомогти відокремити проблему подій від запитів і перевірити виправлення на тестовій версії сайту.

Обговорення

Залиште коментар

Будьте першими, хто прокоментує

Ваш email не буде опубліковано. Обов’язкові поля позначені *.

Захист від спаму Cloudflare Turnstile