MakeFirstStep· Frontend + ИИ

Урок 8 · DOM

DOM: поиск и изменение элементов

До сих пор JavaScript жил в консоли: мы считали, фильтровали и выводили текст. Сегодня он выходит на страницу. Каталог товаров, корзина, список задач, оценки в личном кабинете — всё это на настоящих сайтах рисует JavaScript, который находит нужное место на странице и меняет его. Сегодня вы соберёте страницу, в которой весь контент создаётся из массива.

  • Объяснить, что такое DOM и чем он отличается от HTML-файла
  • Находить элементы через querySelector и querySelectorAll
  • Менять текст, разметку, классы, атрибуты и data-*
  • Создавать элементы через createElement, вставлять их и удалять
  • Рисовать список карточек из массива объектов и очищать его
  • Делить работу с ИИ: он пишет разметку и стили, вы — логику
Как работать с этой страницей

В этом уроке много песочниц. В каждой три вкладки: index.html (разметка), style.css (стили, если есть) и script.js (ваш код). Нажмите ▶ Запустить или Ctrl + Enter — ниже появится живая страница, а под ней консоль. Задачи проверяются по тому, что вы вывели через console.log, поэтому оставляйте строки с выводом на месте. Подсказки спрятаны: сначала попробуйте сами.

Одно правило песочницы: ваша разметка стоит в <body> не одна — после неё браузер добавляет служебные <script>. Поэтому мы всегда кладём содержимое в одну обёртку или ищем элементы по селектору, а не считаем детей у body.

01 Что такое DOM

Вы пишете HTML — это обычный текст в файле. Когда браузер открывает страницу, он читает этот текст и строит из него дерево объектов. Это дерево называется DOM — Document Object Model, «объектная модель документа». Каждый тег превращается в объект, у которого есть свойства (текст, классы, размеры) и методы. Именно с этими объектами работает JavaScript.

Аналогия из жизни

HTML-файл — это напечатанное меню в папке. DOM — это накрытый стол в чойхоне, который строится по этому меню. Официант (JavaScript) может поставить на стол ещё одну самбусу, убрать пустую пиалу или поменять чай. Напечатанное меню в папке от этого не меняется. Обновили страницу — стол накрывается заново по меню.

Вот простая страница и дерево, которое из неё получается:

HTML-файл
<body>
  <h1>Меню</h1>
  <ul class="menu">
    <li>Самбуса</li>
    <li>Курутоб</li>
  </ul>
</body>
  • document
    • body
      • h1
        • «Меню»текстовый узел
      • ul.menu
        • li → «Самбуса»
        • li → «Курутоб»
Тег внутри тега — это «ребёнок» внутри «родителя». Текст тоже узел дерева.
document
Корень всего дерева и «входная дверь» в страницу. Через него вы ищете элементы: document.querySelector(...).
Элемент
Объект, соответствующий тегу: <h1>, <li>, <div>. Именно их мы будем искать, менять, создавать и удалять.
Родитель и дети
<ul> — родитель двух <li>. Два <li> — соседи (siblings).
Текстовый узел
Кусочек текста внутри элемента. Пробелы и переносы строк между тегами — тоже текстовые узлы.

Как посмотреть DOM своими глазами

Откройте любой сайт, нажмите F12 и перейдите на вкладку Elements. Это и есть живой DOM: вы видите дерево, можете кликать по элементам, менять текст двойным щелчком, перетаскивать теги — и страница реагирует сразу. Сам файл при этом не меняется. Выделенный в Elements элемент доступен в консоли как $0.

Ходим по дереву

У каждого элемента есть «стрелки» к соседним узлам. Они пригодятся, когда один элемент найден и нужно добраться до соседей:

СвойствоЧто даёт
el.parentElementродитель
el.childrenвсе дети-элементы (список, у него есть .length)
el.firstElementChild / el.lastElementChildпервый и последний ребёнок
el.nextElementSibling / el.previousElementSiblingследующий и предыдущий сосед
Ходим по дереву · Ctrl + Enter
<ul class="menu">
  <li>Самбуса</li>
  <li>Курутоб</li>
  <li>Шакароб</li>
</ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

textContent — это текст внутри элемента. Подробнее про него — в третьем разделе, пока просто пользуйтесь.

Частая ошибка: childNodes вместо children

Есть похожее свойство childNodes. Оно возвращает все узлы, включая пробелы и переносы строк между тегами. Поэтому «детей» оказывается больше, чем вы видите глазами. Для работы с элементами используйте children и семейство ...ElementChild, ...ElementSibling.

childNodes считает и пробелы · Ctrl + Enter
const box = document.createElement("div");
box.innerHTML = `
  <p>Один</p>
  <p>Два</p>
`;

console.log(box.children.length);    // → 2 — два абзаца
console.log(box.childNodes.length);  // → 5 — два абзаца и три «пустых» текстовых узла

Читать подробнее: learn.javascript.ru — Дерево DOM · Навигация по DOM-элементам

Практика: закрепляем

Три задачи на ходьбу по дереву. Результат проверяется по выводу в консоль под песочницей.

ЗАДАЧА 1.1

Сколько детей у списка

лёгкая

В песочнице — список блюд. Выведите в консоль три строки: сколько у списка детей, текст первого блюда и текст последнего.

Список блюд · Ctrl + Enter
<ul>
  <li>Нон</li>
  <li>Сомса</li>
  <li>Чой</li>
  <li>Мева</li>
</ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
4
Нон
Мева
Подсказка 1

Количество детей — list.children.length.

Подсказка 2

Первый ребёнок — list.firstElementChild, последний — list.lastElementChild. Текст берём через .textContent. Каждое значение — отдельный console.log.

ЗАДАЧА 1.2

Предскажите вывод

лёгкая

Сначала не запуская код, запишите в тетрадь, что выведут пять строк. Потом запустите и сверьтесь. После этого добавьте шестую строку: выведите текст абзаца «Преподаватель…», но двигаясь от конца — через group.lastElementChild.previousElementSibling.

Группа JS-12 · Ctrl + Enter
<div class="group">
  <h2>Группа JS-12</h2>
  <p>Преподаватель: Шахром</p>
  <p>Студентов: 12</p>
</div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
H2
Преподаватель: Шахром
Студентов: 12
DIV
3
Преподаватель: Шахром
Подсказка 1

Последний ребёнок группы — абзац «Студентов: 12». Нам нужен абзац перед ним.

Подсказка 2

console.log(group.lastElementChild.previousElementSibling.textContent);

ЗАДАЧА 1.3

Лишние узлы

средняя

Выведите в консоль: сколько у списка childNodes, сколько children, а затем текст каждого города — каждый на своей строке (используйте for...of по list.children). Подумайте, откуда взялась разница в первых двух числах.

Города · Ctrl + Enter
<ul>
  <li>Душанбе</li>
  <li>Худжанд</li>
  <li>Бохтар</li>
</ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
7
3
Душанбе
Худжанд
Бохтар
Подсказка 1

Между тегами в разметке есть переносы строк и отступы. Каждый такой кусочек — отдельный текстовый узел: три перед <li> и один после последнего. Итого 3 элемента + 4 текстовых узла = 7.

Подсказка 2

for (const li of list.children) { console.log(li.textContent); }

02 Поиск элементов: querySelector

Чтобы что-то изменить на странице, сначала нужно это «что-то» найти. Для поиска в современном JavaScript почти всегда хватает двух методов. Главное: в них вы пишете обычный CSS-селектор — тот же, что в файле стилей. Новый язык учить не нужно.

document.querySelector(селектор)
Возвращает первый подходящий элемент. Если ничего не нашлось — null.
document.querySelectorAll(селектор)
Возвращает список всех подходящих элементов (NodeList). Если ничего нет — пустой список, не null.
Ищем по селекторам · Ctrl + Enter
<h1 id="title">Чайхона «Сомон»</h1>
<ul class="menu">
  <li class="item">Курутоб</li>
  <li class="item">Оши палав</li>
  <li class="item hot">Шакароб</li>
</ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Аналогия из жизни

Вы стоите на Зелёном базаре и говорите: «Найдите мне самую первую палатку с курагой» — это querySelector. А «Покажите все палатки с курагой» — это querySelectorAll. Селектор — это то, как вы описываете палатку: по номеру (#id), по вывеске (.class) или по типу (li).

Какие селекторы можно писать

СелекторЧто находит
"#title"элемент с id="title"
".item"элементы с классом item
"li"все теги <li>
".menu li"<li> внутри .menu (на любой глубине)
".menu > li"<li> — прямые дети .menu
"li:first-child", "li:nth-child(2)"первый и второй по счёту
"[data-id='17']"по атрибуту
"input[type='text']"тег и атрибут вместе
Совет

Если не уверены, правильный ли селектор, проверьте его в DevTools: на вкладке Elements нажмите Ctrl + F и вставьте селектор. Браузер подсветит найденное и покажет, сколько совпадений.

NodeList: похож на массив, но не массив

querySelectorAll возвращает NodeList. У него есть .length, к элементам можно обратиться по индексу, его можно перебирать через for...of и forEach. Но методов map, filter и reduce у него нет. Если нужны — превращаем в настоящий массив:

Можно редактировать · Ctrl + Enter
const box = document.createElement("div");
box.innerHTML = `<p class="n">10</p><p class="n">20</p><p class="n">30</p>`;

const nodes = box.querySelectorAll(".n");

console.log(nodes.length);         // → 3
console.log(nodes[1].textContent); // → 20

nodes.forEach(p => console.log("p:", p.textContent));   // forEach есть

const numbers = Array.from(nodes).map(p => Number(p.textContent)); // map — только у массива
// то же самое: [...nodes].map(...)
console.log(numbers);              // → [10, 20, 30]

Ищем не по всей странице, а внутри элемента

Метод querySelector есть не только у document, но и у любого элемента. Тогда поиск идёт только внутри него. Это важно, когда на странице много одинаковых карточек и нужно достать цену именно из этой карточки:

Поиск внутри карточки · Ctrl + Enter
<div class="card"><span class="name">Шапка</span> — <span class="price">60</span></div>
<div class="card"><span class="name">Шарф</span> — <span class="price">45</span></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Есть и старые методы: getElementById("title"), getElementsByClassName, getElementsByTagName. Они работают, и вы встретите их в чужом коде, но querySelector универсальнее — достаточно помнить один способ.

Частая ошибка: элемент не найден (null)

Если querySelector ничего не нашёл, он возвращает null. А у null нет ни textContent, ни других свойств — получаем самую знаменитую ошибку DOM. Причин обычно три: опечатка в селекторе, забытая точка или решётка ("item" вместо ".item"), либо скрипт запустился раньше, чем элемент появился на странице.

Этот код выдаст ошибку · Ctrl + Enter
const price = document.querySelector(".price"); // на странице такого элемента нет → null
console.log(price);                              // → null
console.log(price.textContent);                  // ✖ TypeError: Cannot read properties of null

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

index.html — как подключать скрипт в реальном проекте
<body>
  <ul id="list"></ul>
  <script src="script.js" defer></script>
</body>
Привычка на всю жизнь

Найденные элементы кладите в переменные один раз в начале скрипта: const list = document.querySelector("#list");. Не ищите один и тот же элемент по десять раз — код станет короче, а браузер быстрее.

Читать подробнее: learn.javascript.ru — Поиск: getElement*, querySelector* · MDN — querySelector

Практика: закрепляем

Находим элементы разными способами.

ЗАДАЧА 2.1

Найди по id и классу

лёгкая

Выведите в консоль три строки: текст заголовка (найдите по id), текст первого блюда с классом item и количество всех блюд с этим классом.

Меню чайханы · Ctrl + Enter
<h1 id="title">Чайхона «Сомон»</h1>
<ul class="menu">
  <li class="item">Курутоб</li>
  <li class="item">Оши палав</li>
  <li class="item hot">Шакароб</li>
</ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
Чайхона «Сомон»
Курутоб
3
Подсказка 1

Заголовок: document.querySelector("#title"). Решётка — для id, точка — для класса.

Подсказка 2

Первый .item — querySelector(".item"), все — querySelectorAll(".item") и затем .length.

ЗАДАЧА 2.2

Найди три бага в селекторах

средняя

Код хочет вывести заголовок, количество товаров и текст товара со скидкой, но ничего не работает. Запустите его, прочитайте ошибку в консоли и исправьте три места, помеченные номерами. Исправляйте по одному и каждый раз запускайте — ошибки открываются по очереди.

Корзина (с багами) · Ctrl + Enter
<h1 id="title">Корзина</h1>
<ul id="cart">
  <li class="item">Нон — 4</li>
  <li class="item">Молоко — 12</li>
  <li class="item sale">Зардолу — 25</li>
</ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
Корзина
3
Зардолу — 25
Подсказка 1

Бага 1: у заголовка не класс, а id — значит, нужна решётка. Бага 3 — наоборот: sale — это класс, а не id.

Подсказка 2

Баг 2: querySelector вернёт один элемент, а у него нет .length. Чтобы посчитать все товары, нужен querySelectorAll.

ЗАДАЧА 2.3

Цены из карточек

сложная

В каталоге четыре карточки. Переберите их, достаньте цену и название из каждой (поиск внутри карточки), посчитайте общую сумму и найдите самую дорогую. Цена в textContent — строка, превращайте её в число через Number(...).

Каталог · Ctrl + Enter
<div class="card"><span class="name">Шапка</span> <span class="price">60</span></div>
<div class="card"><span class="name">Шарф</span> <span class="price">45</span></div>
<div class="card"><span class="name">Перчатки</span> <span class="price">35</span></div>
<div class="card"><span class="name">Носки</span> <span class="price">15</span></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
Итого: 155 сомони
Самый дорогой: Шапка
Подсказка 1

Цикл for (const card of cards) { ... }. Внутри: const price = Number(card.querySelector(".price").textContent); и такая же строка для имени.

Подсказка 2

Сумма: total += price. Если price > maxPrice, запишите новую цену в maxPrice и имя — в maxName.

03 Меняем содержимое: textContent и innerHTML

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

textContent — только текст

Читает и записывает обычный текст. Любые < и > считаются просто символами, теги не создаются. Быстро и безопасно.

innerHTML — текст и разметка

Читает и записывает HTML-строку. Теги внутри превращаются в настоящие элементы. Мощно, но опасно, если в строке чужой текст.

textContent и innerHTML · Ctrl + Enter
<h2 id="dish">Загрузка…</h2>
<div id="box">Старое содержимое</div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Аналогия из жизни

Представьте, что вы заказываете торт с надписью. textContent — это когда кондитер пишет на торте ровно то, что вы сказали, даже если вы продиктовали странные значки. innerHTML — когда вы даёте кондитеру инструкцию: «поставь здесь украшение, а вот тут сделай жирную надпись». Он выполнит инструкцию буквально. Если инструкцию написал посторонний человек — он может заставить кондитера сделать что угодно.

Читаем и пишем

  • el.textContent — прочитать весь текст внутри элемента (включая вложенные теги, но без самих тегов).
  • el.textContent = "новый текст" — заменить всё содержимое одним текстом.
  • el.innerHTML — прочитать содержимое вместе с тегами, как строку.
  • el.innerHTML = "<b>жирный</b>" — заменить содержимое, разобрав строку как HTML.

Есть ещё innerText: он учитывает CSS и не показывает скрытый текст. Для обычной работы берите textContent — он проще и предсказуемее.

Почему innerHTML опасен

Допустим, на вашем сайте пользователь пишет комментарий, а вы вставляете его на страницу через innerHTML. Если вместо «Отличный курс!» он напишет специально подготовленный тег <img> со скрытым кодом — браузер создаст этот элемент и выполнит чужой код на страницах других посетителей. Такая атака называется XSS и входит в число самых распространённых в вебе.

Правило безопасности

Всё, что пришло от пользователя, из формы, из адресной строки или от внешнего API, вставляйте через textContent. innerHTML — только для разметки, которую написали вы сами (или ИИ, и вы её проверили). Посмотрите, как выглядит разница:

Можно редактировать · Ctrl + Enter
const comment = "Круто! <b>Пять звёзд</b>";      // представим, что это ввёл пользователь

const safe = document.createElement("div");
safe.textContent = comment;          // текст остался текстом
console.log(safe.children.length);   // → 0 — никаких тегов не появилось

const risky = document.createElement("div");
risky.innerHTML = comment;           // строка разобрана как HTML
console.log(risky.children.length);  // → 1 — появился настоящий тег <b>

Ловушка: += для innerHTML

Запись list.innerHTML += "<li>…</li>" выглядит как «добавить в конец», но на самом деле браузер сносит весь список и собирает заново. Все ранее найденные элементы в ваших переменных теряют связь со страницей.

Что происходит при += для innerHTML · Ctrl + Enter
const list = document.createElement("ul");
list.innerHTML = "<li>Нон</li>";

const first = list.firstElementChild;     // запомнили первый пункт
list.innerHTML += "<li>Чой</li>";         // «добавили» второй

console.log(list.children.length);             // → 2
console.log(first === list.firstElementChild); // → false — это уже другой, новый элемент!
console.log(list.contains(first));             // → false — старый пункт выброшен

В разделе 5 вы узнаете, как добавлять элементы без такого побочного эффекта — через createElement и append.

Собираем список строкой

Если разметку пишете вы сами, а данные безопасные (ваш массив, а не ввод пользователя), удобно собирать HTML шаблонными строками и методами массивов, которые вы уже знаете:

Можно редактировать · Ctrl + Enter
const dishes = ["Самбуса", "Курутоб", "Шакароб"];

const html = dishes.map(d => `<li>${d}</li>`).join("");
console.log(html);   // → <li>Самбуса</li><li>Курутоб</li><li>Шакароб</li>

const menu = document.createElement("ul");
menu.innerHTML = html;
console.log(menu.children.length); // → 3

Читать подробнее: learn.javascript.ru — Свойства узлов DOM · MDN — textContent

Практика: закрепляем

Читаем и пишем содержимое.

ЗАДАЧА 3.1

Заголовок и цена

лёгкая

Страница показывает заглушки «Загрузка…». Замените текст заголовка на Курутоб, а текст цены — на 35 сомони через textContent. После замены выведите оба текста в консоль.

Карточка блюда · Ctrl + Enter
<h1 id="title">Загрузка…</h1>
<p class="price">0 сомони</p>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
Курутоб
35 сомони
Подсказка 1

Запись в свойство — это присваивание: title.textContent = "...";

Подсказка 2

Две строки между поиском и console.log: title.textContent = "Курутоб"; и price.textContent = "35 сомони";

ЗАДАЧА 3.2

Безопасный вывод имени

средняя

Строка userName — это «ввод пользователя». Положите её в блок #a через innerHTML, а в блок #b — через textContent. Посмотрите на preview: в каком блоке видны теги? Консоль подтвердит разницу.

Две вставки · Ctrl + Enter
<div id="a"></div>
<div id="b"></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
1
0
Ali <b>Pro</b>
Подсказка 1

Каждый блок — одна строка присваивания: a.innerHTML = userName;

Подсказка 2

Второй блок — точно так же, но свойство textContent. В #a появится настоящий тег <b> (1 ребёнок), в #b — просто текст с видимыми угловыми скобками (0 детей).

ЗАДАЧА 3.3

Меню через map и join

средняя

Соберите HTML-строку из массива dishes: каждое блюдо — <li>. Используйте map и join(""). Запишите строку в #menu через innerHTML и выведите в консоль количество детей и саму разметку.

Меню из массива · Ctrl + Enter
<ul id="menu"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
3
<li>Самбуса</li><li>Курутоб</li><li>Шакароб</li>
Подсказка 1

map превращает каждое блюдо в строку с тегами, join("") склеивает их без пробелов. Шаблон: `<li>${d}</li>`.

Подсказка 2

menu.innerHTML = dishes.map(d => `<li>${d}</li>`).join("");

04 Классы, стили и атрибуты

Внешний вид элемента почти всегда задаётся в CSS. JavaScript не должен красить страницу сам, его работа — включать и выключать готовые классы. Вы пишете в CSS, как выглядит .hot, а в JS только решаете, когда элементу его выдать. Так код остаётся чистым, а дизайн меняется в одном месте.

Аналогия из жизни

Класс — это форма на курсе: бейдж «преподаватель», «студент», «гость». Бейдж один раз придумали, дизайн утвердили. Охранник (JS) не рисует каждому новый бейдж, а просто выдаёт готовый или забирает. Выдать — add, забрать — remove, поменять на противоположный — toggle.

classList: управляем классами

МетодЧто делает
el.classList.add("hot")добавляет класс (если уже есть — ничего не происходит)
el.classList.remove("old")убирает класс
el.classList.toggle("open")есть — убирает, нет — добавляет
el.classList.contains("hot")возвращает true или false
el.classList.replace("old", "new")заменяет один класс другим
Подсвечиваем «горячее» · Ctrl + Enter
<ul class="menu">
  <li class="item">Самбуса</li>
  <li class="item">Курутоб</li>
  <li class="item">Шакароб</li>
</ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Частая ошибка: className = "…" затирает все классы

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

Можно редактировать · Ctrl + Enter
const el = document.createElement("div");
el.className = "card sale";

el.className = "hot";                 // затёрли «card» и «sale»!
console.log(el.className);            // → hot

el.className = "card sale";
el.classList.add("hot");              // правильно: добавили к существующим
console.log(el.className);            // → card sale hot

style: быстрый, но грубый способ

Через el.style.свойство можно задать стиль прямо у элемента. Названия свойств пишутся в camelCase: background-color становится backgroundColor, а значения — всегда строки:

el.style · Ctrl + Enter
<p id="note">Скидка до конца недели</p>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Правило: если стиль нужен надолго или нескольким элементам — заведите класс. style оставляйте для значений, которые рассчитываются в коде (ширина полоски загрузки, позиция). Так JS не смешивается с дизайном, и ИИ-помощнику проще вам помогать.

Атрибуты: getAttribute, setAttribute и свойства

Атрибуты — это то, что пишется в открывающем теге: href, src, alt, disabled, data-id. Читать и менять их можно двумя способами:

СпособПримерКогда использовать
Методыel.getAttribute("href"), el.setAttribute("href", "…"), el.removeAttribute("disabled"), el.hasAttribute("disabled")универсально, подходит для любых атрибутов, в том числе data-*
Свойстваinput.value, button.disabled = true, el.hidden = true, img.src, a.hrefдля стандартных атрибутов — короче и удобнее

data-атрибуты: свои данные в разметке

Хотите «привязать» к элементу собственные данные — например, id товара или его цену? Для этого придуманы атрибуты, начинающиеся с data-. В JS они доступны через el.dataset, причём имя после data- превращается в свойство:

dataset · Ctrl + Enter
<button id="buy" data-id="17" data-price="35" data-product-name="Курутоб">Купить</button>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ловушка: всё в dataset — строки

dataset.price вернёт "35", а не 35. Складывать такие значения опасно: "35" + "5" даст "355". Перед расчётами всегда превращайте в число через Number(...).

Читать подробнее: learn.javascript.ru — Стили и классы · Атрибуты и свойства · MDN — classList

Практика: закрепляем

Классы, стили и атрибуты на деле.

ЗАДАЧА 4.1

Бейджи в меню

лёгкая

Добавьте класс hot блюду «Курутоб», а у «Шакароб» уберите класс old. Потом выведите className каждого пункта — по одному на строке.

Меню с бейджами · Ctrl + Enter
<ul class="menu">
  <li class="item">Самбуса</li>
  <li class="item">Курутоб</li>
  <li class="item old">Шакароб</li>
</ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
item
item hot
item
Подсказка 1

Используйте classList.add("...") и classList.remove("..."), а не className =.

Подсказка 2

items[1].classList.add("hot"); и items[2].classList.remove("old");

ЗАДАЧА 4.2

Панель: toggle и contains

лёгкая

У панели изначально классы panel open. Выполните по порядку: 1) выведите, есть ли класс open; 2) переключите open и снова выведите, есть ли он; 3) переключите open обратно, добавьте переключением класс dark и выведите className.

Панель · Ctrl + Enter
<div id="panel" class="panel open">Меню</div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
true
false
panel open dark
Подсказка 1

После первого toggle("open") класса не будет (false), после второго — он вернётся в конец списка классов.

Подсказка 2

Третий шаг: panel.classList.toggle("open"); panel.classList.toggle("dark"); console.log(panel.className); Порядок классов — по порядку добавления.

ЗАДАЧА 4.3

Ссылка и кнопка покупки

средняя

Выполните четыре действия и выведите результат каждого: 1) смените href ссылки на https://makefirststep.tj и выведите его через getAttribute; 2) выведите цену из data-price, умноженную на 2 (как число); 3) смените data-id на 18 через dataset и выведите значение через getAttribute("data-id"); 4) отключите кнопку и выведите hasAttribute("disabled").

Ссылка и кнопка · Ctrl + Enter
<a id="link" href="https://example.com">Сайт</a>
<button id="buy" data-id="17" data-price="35">Купить</button>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
https://makefirststep.tj
70
18
true
Подсказка 1

link.setAttribute("href", "https://makefirststep.tj"); — первым аргументом имя атрибута, вторым значение.

Подсказка 2

Свойство disabled у кнопки — логическое: buy.disabled = true;. После этого атрибут disabled появляется в разметке, и hasAttribute вернёт true.

ЗАДАЧА 4.4

Светофор цен

сложная

У каждой карточки есть data-price. Переберите карточки: если цена 50 и больше — добавьте класс expensive, если меньше 20 — класс sale, иначе ничего. Выведите className каждой карточки, а затем количество карточек с классом expensive (через querySelectorAll).

Светофор цен · Ctrl + Enter
<div class="card" data-price="60">Шапка</div>
<div class="card" data-price="45">Шарф</div>
<div class="card" data-price="35">Перчатки</div>
<div class="card" data-price="15">Носки</div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
card expensive
card
card
card sale
1
Подсказка 1

Внутри цикла нужны два if: if (price >= 50) card.classList.add("expensive"); и такой же для price < 20. Не забудьте Number(...) — у вас она уже есть.

Подсказка 2

В конце: console.log(document.querySelectorAll(".expensive").length);

05 Создаём и удаляем элементы

Пока мы только находили и меняли то, что уже написано в HTML. Но у настоящего сайта, например интернет-магазина, список товаров приходит из данных, и разметки под него заранее никто не пишет. Элементы нужно создавать из кода. Это три шага:

  1. Создали — document.createElement("li"). Элемент появился «на складе»: он существует, но на странице его пока не видно.
  2. Настроили — задали текст, классы, атрибуты: li.textContent = "Самбуса", li.classList.add("item").
  3. Вставили — parent.append(li). Только после этого он появляется на странице.
Аналогия из жизни

Бозор. Продавец достаёт товар из мешка (createElement), вешает ценник (настройка) и только потом выкладывает на прилавок (append). Пока товар в мешке, покупатели его не видят. Самая частая ошибка новичка — сделать два первых шага и забыть про прилавок.

Создали, настроили, вставили · Ctrl + Enter
<h2>Сегодня в меню</h2>
<ul id="menu"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Куда вставлять

МетодКуда попадёт элемент
parent.append(el)в конец родителя
parent.prepend(el)в начало родителя
ref.before(el)перед элементом ref, на одном уровне с ним
ref.after(el)после элемента ref
el.remove()удаляет элемент со страницы
el.cloneNode(true)создаёт копию (с содержимым, если true)
parent.replaceChildren()удаляет всех детей. С аргументами — заменяет детей на переданные

append умеет принимать несколько элементов сразу: card.append(title, price). Если передать строку, она вставится как текст (не как HTML), так что это безопасно.

Очередь: вставляем и удаляем · Ctrl + Enter
<ol id="queue">
  <li>Рустам</li>
  <li class="gone">Мадина</li>
  <li>Фарход</li>
</ol>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Элемент не копируется, а переезжает

Один и тот же элемент не может быть в двух местах дерева. Если вставить уже существующий элемент в другое место, он переедет, а на старом месте исчезнет. Нужна копия — делайте cloneNode(true).

Можно редактировать · Ctrl + Enter
const a = document.createElement("ul");
const b = document.createElement("ul");

const li = document.createElement("li");
li.textContent = "Самбуса";
a.append(li);

b.append(li);                        // переехал из a в b
console.log(a.children.length, b.children.length);  // → 0 1

a.append(li.cloneNode(true));        // копия остаётся и там, и там
console.log(a.children.length, b.children.length);  // → 1 1

createElement или innerHTML?

createElement + appendinnerHTML со строкой
Записываетсядлиннеекороче, похоже на настоящий HTML
Безопасностьтекст всегда через textContent — безопасноопасно, если в строке чужой текст
Старые ссылки на элементысохраняютсяломаются при += и перезаписи
Когда выбиратьданные от пользователя и API, когда элемент нужен в переменнойстатичный шаблон, который вы написали сами

В этом уроке мы идём по безопасному пути: createElement. Он чуть длиннее, но учит видеть структуру страницы как дерево — а это именно то, что потом пригодится в React.

Частая ошибка: забыли вставить

Создать элемент — не значит показать его. Без append (или prepend, before, after) он остаётся «в мешке». Если на странице ничего не появилось, а ошибок нет, — первым делом проверьте, что вставка действительно вызывается.

Читать подробнее: learn.javascript.ru — Изменение документа · MDN — createElement · MDN — replaceChildren

Практика: закрепляем

Создаём, вставляем, переставляем, удаляем.

ЗАДАЧА 5.1

Первый элемент

лёгкая

Создайте <li> с текстом Самбуса и вставьте его в список #menu. Выведите количество детей списка и его текст.

Пустое меню · Ctrl + Enter
<ul id="menu"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
1
Самбуса
Подсказка 1

Три шага: createElement("li"), затем textContent, затем menu.append(...).

Подсказка 2

const li = document.createElement("li"); li.textContent = "Самбуса"; menu.append(li);

ЗАДАЧА 5.2

Три пункта циклом

лёгкая

Для каждого блюда из массива создайте <li> с классом item и вставьте в #menu. Выведите количество пунктов, текст последнего и класс первого.

Меню циклом · Ctrl + Enter
<ul id="menu"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
3
Чой
item
Подсказка 1

Тело цикла for (const dish of dishes) { ... } содержит те же три шага, что и в задаче 5.1, плюс li.classList.add("item").

Подсказка 2

Создание нового li должно быть внутри цикла — на каждый пункт свой элемент.

ЗАДАЧА 5.3

Очередь в банке

средняя

Выполните четыре операции с очередью: 1) удалите того, у кого класс old; 2) поставьте Зарина в самое начало; 3) поставьте Далер сразу после Алии; 4) добавьте Нилуфар в конец. Вспомогательная функция make уже есть.

Очередь · Ctrl + Enter
<ul id="line">
  <li>Алия</li>
  <li class="old">Бахром</li>
  <li>Умар</li>
</ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
Зарина, Алия, Далер, Умар, Нилуфар
Подсказка 1

Первая операция: найдите .old через querySelector и вызовите у него remove(). «После Алии» — это метод у самой alia: alia.after(...).

Подсказка 2

line.querySelector(".old").remove(); line.prepend(make("Зарина")); alia.after(make("Далер")); line.append(make("Нилуфар"));

ЗАДАЧА 5.4

Переехал или скопировался

сложная

Сначала предскажите два числа в первых двух строках вывода — сколько элементов будет в левом и правом списках после каждой операции. Потом запустите. Затем допишите код: клон перед вставкой должен получить текст Курутоб (копия) (к тексту клона допишите " (копия)"), а в конце выведите текст последнего элемента правого списка.

Два списка · Ctrl + Enter
<ul id="left">
  <li>Самбуса</li>
  <li>Курутоб</li>
  <li>Шакароб</li>
</ul>
<ul id="right"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
2 1
2 2
Курутоб (копия)
Подсказка 1

Первый append переносит «Самбусу» — слева остаётся 2, справа 1. Второй вставляет копию — слева по-прежнему 2, справа 2.

Подсказка 2

clone.textContent += " (копия)"; до вставки, а в конце — console.log(right.lastElementChild.textContent);

06 Рендер: из массива в карточки

Теперь соединим всё, что вы знаете. В уроках 6 и 7 вы работали с массивами объектов — каталогами товаров, студентов, заказов. Теперь эти данные можно нарисовать. Слово рендер (render) как раз и означает «из данных построить то, что видно на экране». Схема всегда одна и та же:

  1. Данные — массив объектов: const products = [{ title: "Нон", price: 4 }, …]. Это главный источник правды.
  2. Функция-фабрика createCard(product) — получает один объект и возвращает готовый элемент.
  3. Функция рендера render(items) — очищает контейнер и вставляет в него карточки для всего массива.
  4. Вызов render(products) — страница нарисована. Изменили массив — вызвали render снова.
Аналогия из жизни

Оши палав на свадьбе. Рецепт и список гостей — это данные. Повар — функция-фабрика: на входе рис и мясо, на выходе одна порция. А официант — render: проходит по всему списку гостей, ставит порцию перед каждым. Новый гость пришёл — не нужно менять рецепт, просто ещё одна порция по тому же рецепту.

Рендер каталога + кнопка очистки · Ctrl + Enter
<button id="clear">Очистить</button>
<div id="catalog" class="catalog"></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Запустите, а затем нажмите кнопку «Очистить» в preview — карточки исчезнут. Измените цену или добавьте в массив ещё товар, запустите снова: страница перерисуется сама. Меняем данные — страница следует за ними.

Разбираем render по частям

items.map(createCard)
map передаёт каждый товар в createCard и собирает возвращённые элементы в новый массив.
...cards
Оператор spread из прошлого урока: «высыпает» массив элементов в аргументы replaceChildren(card1, card2, …).
replaceChildren(…)
Сначала убирает всех старых детей, потом вставляет новых. Поэтому повторный render не дублирует карточки.
replaceChildren()
Без аргументов — просто очищает контейнер. Это и есть «кнопка очистки».
Кнопка пока на «честном слове»

Строка clearBtn.onclick = … — первый взгляд на события. Пока запомните её как «когда нажмут на кнопку — сделай это». Полностью события (addEventListener, объект события, делегирование) мы разберём в следующем уроке. Для домашнего задания этой строки достаточно.

Пустое состояние

Что видит пользователь, если по его фильтру ничего не нашлось? Пустую белую страницу — как будто сайт сломался. Хороший рендер всегда обрабатывает пустой массив:

render с пустым состоянием
function render(items) {
  if (items.length === 0) {
    const empty = document.createElement("p");
    empty.className = "empty";
    empty.textContent = "Ничего не найдено";
    catalog.replaceChildren(empty);
    return;                              // ранний выход — как в уроке про функции
  }
  catalog.replaceChildren(...items.map(createCard));
}
Две частые ошибки рендера

1. Вставлять карточки, не очистив контейнер: каждый новый render будет дописывать товары к старым, и список будет расти. Используйте replaceChildren.

2. Создавать элемент вне функции или вне цикла: const card = document.createElement(…) один раз и затем много раз append(card). Элемент один — значит, он будет переезжать, и в итоге на странице окажется одна карточка (помните раздел 5).

Тот же рендер строкой

Короткий вариант с шаблонной строкой тоже допустим, если данные ваши и безопасные. Сравните — результат одинаковый:

Вариант со строкой (только для своих, безопасных данных)
function render(items) {
  catalog.innerHTML = items
    .map(p => `<article class="card"><h3>${p.title}</h3><p class="price">${p.price} сомони</p></article>`)
    .join("");
}

Если же title может прийти от пользователя или с чужого сервера — только createElement и textContent.

Читать подробнее: learn.javascript.ru — Изменение документа · MDN — replaceChildren

Практика: закрепляем

Четыре задачи по нарастающей: от одной функции-фабрики до группировки по категориям.

ЗАДАЧА 6.1

Фабрика пунктов

лёгкая

Допишите функцию createItem(product): она создаёт <li> с текстом вида Нон — 4 сомони и возвращает его. Рендер в конце уже написан.

Список покупок · Ctrl + Enter
<ul id="list"></ul>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
4
Нон — 4 сомони
Подсказка 1

Функция должна заканчиваться return li; — иначе map соберёт массив из undefined.

Подсказка 2

li.textContent = `${product.title} — ${product.price} сомони`;

ЗАДАЧА 6.2

Пустое состояние

средняя

Напишите функцию render(items): она очищает #catalog; если массив пуст — вставляет абзац <p class="empty">Ничего не найдено</p>; иначе — по одному <div class="card"> с названием товара. Проверьте на трёх наборах данных.

Каталог с пустым состоянием · Ctrl + Enter
<div id="catalog"></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
3
2
1
Ничего не найдено
Подсказка 1

Начните функцию с проверки if (items.length === 0) и ранним return. Остальной код будет только для непустого массива.

Подсказка 2

В обеих ветках используйте catalog.replaceChildren(...): в пустой — с одним абзацем, в обычной — с массивом карточек через items.map.

ЗАДАЧА 6.3

Карточка с отметкой «нет в наличии»

средняя

Допишите createCard({ title, price, inStock }): внутри <article class="card"> лежат <h3> с названием и <p class="price"> с текстом 60 сомони. Если товара нет в наличии, карточке нужен ещё класс out. Деструктуризация в параметрах — как на уроке про объекты.

Каталог с наличием · Ctrl + Enter
<div id="catalog"></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
4
1
60 сомони
Подсказка 1

Схема та же, что в примере выше: card, h3, p.price, затем card.append(h3, p) и return card.

Подсказка 2

if (!inStock) card.classList.add("out"); — до return.

ЗАДАЧА 6.4

Группировка по категориям

сложная

Покажите товары по разделам: для каждой категории создайте <section> с заголовком <h3> (название категории) и списком <ul>, где каждый товар — <li>. Сначала сгруппируйте массив через reduce в объект «категория → массив названий», потом постройте DOM из Object.entries(groups). Порядок разделов — порядок первого появления категории.

Каталог по разделам · Ctrl + Enter
<div id="catalog"></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
3
Продукты: 2
Кухня: 1
Сувениры: 2
Подсказка 1

Группировка: products.reduce((acc, p) => { if (!acc[p.category]) acc[p.category] = []; acc[p.category].push(p.title); return acc; }, {}). Начальное значение — пустой объект.

Подсказка 2

Цикл for (const [category, titles] of Object.entries(groups)). Внутри создайте section, h3 (текст — category), ul; для каждого названия из titles — li. Собранный section — catalog.append(section).

07 Разделение труда: ИИ пишет вёрстку, вы — логику

DOM — идеальное место, чтобы научиться работать с ИИ правильно. Разметка и CSS — это объёмная, но однообразная работа: ИИ делает её быстро и обычно хорошо. А вот логика — какие данные, в каком порядке, что сделать при пустом списке — это то, что вы должны понимать до последней строки. Поэтому в нашем курсе правило такое:

Правило урока 8

ИИ пишет HTML и CSS. JavaScript-логику вы пишете сами. Сначала 15 минут пробуете сами, и только потом идёте с промптом к ИИ. Не вставляйте код, который не можете объяснить строчка за строчкой.

Отдаём ИИДелаем сами
Разметка карточки, формы, шапкиСтруктура данных (какие поля у товара)
CSS: сетка, цвета, отступы, адаптивностьФункции createCard и render
Тестовые данные (массив из 10 товаров)Проверка: данные правдоподобны, типы верные
Объяснение незнакомого методаРешение, какой метод и когда применить

Мини-справка: BEM — имена классов без хаоса

Когда ИИ делает вёрстку, просите именовать классы по BEM. Это договорённость об именах, чтобы по классу сразу было видно, что это такое:

Блок
Самостоятельный компонент: product-card.
Элемент
Часть блока, через два подчёркивания: product-card__title, product-card__price.
Модификатор
Вариант блока, через два дефиса: product-card--out («нет в наличии»). В JS его включают и выключают через classList — тот самый разговор про бейджи из раздела 4.

Промпт на HTML-шаблон

Сравните два запроса к ИИ. Первый — как делают новички. Второй — по формуле курса: контекст → задача → ограничения → формат ответа → проверка.

сделай карточку товара
Контекст: я учу JavaScript, делаю учебный интернет-магазин на чистом HTML/CSS/JS без библиотек. Мне нужна только вёрстка — JavaScript я напишу сам. Задача: сделай HTML-шаблон одной карточки товара (название, цена, кнопка «В корзину») и CSS к нему, плюс CSS для адаптивной сетки таких карточек (grid). Ограничения: классы по BEM (product-card, product-card__title, product-card__price, product-card__btn, модификатор product-card--out для товара, которого нет в наличии). Без JavaScript, без inline-стилей, без onclick, без внешних картинок и шрифтов. Светлая тема, скруглённые углы. Формат ответа: два блока кода — HTML и CSS. После них — список классов и что каждый значит, в три-пять строк. Проверка: напиши, какие элементы шаблона мне придётся заполнять из JavaScript.

Что получает ИИ из второго промпта? Стек, уровень, границы («JavaScript не пиши»), конкретные имена классов, формат ответа и просьбу самому проверить себя. А вам остаётся логика — и вы точно знаете, на какие классы она опирается.

Что ИИ мог ответить — и как это превратить в код

Допустим, ИИ вернул такую разметку одной карточки:

Ответ ИИ: HTML
<article class="product-card">
  <h3 class="product-card__title">Название товара</h3>
  <p class="product-card__price">0 сомони</p>
  <button class="product-card__btn">В корзину</button>
</article>

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

В шаблоне ИИВ вашей функции createCard
<article class="product-card">createElement("article") + className = "product-card"
<h3 class="product-card__title">Название</h3>createElement("h3"), класс, textContent = product.title
<p class="product-card__price">createElement("p"), класс, textContent = product.price + " сомони"
<button class="product-card__btn">createElement("button"), класс, textContent = "В корзину"
вложенность: всё внутри articlecard.append(title, price, btn)
Шаблон от ИИ + ваша createCard · Ctrl + Enter
<div id="catalog" class="catalog"></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Проверяем ответ ИИ: чек-лист

  • Есть ли в ответе <script>, onclick="…", inline-стили style="…"? Вы просили без них — значит, убираем.
  • Совпадают ли имена классов с теми, что вы будете использовать в JS? Одна лишняя буква — и querySelector вернёт null.
  • Подходящие ли теги: article для карточки, h3 для названия, button для кнопки (а не div с кликом)?
  • Нет ли внешних картинок и шрифтов, которые не загрузятся?
  • Если ИИ предложил innerHTML со вставкой данных — спросите, безопасно ли это, и перепишите на textContent.
  • Не придумал ли ИИ несуществующих методов? Проверьте незнакомое в MDN.
Не просите «напиши весь рендер»

Если попросить ИИ написать createCard и render, он напишет рабочий код за пять секунд — и вы ничему не научитесь. На проверке ДЗ преподаватель может попросить объяснить любую строку. Хороший способ использовать ИИ для логики: сначала написать свой вариант, а потом попросить «объясни построчно, что делает мой код, и назови 2 места, где он сломается».

Вот моя функция createCard: [код]. Объясни построчно, что она делает. Ничего не переписывай. В конце назови два крайних случая, на которых она сломается (например, нет поля price).

Практика с ИИ

Одна задача на проверку чужого кода, одна — на написание своего промпта.

ЗАДАЧА 7.1

Проверьте код от ИИ

средняя

Вы попросили ИИ нарисовать каталог. Он вернул код ниже. В нём три ошибки. Требования: на странице должно быть две карточки; в каждой — заголовок с названием и абзац .price с текстом вида 450 сомони. Найдите и исправьте ошибки по одной, перезапуская код. Сначала прочитайте ошибку в консоли.

Код от ИИ (с ошибками) · Ctrl + Enter
<div id="catalog" class="catalog"></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
2
Чапан
450 сомони
Подсказка 1

Первая ошибка видна сразу: в консоли null. Сравните селектор с классом контейнера в HTML. Вторая: вспомните, что происходит, когда один и тот же элемент вставляют несколько раз. Третья — в тексте цены: чего не хватает по требованиям?

Подсказка 2

Селектор должен быть ".catalog". Создание card нужно перенести внутрь forEach, чтобы на каждый товар был свой элемент. В шаблонной строке цены допишите сомони после ${p.price}.

ЗАДАЧА 7.2

Составьте свой промпт

лёгкая

Составьте по формуле курса промпт для ИИ: HTML и CSS карточки курса MakeFirstStep (название, длительность, цена, кнопка «Записаться»). Обязательно: BEM, без JavaScript, без inline-стилей, ответ — два блока кода. Отправьте его ИИ, проверьте ответ по чек-листу выше и сохраните промпт в файл prompt.txt — он пригодится для домашнего задания.

Для этой задачи нет автопроверки: отметьте её выполненной, когда промпт составлен, а ответ проверен по всем пунктам чек-листа.

Подсказка 1

Пройдите по формуле: контекст (проект, уровень), задача (одна), ограничения (BEM, без JS), формат (два блока кода), проверка (что заполнять из JS).

Подсказка 2

Возьмите промпт из этого раздела и замените товар на курс: поля title, duration, price, кнопка «Записаться».

08 Практика на уроке: собираем каталог карточек

Время собрать всё вместе. Цель: страница магазина, на которой 10 карточек рисуются из массива, а кнопка «Очистить» убирает их все. Вёрстку и стили мы получили от ИИ (они уже лежат в песочницах ниже), логику пишете вы — шаг за шагом.

  1. Данные. Заведите массив из 10 объектов: title, price, category, inStock, rating. В реальной жизни такие данные можно попросить у ИИ (урок 7) — но проверить 2–3 объекта на глаз всё равно нужно.
    JavaScript
    const products = [
      { title: "Самбуса", price: 5, category: "Еда", inStock: true, rating: 4.9 },
      // ... ещё 9 товаров
    ];
  2. Найти контейнер и кнопку — один раз, в начале скрипта.
    JavaScript
    const catalog = document.querySelector("#catalog");
    const clearBtn = document.querySelector("#clear");
  3. Фабрика карточки. Функция createCard(product): создаёт article, h3, p, настраивает классы по BEM и текст через textContent, собирает через append и возвращает карточку. Для товара не в наличии — модификатор product-card--out.
  4. Рендер. Функция render(items): catalog.replaceChildren(...items.map(createCard)).
  5. Вызов. render(products) — и посмотрите на страницу.
  6. Кнопка очистки. clearBtn.onclick = () => catalog.replaceChildren();
  7. Проверка: вывод в консоль количества карточек — до очистки, после и после повторного рендера.
Как идти по шагам

После каждого шага нажимайте ▶ Запустить и смотрите на preview. Не пишите всё сразу: если на шаге 3 карточка не появилась, вы точно знаете, где искать. Это тот же приём, что и в «Разделении труда» раздела 7: маленькие шаги, проверка после каждого.

Практика: магазин «Бозор»

Две задачи: сначала страница целиком, потом улучшения. В песочнице уже есть вёрстка и CSS от ИИ; данные тоже готовы.

ЗАДАЧА 8.1

Каталог из 10 карточек и кнопка очистки

средняя

Допишите createCard, render и кнопку очистки по шагам выше. Проверочный код внизу сам «нажмёт» кнопку методом click(). Он выведет: сколько карточек стало, сколько из них «нет в наличии», сколько после очистки и сколько после повторного рендера.

Магазин «Бозор»: каталог · Ctrl + Enter
<header class="shop__head">
  <h2 class="shop__title">Магазин «Бозор»</h2>
  <button id="clear" class="shop__btn">Очистить</button>
</header>
<div id="catalog" class="catalog"></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
10
2
0
10
Подсказка 1

Если на странице пусто, а ошибок нет — проверьте, что createCard заканчивается return card;, а render действительно вставляет результат в catalog.

Подсказка 2

render — одна строка: catalog.replaceChildren(...items.map(createCard)); Кнопка: clearBtn.onclick = () => catalog.replaceChildren();

ЗАДАЧА 8.2

Хиты и счётчик

сложная

Каталог уже работает. Улучшите его: 1) товарам с рейтингом 4.8 и выше добавьте класс product-card--hit и значок <span class="product-card__badge">Хит</span> внутрь карточки; 2) после каждого render показывайте в #stats строку вида Хитов: 3 из 10 (общее число — это items.length, хиты считайте по массиву items). Значок вставляйте только через createElement.

Магазин «Бозор»: хиты · Ctrl + Enter
<header class="shop__head">
  <h2 class="shop__title">Магазин «Бозор»</h2>
  <button id="clear" class="shop__btn">Очистить</button>
</header>
<p id="stats" class="shop__stats"></p>
<div id="catalog" class="catalog"></div>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
3
Хитов: 3 из 10
Подсказка 1

Условие product.rating >= 4.8 внутри createCard: добавьте класс через classList, создайте span, задайте класс и текст и вставьте его в карточку через card.append(...).

Подсказка 2

Счётчик: const hits = items.filter(p => p.rating >= 4.8).length; и stats.textContent = `Хитов: ${hits} из ${items.length}`;

09 Домашнее задание

Тема: «Страница, собранная из JS». Соберите свою страницу-каталог: весь контент появляется из массива, а в HTML-файле нет ни одной карточки.

Что нужно сделать

  • ☐ Данные. Массив минимум из 10 объектов на тему на ваш выбор: магазин, меню чайханы, курсы MakeFirstStep, маршрутки Душанбе, фильмы. У каждого объекта минимум 4 поля, среди которых есть title и логическое поле (например, inStock).
  • ☐ Вёрстка от ИИ. HTML-шаблон и CSS получены по промпту по формуле курса. Промпт сохранён в файле prompt.txt. Классы — по BEM.
  • ☐ createCard(item). Отдельная функция, которая через createElement собирает карточку и возвращает её. Весь текст — через textContent.
  • ☐ render(items). Отдельная функция: очищает контейнер через replaceChildren и вставляет карточки. Для пустого массива показывает «Ничего не найдено».
  • ☐ Кнопка очистки. Кнопка «Очистить» убирает все карточки (через onclick, как в уроке). Бонус: кнопка «Показать всё» возвращает их.
  • ☐ Счётчик. Текст «Показано: N» обновляется внутри render.
  • ☐ Модификатор. Карточки, для которых логическое поле ложно, получают дополнительный класс (--out, --sold и т. п.) через classList, и CSS их выделяет.
  • ☐ Бонус. Вызовите render с отфильтрованным массивом (например, только дешёвые) — просто строкой в коде, без кнопок. Кнопки-фильтры будут на следующем уроке.
Правила работы с ИИ в этом ДЗ

ИИ можно использовать для вёрстки, CSS и тестовых данных. Логику (createCard, render, очистку, счётчик) пишете сами. На проверке преподаватель может попросить объяснить любую строку вашего JS. Если не можете — оценка снижается.

Что сдавать

  • Три файла: index.html, style.css, script.js и файл prompt.txt с промптом, по которому ИИ сделал вёрстку.
  • Скриншот страницы с 10 карточками и скриншот после нажатия «Очистить».
  • Два-три предложения в чате группы: что оказалось самым сложным и что вы исправили после проверки кода ИИ.

Как оценим (0–10)

КритерийБаллы
Не менее 10 карточек рисуются из массива, в HTML их нет3
createCard и render — отдельные функции, render вызывается повторно без дублей1
Кнопка очистки работает1
Счётчик и пустое состояние1
Модификатор класса через classList и стиль для него1
Безопасная вставка: данные через textContent, а не innerHTML1
Чистый код: осмысленные имена, нет копипасты, найденные элементы в переменных1
Промпт по формуле в prompt.txt и умение объяснить код1
Совет, с чего начать

Сначала вручную напишите в HTML одну карточку и добейтесь, что она красивая. Потом удалите её из HTML и воспроизведите в createCard — так вы точно знаете, какая структура нужна. Используйте console.log после каждого шага и вкладку Elements в DevTools, чтобы увидеть, что реально построил ваш код.

✓ Шпаргалка урока

  • DOM — живое дерево объектов, которое браузер строит из HTML. JavaScript работает с ним через document. Файл HTML при этом не меняется.
  • Навигация: parentElement, children, firstElementChild, lastElementChild, nextElementSibling, previousElementSibling. childNodes считает ещё и пробелы.
  • Поиск: querySelector(sel) — первый элемент или null; querySelectorAll(sel) — NodeList (для map — Array.from). Искать можно внутри элемента: card.querySelector(".price").
  • Текст и разметка: textContent — безопасный текст; innerHTML — разметка, только для своих данных. Чужой текст — всегда textContent. += для innerHTML ломает старые ссылки.
  • Классы: classList.add / remove / toggle / contains. className = затирает всё. Дизайн — в CSS-классах, а не в el.style.
  • Атрибуты: getAttribute / setAttribute / removeAttribute / hasAttribute; свойства value, disabled, hidden; свои данные — data-* и el.dataset.name (всегда строка).
  • Создание: createElement → настроить → append / prepend / before / after. Удаление — el.remove(), очистка — parent.replaceChildren(), копия — cloneNode(true). Элемент не копируется, а переезжает.
  • Рендер: данные (массив) → createCard(item) возвращает элемент → render(items) делает replaceChildren(...items.map(createCard)). Всегда обрабатывайте пустой массив.
  • С ИИ: ИИ пишет HTML и CSS (BEM, без JS и inline-стилей), вы — логику. Проверяйте имена классов, безопасность вставки и выдуманные методы. Не вставляйте код, который не можете объяснить.