Перейти к содержанию

DOM: поиск элементов страницы

Все предыдущие результаты выводились в консоль. Теперь перенесём их на страницу. Браузер представляет разобранный HTML как дерево объектов — DOM. JavaScript получает ссылку на нужный элемент и меняет его свойства, а браузер отражает это в интерфейсе.

Начнём с обязательных мест каталога: формы выбора, сообщения и списка карточек. В этом уроке ещё не создаём карточки. Результат будет проще: программа найдёт элементы и обновит текст сообщения, причём отсутствие нужного элемента обнаружится явно.

HTML как контракт интерфейса

Полностью замените index.html следующим документом. Пока используем обычный внешний скрипт с defer, как в первом уроке.

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Учебный каталог</title>
  <script src="./app.js" defer></script>
</head>
<body>
<main>
  <h1>Учебный каталог</h1>
  <form id="catalog-controls">
    <label>Тема <select name="topic">
      <option value="all">Все темы</option>
      <option value="frontend">Фронтенд</option>
      <option value="publishing">Публикация</option>
    </select></label>
  </form>
  <p id="catalog-status" role="status" aria-live="polite">Каталог ещё не показан.</p>
  <ul id="catalog-list" aria-label="Курсы"></ul>
</main>
</body>
</html>

Идентификаторы id должны быть уникальны в документе. Они связывают постоянные места HTML с программой. Поле name="topic" относится к значению формы и будет использовано в следующей части серии. Названия и количество карточек здесь пока не дублируем: позже они появятся из массива данных.

Вложенная подпись label связывает понятный текст с полем выбора. Список ul задаёт семантику набора элементов; отдельные карточки станут его li. Мы используем обычные HTML-элементы, чтобы клавиатурное поведение формы не пришлось заново программировать.

Атрибуты сообщения задают область уведомления об изменении состояния. Они не исправляют ошибочную логику и не обещают одинаковый способ озвучивания всеми вспомогательными программами. Для реального проекта понадобилась бы ручная проверка доступности. Здесь важно, что сообщение находится на странице, а не существует только в консоли.

Ссылка на DOM-узел

Пока полностью замените app.js этим коротким примером:

const list = document.querySelector('#catalog-list');
const status = document.querySelector('#catalog-status');
const form = document.querySelector('#catalog-controls');
if (!list || !status || !form) {
  throw new Error('В HTML отсутствует обязательный элемент каталога');
}
status.textContent = 'Места каталога найдены. Готовим четыре курса.';
console.log(list.tagName);

Ожидаемое сообщение появится вместо первоначального текста абзаца. В консоли ожидается UL. Метод querySelector принимает CSS-селектор и возвращает первый подходящий элемент либо null. Знак решётки обозначает выбор по идентификатору, как в CSS.

Проверка выполняется до использования найденных узлов. Если имя в HTML потеряно или изменено, программа выбрасывает объяснимую ошибку вместо попытки прочитать свойство у null. Это ошибка устройства страницы, а не ошибка входного JSON; не стоит скрывать её как успешный пустой каталог.

status содержит объект элемента, а не текстовую копию HTML. Присваивание свойству textContent меняет текст этого элемента. Внешняя переменная объявлена через const, но сам объект DOM изменяемый — та же разница связи и содержимого, которую мы рассмотрели на массивах.

Механизм выбора и отсутствие совпадения описаны в MDN о querySelector. Общая связь документа с DOM представлена в обзоре DOM.

Момент выбора и область поиска

Наш скрипт выполняется после разбора HTML благодаря defer. Если обычный скрипт в начале документа выполнялся бы сразу, нужные элементы могли бы ещё не существовать. Правильный селектор сам по себе не исправляет неправильный момент выполнения. Поэтому сначала сохраните знакомый способ подключения, а не добавляйте случайные задержки.

В модулях позднее появится другой способ подключения, который также учитывает разбор документа. Сейчас не нужны одновременно defer, обработчик DOMContentLoaded и таймер на всякий случай. Достаточно одного понятного механизма запуска и проверки обязательных узлов.

Поиск можно ограничить элементом формы: form.querySelector('[name="topic"]') получает поле именно внутри неё. Так разные формы могут иметь одинаковые имена полей, не мешая друг другу. Идентификаторы остаются уникальными на странице, а name обозначает роль поля в конкретном наборе данных.

querySelectorAll получает коллекцию всех совпадений. Не стоит применять его там, где контракт обещает один узел: это потребовало бы дополнительно выбирать индекс. И наоборот, один querySelector не подходит для изменения всех карточек. Сначала решите, сколько результатов должно существовать, затем выберите метод.

Проверка связи разметки и программы

Для самостоятельного наблюдения временно измените id="catalog-status" на другое имя и обновите страницу. Ожидается наша ошибка обязательного элемента, а первоначальный текст останется в документе. Восстановите идентификатор и убедитесь, что ожидаемое новое сообщение снова должно появиться.

Переименование узла в CSS и в JavaScript — связанное изменение. Красивое оформление может продолжать работать по классу, а программа уже перестанет находить элемент по прежнему id. Поэтому общие имена закреплены контрактом серии. Они относятся к интерфейсу между разметкой и кодом, а не к случайному выбору автора листинга.

Изменения DOM не переписывают index.html на диске. После обновления вкладки браузер снова получает первоначальную разметку и выполняет программу. Это различие объясняет, почему редактор исходника не показывает текст, присвоенный через textContent, и почему интерактивное состояние не является сохранением материала сайта.

Селектор и назначение элемента

Для поиска можно использовать класс, идентификатор или селектор атрибута. Наши обязательные места имеют id, потому что программа ожидает один список и одно сообщение. Класс оформления лучше связывать с внешним видом. Тогда дизайнер может изменить название визуального класса карточки, не разрушая постоянную точку подключения управления.

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

Неверный синтаксис CSS-селектора отличается от отсутствия совпадения: метод может выбросить исключение ещё при попытке разобрать строку. Наши селекторы постоянны и просты, поэтому не требуют экранирования. Если позже имя создаётся из произвольного внешнего значения, такую ситуацию нужно рассматривать отдельно. Не вставляйте любой текст читателя в селектор без понимания допустимой формы.

Сохранённая ссылка на узел не является автоматическим повторным поиском. Если программа заменит сам элемент ul новым, переменная list продолжит указывать на прежний узел. Поэтому дальше меняем только детей постоянного списка, а не уничтожаем его оболочку. Это небольшое решение сохраняет ясность всех последующих обработчиков.

Свойства DOM не всегда совпадают с буквальными атрибутами исходника. Например, свойство элемента ссылки может показывать вычисленный абсолютный адрес, хотя в HTML записан относительный путь. Это не означает, что программа переписала файл на диске. Объект браузера представляет текущий документ и предоставляет интерфейс работы с ним.

Поэтому при диагностике полезно различать исходный HTML, текущее дерево элементов и свойства выбранного объекта. Инструмент просмотра исходника показывает полученный текст документа; панель элементов показывает состояние DOM после действий программы. Эти представления могут расходиться ожидаемо, если карточки были созданы динамически.

В текущем уроке изменение сообщения — специально небольшой наблюдаемый эффект. Он подтверждает, что найден именно нужный узел и изменение применяется. Если сразу построить сложные карточки, неверный селектор, потерянные данные и CSS-ошибка станут похожими причинами пустого места. Последовательное усложнение отделяет эти причины. После подтверждения связи перейдём к созданию детей списка, сохраняя уже найденную оболочку и сообщение как постоянные элементы.

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

Оглавление курса