События и изменение состояния каталога
Программа уже умеет показывать любой переданный набор курсов. Теперь предоставим управление читателю: выбор темы в поле должен изменить карточки. Браузер сообщает о таком действии событием, а наш обработчик обновляет состояние и вызывает существующие функции.
Продолжим index.html из урока о DOM. В app.js остаются четыре курса, найденные обязательные узлы и renderCourses предыдущего урока. Уберите временный вызов, показывающий только Markdown: начальный интерфейс должен отображать все темы, а последующие изменения должны происходить из поля выбора.
Обработчик как функция
Добавьте этот полный блок управления после определения renderCourses:
const topicSelect = form.querySelector('[name="topic"]');
if (!topicSelect) throw new Error('Нет поля выбора темы');
const selection = { topic: 'all' };
function showSelection() {
const visible = courses.filter(course =>
selection.topic === 'all' || course.topic === selection.topic);
renderCourses(list, visible);
status.textContent = visible.length === 0
? 'По выбранной теме курсов нет.'
: `Курсов: ${visible.length}.`;
}
topicSelect.addEventListener('change', event => {
selection.topic = event.currentTarget.value;
showSelection();
});
showSelection();
Ожидаемое начальное количество равно четырём. После выбора фронтенда появляются три карточки, после выбора публикации — одна. Возвращение к варианту всех тем снова показывает четыре, потому что отбор не уничтожает исходный массив.
addEventListener регистрирует функцию, но не вызывает её немедленно. Браузер вызовет обработчик при соответствующем событии. Для начального состояния нужен отдельный последний вызов showSelection. Без него карточки появились бы только после первого изменения поля, и страница выглядела бы неполной.
Функция обработки получает объект события event. Свойство currentTarget обозначает объект, на котором выполняется зарегистрированный обработчик, поэтому здесь это наше поле. Его value содержит машинный код выбранного варианта. Мы не читаем видимую подпись «Фронтенд», потому что логика использует стабильное frontend.
Состояние между событиями
Объект selection существует вне обработчика и сохраняется между вызовами. Каждое новое действие меняет поле topic, затем создаётся новый видимый массив. Объявление const selection не мешает изменению свойства, потому что сама связь с объектом не переназначается.
Программа не выполняет бесконечный цикл ожидания выбора. Браузер организует обработку событий и вызывает зарегистрированную функцию в нужный момент. После завершения обработчика управление возвращается среде. Это отличается от синхронного цикла, который сам последовательно перебирает данные внутри одного действия.
Не выполняйте тяжёлые вычисления без необходимости в обработчике. Пока четыре записи обрабатываются простой функцией, и мы не приводим выдуманные измерения скорости. Для большого массива потребовалось бы отдельно измерять длительность взаимодействия и выбирать подходящее представление. Механизм события сам по себе не делает работу асинхронной или бесплатной.
change для поля выбора обозначает зафиксированное изменение значения. Для текстового поля, где нужна реакция при вводе, часто используют input, но это другая задача и другие требования к частоте обработки. Наш пример не является поиском: он выбирает одну из трёх заранее заданных тем.
Регистрация и объект обработчика объясняются в MDN об addEventListener. Различие текущего объекта обработки и источника события рассматривается в MDN о currentTarget.
Один набор обработчиков
Регистрацию выполняем один раз при запуске, а не внутри showSelection. Иначе каждое обновление могло бы добавлять новую функцию к тому же полю. Одно действие читателя вызвало бы несколько обработчиков и несколько повторных отрисовок. Визуально результат иногда выглядел бы правильным, но лишняя работа и побочные действия накапливались бы.
Передача в addEventListener должна быть самой функцией. Запись addEventListener('change', showSelection()) сначала выполнит showSelection и передаст её результат вместо обработчика. Скобки означают вызов сейчас. Наш вариант с функцией-обёрткой нужен, потому что сначала читается новое значение, затем вызывается отображение.
Для обработчика, который требуется позже удалить, удобно сохранить именованную функцию и передать ту же ссылку в removeEventListener. Новая похожая стрелочная функция не является той же функцией по идентичности. В маленьком каталоге жизненный цикл страницы прост, поэтому удаление не нужно, но принцип ссылки полезно понимать заранее.
Значение и отображение
Поле темы — источник нового выбора, а сообщение и список — результаты отображения. Не используйте количество DOM-элементов как замену данным каталога: оно может включать служебные карточки или прежнее состояние. Мы вычисляем visible.length из массива, затем показываем и карточки, и сообщение из одного результата.
Текущий обработчик реагирует на каждое изменение сразу. В следующем уроке заменим это поведение отправкой формы: читатель сможет настроить тему и порядок, затем применить их одной кнопкой. Это другое продуктовое решение, поэтому прежний обработчик нужно будет убрать, а не оставить параллельно с новым.
Для ручного наблюдения переключите тему несколько раз. Каждый раз ожидается ровно текущий набор без дубликатов, а исходный массив содержит четыре записи. Затем выберите текущую тему ещё раз: отсутствие изменения обычно не обязано порождать новое change. Такой случай напоминает, что события отражают действия среды, а не являются командой постоянно перерисовывать всё.
Источник события и текущий обработчик
У объекта события есть также target. Он обозначает исходный объект действия и не всегда совпадает с currentTarget. Если слушатель находится на контейнере, а пользователь нажал вложенную ссылку, целью будет ссылка, текущим объектом обработки — контейнер. Поэтому нельзя переносить чтение currentTarget.value на любой элемент без учёта места регистрации.
События могут распространяться от вложенного элемента к предкам. Такой механизм позволяет регистрировать один обработчик на списке, даже если карточки создаются позднее. Это называется делегированием, но в нашем текущем отборе оно не нужно: поле формы существует постоянно, и обработчик установлен непосредственно на нём. Не добавляйте обработчик списка лишь ради знакомства с термином.
Обычные ссылки карточек уже выполняют переход без JavaScript. Если когда-нибудь добавится действие «сохранить курс», используйте кнопку с понятным назначением, а не переопределяйте любой щелчок по карточке. Тогда нативный переход останется отдельным действием, а обработчик кнопки сможет опираться на стабильный id курса.
Наконец, изменение select.value программой не следует считать гарантированным действием пользователя. Если код сам восстанавливает выбор, он может сразу вызвать нужную функцию отображения. Не нужно искусственно отправлять событие там, где цель состоит в вызове собственной операции. Эта граница помогает отличать механизм уведомления среды от явного управления программой.
Обработчик использует существующий массив, а не перечитывает данные после каждого выбора. Это соответствует текущей задаче: тема меняет представление, а не источник. В следующей части серии загрузка станет отдельным действием кнопки обновления. Если смешать эти обязанности, каждое переключение темы могло бы вызывать ненужный запрос и показывать состояние ожидания вместо быстрого локального результата.
Программное состояние и поле должны иметь согласованное начало. Сейчас вариант all стоит первым в разметке, и объект selection также содержит all. Если поменять лишь одну сторону, первоначальные карточки могут не соответствовать показанному выбору. Синхронизация начинается с контракта, а не со случайного срабатывания первого события.
После обновления вкладки обработчик регистрируется для новой страницы, а прежняя работающая программа больше не управляет новым документом. Это отличается от накопления обработчиков внутри одного запуска. Сохраняйте диагностику так, чтобы различать повторное посещение и повторную регистрацию. Для нашего интерфейса одну регистрацию легко увидеть вне showSelection, рядом с единственным первоначальным вызовом отображения.
Клавиатурное управление остаётся нативным поведением select. Не добавляем отдельные обработчики стрелок и Enter, потому что это могло бы нарушить привычный механизм. К моменту перехода к форме у нас есть явный поток: действие читателя, новое состояние, отбор, создание карточек и сообщение результата.