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

Создание карточек через DOM

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

Продолжаем документ предыдущего урока. В app.js восстановите массив courses из урока об объектах и объявления list, status, form с проверкой обязательных узлов. Ниже добавим одну функцию представления. Она получает список и массив уже пригодных для отображения курсов; проверка данных остаётся отдельной обязанностью.

От объекта курса к элементам

Полная функция создания карточек:

function renderCourses(list, courses) {
  const nodes = courses.map(course => {
    const item = document.createElement('li');
    const link = document.createElement('a');
    const details = document.createElement('p');
    link.href = course.url;
    link.textContent = course.title;
    details.textContent = `Уроков: ${course.lessons}`;
    item.dataset.courseId = course.id;
    item.append(link, details);
    return item;
  });
  list.replaceChildren(...nodes);
}
renderCourses(list, courses);
status.textContent = `Курсов: ${courses.length}.`;

Ожидается четыре пункта списка с названием-ссылкой и подписью количества. document.createElement создаёт узел, но сначала он ещё не является видимой частью документа. Вызов item.append связывает дочерние узлы с карточкой. В конце replaceChildren помещает весь новый набор в существующий список.

Наш map теперь преобразует объекты данных в объекты DOM. Функция обработки возвращает каждый li, поэтому nodes содержит элементы, а не значения undefined. Если потерять return item, список не получит ожидаемые карточки. Это тот же механизм возвращаемого результата, который ранее использовали для строковых подписей.

Свойство dataset.courseId создаёт атрибут data-course-id. Он позволяет видеть постоянный идентификатор при исследовании карточки в инструментах разработчика. Это не новая тема курса и не номер позиции. Идентификатор не меняется при сортировке, потому что берётся из самой записи.

Создание элементов рассматривается в MDN о createElement; замена детей — в MDN о replaceChildren.

Текст и HTML

Для названия используем textContent. Если название содержит угловые скобки, они будут отображаться как текст, а не интерпретироваться как разметка. Это соответствует обязанности поля title: оно содержит название, а не исполняемый HTML-фрагмент.

Не заменяйте присваивание на innerHTML ради более короткого шаблона со строками. При загружаемых данных такое решение потребует отдельной стратегии безопасного HTML. В нашем плоском каталоге разметку полностью задаёт программа, а внешние значения становятся только текстом. Механизм textContent описан в MDN.

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

Создайте локальные страницы назначения в папке courses или возьмите заглушки итогового fixture. До появления таких файлов щелчок ссылки ожидаемо не покажет настоящую учебную страницу. Это ошибка отсутствующего ресурса проекта, а не создания элемента a. Не подменяйте адрес ссылкой #: она скрыла бы неполноту файла и изменила назначение карточки.

Повторное отображение

Добавьте после первого отображения временный вариант:

const publishingCourses = courses.filter(course => course.topic === 'publishing');
renderCourses(list, publishingCourses);
status.textContent = `Курсов: ${publishingCourses.length}.`;

Ожидается только Markdown-карточка, а сообщение покажет один курс. Первый набор заменяется вторым. Если бы мы просто добавляли каждую карточку к списку через append, на странице осталось бы пять элементов: четыре прежних и один новый. Такой результат не выражает текущий отбор.

Spread-запись ...nodes в вызове передаёт элементы массива отдельными аргументами. Без неё массив был бы одним аргументом с другим преобразованием. Это применение знакомой записи распаковки к вызову функции, а не глубокое копирование объектов.

Передача пустого массива вызывает replaceChildren без новых узлов и очищает список. Само отсутствие карточек не объясняет причину, поэтому сообщение должно назвать состояние: по теме ничего нет, загрузка ещё не началась или произошла ошибка. В этом уроке пока показываем длину; позднее управляющая программа выберет точную подпись состояния.

Функция представления и данные

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

Полная замена списка удобна для четырёх учебных записей. Для очень большого каталога и сложных интерактивных карточек стоит отдельно рассматривать объём DOM, сохранение фокуса и частичное обновление. Не объявляем выбранный приём универсально самым быстрым: здесь важнее ясный результат без накопления дубликатов.

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

Для ручного наблюдения уберите временный фильтр, вызовите renderCourses(list, courses) дважды и посчитайте элементы. Ожидается всё ещё четыре карточки. Затем передайте пустой массив и ожидайте пустой список. Эти два результата проверяют именно замену набора, а не только правильность текстов.

Полная замена детей не требует очищать list.textContent перед каждым вызовом: replaceChildren уже выполняет нужную замену. Два независимых способа очистки сделали бы поток сложнее без нового результата. Особенно опасно присвоить textContent не списку, а всему main: тогда исчезнут форма и сообщение, хотя сохранённые переменные всё ещё будут ссылаться на прежние узлы.

При создании карточки свойство href и видимый текст не обязаны совпадать. Название объясняет назначение человеку, адрес определяет ресурс перехода. Не показывайте машинный id вместо понятной подписи только ради того, что он короткий. В то же время одинаковая видимая подпись нескольких разных ссылок может ухудшить понимание выбора; в расширенном каталоге это решается содержанием карточек, а не сменой механизма DOM.

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

Порядок аргументов append определяет порядок детей внутри карточки. Ссылка появляется перед подписью количества. Если понадобилась другая структура, меняем создание узлов, а не подставляем дополнительный HTML в название. Так данные курса не приобретают обязанности верстки.

Для учебного наблюдения временно задайте название с символами < и >. Ожидается буквальный текст внутри ссылки, а не новый элемент. После опыта восстановите постоянное название. Этот результат проверяет выбранную границу текста. Он не доказывает безопасность любой другой части интерфейса: ссылки имеют свой контракт, события — свою логику, а внешнее оформление рассматривается отдельно. Один безопасный способ вставки не заменяет понимание остальных значений.

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

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