Делегирование и композиция обработчиков

Основы делегирования событий

В ally.js делегирование событий используется для управления поведением интерактивных элементов без необходимости навешивания отдельных слушателей на каждый элемент. Это особенно важно при работе с динамическим DOM, где элементы могут появляться и исчезать после первоначальной загрузки страницы.

Основная идея делегирования заключается в том, что слушатель события привязывается к родительскому элементу, а обработка конкретного события осуществляется на основе целевого элемента (target). Это позволяет минимизировать количество слушателей и повысить производительность.

Пример базового делегирования в ally.js:

import { tabbable } from 'ally.js';

const container = document.querySelector('#menu');

container.addEventListener('keydown', event => {
  const target = event.target;
  if (!container.contains(target)) return;

  // Обработка только для tabbable элементов
  if (tabbable(target).length) {
    if (event.key === 'ArrowDown') {
      focusNext(target);
    } else if (event.key === 'ArrowUp') {
      focusPrevious(target);
    }
  }
});

function focusNext(element) {
  const elements = tabbable(container);
  const index = elements.indexOf(element);
  if (index !== -1 && index < elements.length - 1) {
    elements[index + 1].focus();
  }
}

function focusPrevious(element) {
  const elements = tabbable(container);
  const index = elements.indexOf(element);
  if (index > 0) {
    elements[index - 1].focus();
  }
}

Ключевой момент: использование функции tabbable позволяет точно определить элементы, доступные для навигации с клавиатуры, что повышает доступность интерфейса.


Композиция обработчиков

Композиция обработчиков в ally.js позволяет объединять несколько функций обработки событий, обеспечивая модульность и переиспользуемость кода. Вместо создания одного «гигантского» обработчика события, код делится на мелкие, независимые функции, которые можно комбинировать.

Пример композиции:

function stopPropagation(event) {
  event.stopPropagation();
}

function preventDefault(event) {
  event.preventDefault();
}

function handleArrowNavigation(container) {
  return function(event) {
    const target = event.target;
    if (!container.contains(target)) return;

    const elements = tabbable(container);
    const index = elements.indexOf(target);

    if (event.key === 'ArrowDown' && index < elements.length - 1) {
      elements[index + 1].focus();
    } else if (event.key === 'ArrowUp' && index > 0) {
      elements[index - 1].focus();
    }
  };
}

const container = document.querySelector('#menu');

container.addEventListener('keydown', event => {
  [stopPropagation, preventDefault, handleArrowNavigation(container)].forEach(fn => fn(event));
});

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


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

Ally.js предоставляет ряд функций, которые упрощают работу с делегированием:

  • tabbable(container) — возвращает список элементов, доступных для фокусировки;
  • focusable(container) — возвращает все элементы, которые можно сделать активными;
  • nextFocus(element) / previousFocus(element) — перемещение фокуса в пределах контейнера;
  • observe.focus(container, callback) — подписка на события фокуса с автоматическим управлением делегированием.

Пример с наблюдением за фокусом:

import { observe } from 'ally.js';

const container = document.querySelector('#menu');

const observer = observe.focus(container, ({ target, type }) => {
  if (type === 'focus') {
    target.classList.add('focused');
  } else if (type === 'blur') {
    target.classList.remove('focused');
  }
});

// Позднее можно отключить наблюдение
// observer.disengage();

Преимущество использования observe.focus в том, что оно автоматически учитывает динамически добавленные элементы, что делает делегирование максимально прозрачным.


Комбинирование делегирования и композиции

Эффективное использование ally.js достигается при совместном применении делегирования и композиции обработчиков:

  1. Создание небольших обработчиков для отдельных типов событий (клавиатура, мышь, фокус).
  2. Композиция обработчиков в массив или цепочку функций.
  3. Привязка к родительскому контейнеру, минимизируя количество слушателей.
  4. Использование утилит ally.js для работы с tabbable/focusable элементами и наблюдением за фокусом.

Пример объединенного подхода:

const container = document.querySelector('#menu');

function handleClick(event) {
  if (!container.contains(event.target)) return;
  console.log('Clicked:', event.target);
}

function handleKeyboard(event) {
  const target = event.target;
  if (!container.contains(target)) return;

  const elements = tabbable(container);
  const index = elements.indexOf(target);

  if (event.key === 'ArrowDown') elements[index + 1]?.focus();
  if (event.key === 'ArrowUp') elements[index - 1]?.focus();
}

container.addEventListener('click', handleClick);
container.addEventListener('keydown', event => [handleKeyboard].forEach(fn => fn(event)));

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


Практические рекомендации

  • Делегирование лучше всего использовать для списков, меню и форм с динамическими элементами.
  • Композиция обработчиков снижает вероятность дублирования кода и облегчает тестирование.
  • Всегда проверять, что элемент действительно tabbable или focusable перед обработкой событий.
  • При необходимости отключения обработчиков или наблюдателей использовать методы disengage или removeEventListener для предотвращения утечек памяти.

Эти принципы позволяют строить гибкие, масштабируемые и доступные интерфейсы, полностью используя возможности ally.js в контексте современного веб-разработки.