JS · Урок 9

DOM и события

Финальный аккорд: JavaScript управляет страницей — находит элементы, меняет их и реагирует на действия пользователя.

Что такое DOM

DOM (Document Object Model) — древовидная модель документа. Браузер превращает HTML в дерево объектов, а JavaScript умеет ходить по этому дереву, читать и менять его. Любая «живая» страница — это работа с DOM.

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

const h1 = document.querySelector("h1");      // первый совпавший
const cards = document.querySelectorAll(".card"); // все
const btn = document.getElementById("save");    // по id

console.log(h1.textContent);   // читаем текст

Изменение элементов

const h1 = document.querySelector("h1");
h1.textContent = "Новый заголовок";

const card = document.querySelector(".card");
card.style.backgroundColor = "#1e293b";
card.classList.add("active");   // или remove / toggle
ℹ️
textContent — безопасный текст, innerHTML подставляет HTML-код. Используйте innerHTML аккуратно: если подставляете пользовательский ввод, есть риск XSS-атак.

События

Событие — сигнал «что-то произошло». Слушаем клики:

const btn = document.querySelector("#likeBtn");

btn.addEventListener("click", () => {
  likes++;
  document.querySelector("#likeCount").textContent = likes;
});

Популярные события: click, input, submit, keydown, scroll, mouseover.

Живой пример: счётчик кликов

Полный интерактив — тот же код, что и выше, уже живёт здесь:

♥
Кликов: 0

Создание элементов

Можно строить элементы программно и добавлять их в документ:

const li = document.createElement("li");
li.textContent = "Новый пункт";
list.appendChild(li);

list.innerHTML = "";  // очистить список

Финальный мини-проект

Соберите всё вместе — маленький «todo-список»:

  1. Поле ввода и кнопка «Добавить».
  2. По клику — создать элемент <li> и добавить в список.
  3. Клик по элементу — перечёркивать его (класс .done).
  4. Использовать event.preventDefault() на форме, чтобы страница не перезагружалась.
const input = document.querySelector("#task");
const list = document.querySelector("#list");

form.addEventListener("submit", (e) => {
  e.preventDefault();                     // без перезагрузки
  const li = document.createElement("li");
  li.textContent = input.value;
  li.addEventListener("click", () => li.classList.toggle("done"));
  list.appendChild(li);
  input.value = "";
});

Путешествие по дереву DOM

DOM — дерево, поэтому от любого элемента можно гулять к соседям: родителю, детям, братьям и сёстрам:

const el = document.querySelector(".card");

el.parentElement;                 // родитель
el.children;                      // массив прямых детей
el.firstElementChild;             // первый ребёнок
el.lastElementChild;              // последний ребёнок
el.nextElementSibling;            // следующий сосед
el.previousElementSibling;        // предыдущий сосед
💡
Частая задача «подписать все карточки одним ценником» решается через parentElement: нашли кнопку, у неё поднялись к карточке — и уже её правим.

Клавиатура и ввод: событие input

Событие input срабатывает на каждое изменение текста, а keydown ловит нажатия клавиш:

const search = document.querySelector("#search");
const hint = document.querySelector("#count");

search.addEventListener("input", () => {
  hint.textContent = search.value.length + " симв.";
});

document.addEventListener("keydown", (e) => {
  if (e.key === "Escape") closeModal();   // ESC закрывает окно
});
ℹ️
Свойство e.key — имя клавиши («Enter», «Escape», «ArrowUp»). Так делают «живой счётчик символов», перенос фокуса и горячие клавиши.

Делегирование событий

Один слушатель на родителя вместо сотни на детей — событие «всплывает» вверх, и мы ловим его один раз:

document.querySelector(".todo-list").addEventListener("click", (e) => {
  const item = e.target.closest(".todo-item");
  if (item) item.classList.toggle("done");
});
💡
Делегирование работает даже для элементов, добавленных в список уже ПОСЛЕ создания слушателя. Огромная экономия памяти и кода на динамических списках.

localStorage — память браузера

Данные можно сохранить между визитами без сервера. Внутри хранятся только строки, поэтому объекты сериализуем через JSON:

const theme = { dark: true };

localStorage.setItem("theme", JSON.stringify(theme));   // сохранить
const saved = JSON.parse(localStorage.getItem("theme")); // прочитать
localStorage.removeItem("theme");                        // удалить
⚠️
localStorage не безграничен (~5 МБ на домен) и хранит всё открытым текстом. Для секретов и токенов он не годится, но для настроек интерфейса — идеален.

classList и data-атрибуты

Классами управляют через classList, а свои данные читают через dataset — модернизированный способ работы с data-*:

const box = document.querySelector(".box");

box.classList.add("active");       // добавить
box.classList.remove("active");    // убрать
box.classList.toggle("active");    // переключить
box.classList.contains("active");  // проверить

<div data-id="42">...</div>
box.dataset.id;   // "42" — data-id превращается в dataset.id
💡
«Переключить из папки» — излюбленный приём взаимодействия: например, каждый клик по тегу переключает его .selected, не меняя остальных.

Таймеры: setTimeout и setInterval

JS умеет откладывать запуск кода во времени — без сервера и без блокировки страницы:

const timer = setTimeout(() => {
  console.log("через 3 секунды");
}, 3000);
clearTimeout(timer);   // отменить, если передумали

const tick = setInterval(() => {
  seconds++;
}, 1000);          // каждую секунду
clearInterval(tick);   // остановить
⚠️
Интервалы не забывайте останавливать — иначе таймер продолжит жить даже после закрытия «модалки» и будет есть память.

Стили и атрибуты через JS

Кроме классов, в DOM можно править стили и атрибуты напрямую — иногда это проще:

const el = document.querySelector(".card");

el.style.width = "300px";            // инлайн-стиль
el.setAttribute("title", "Помощь");
el.removeAttribute("disabled");
el.getAttribute("data-tooltip");      // просто прочитать
ℹ️
Изменение ширины/прозрачности через style используют в анимациях. Для логики — лучше классы: стили остаются «в одном месте» и не размазаны по JS.

fetch: получение данных с сервера

Браузерный fetch запрашивает данные по API. Ответ обрабатывается цепочкой .then() — она приходит не сразу, поэтому код «продолжается» по готовности:

fetch("https://api.example.com/users")
  .then(res => res.json())
  .then(users => {
    displayUsers(users);
  })
  .catch(err => {
    console.error("Сеть упала:", err);
  });
💡
В современных проектах то же самое пишут через async/await — тоньше и ближе к обычному коду. Но с цепочками .then() начинают такой стиль понимать.

Несколько обработчиков на одном элементе

addEventListener не заменяет старый обработчик — он добавляет ещё один:

const btn = document.querySelector("#green");

btn.addEventListener("click", () => console.log("1-й слушатель"));
btn.addEventListener("click", () => console.log("2-й слушатель"));
// сработают оба, по порядку

once и removeEventListener

Реакция один раз или отписка, когда обработчик больше не нужен:

btn.addEventListener("click", handler, { once: true });

// или снятие вручную (handler должна быть та же функция):
btn.removeEventListener("click", handler);

submit и preventDefault

Форма при отправке сама перезагружает страницу. Обычно мешает:

form.addEventListener("submit", (e) => {
  e.preventDefault();        // отменяем перезагрузку
  console.log("Отправлено без перезагрузки");
});

Остановка всплытия: stopPropagation

События «всплывают» от кликнутого элемента к предкам — это иногда мешает:

btn.addEventListener("click", (e) => {
  e.stopPropagation();        // предки не узнают о клике
  e.preventDefault();         // и браузерного действия не будет
});
⚠️
Нужно АККУРАТНО: остановка всплытия ломает глобальные обработчики (клик вне, аналитику). Начните с preventDefault и продумайте, зачем глушить событие.

Клавиатура: keydown, keyup

Горячие клавиши и поля проверяем по ключу кода:

document.addEventListener("keydown", (e) => {
  if (e.key === "Escape") closeModal();
  if (e.code === "Enter") send();
});
ℹ️
e.key — символ («Enter», «я»), e.code — физическая клавиша (KeyW). Физику используйте для раскладок, символ для игр по букве.

getBoundingClientRect: координаты

Точное положение элемента относительно окна — для всплывашек и анимаций:

const btnRect = btn.getBoundingClientRect();

btnRect.top;      // от верхней кромки окна
btnRect.left;
btnRect.width;    // ширина с учётом масштаба
💡
После прокрутки координаты меняются. Переспрашивайте в scroll/resize, а не храните.

insertAdjacentHTML: вставка куском

Рядом с элементом или вовнутрь можно вставить готовую HTML-строку:

const box = document.querySelector(".box");

box.insertAdjacentHTML("beforeend", "<span>конец списка</span>");
box.insertAdjacentHTML("afterbegin", "<span>начало</span>");
box.insertAdjacentHTML("beforebegin", "<p>перед блоком</p>");
⚠️
Строка разбирается браузером: НЕ вставляйте так пользовательский ввод. Для безопасных файлов — textContent или createElement.

Удаление узлов

Убрать элемент из документа проще простого:

const card = document.querySelector(".card");
card.remove();                 // сам себя

parent.removeChild(card);      // удаляет указанный ребёнок

closest: подъём по дереву

Ищем ближайшего предка с селектором (включая сам элемент):

const input = document.querySelector("input");
const form2 = input.closest("form");   // форма, если есть
const box   = input.closest(".box");

FormData: форма в объект

Собрать все поля формы разом:

form.addEventListener("submit", (e) => {
  e.preventDefault();
  const data = new FormData(form);
  console.log(data.get("name"));   // значение поля name
  console.log(Object.fromEntries(data));
});

scroll: событие и позиция

Следить за прокруткой, знать, насколько ушли вниз:

window.addEventListener("scroll", () => {
  const scrolled = window.scrollY;        // сколько пикселей после верха
  if (scrolled > 200) header.classList.add("compact");
});

resize: размеры окна

Ловим повороты и ресайзы — например, переключать режим:

window.addEventListener("resize", () => {
  const wide = window.innerWidth > 768;
  document.body.classList.toggle("desktop", wide);
});
💡
Событие resize шумит при каждой строке изменения. Для вычислений — throttle/debounce (не более раза в 100–200 мс).

Живые и статические коллекции

querySelectorAll — статичный список, getElementsByClassName — живой:

const items1 = document.querySelectorAll("li");  // статическая
const items2 = document.getElementsByTagName("li"); // живая

list.append(document.createElement("li"));
items1.length;   // не изменилось
items2.length;   // изменилось — она «живая»

Инлайн-стили: style и setProperty

Точные значения пишутся прямо в style — например, анимации и метрики:

el.style.transform = "translateX(40px)";
el.style.width     = `${w}px`;

// CSS-переменная — через setProperty:
el.style.setProperty("--gap", "12px");
⚠️
Инлайн-стили — разовые значения. Если состояние повторяется и живёт в CSS — используйте классы и classList.

Что дальше

Вы прошли весь курс! Теперь стоит идти к большой практике:

🎉
Поздравляем с финалом курса! Лучший способ закрепить — сделать собственный сайт-портфолио. Удачи!

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

Браузер превращает HTML в «дерево» объектов — DOM: каждый тег — узел, каждый узел — объект с родителем и детьми. Скрипт «поднимается» и «спускается» по этим веткам.

<body>
  <h1>Заголовок</h1>
  <p>Абзац</p>
</body>
// дерево: body → (h1, p)
💡
В DevTools вкладка Elements — то же дерево: разворачивайте узлы и смотрите «ветки».

Выбор одного: querySelector

document.querySelector("селектор") возвращает ПЕРВЫЙ подходящий элемент. Селектор — как в CSS: тег, класс, id, комбинация.

const title = document.querySelector("h1");
const btn = document.querySelector(".btn");
console.log(title.textContent);
💡
Не нашли — вернётся null: проверяйте «существует ли», прежде чем трогать.

Выбор многих: querySelectorAll

document.querySelectorAll(".item") — список ВСЕХ совпадений (NodeList). Его можно «обойти» циклом или forEach, но есть и подвох (живые коллекции — в конце курса).

const items = document.querySelectorAll(".item");
for (const el of items) {
  el.classList.add("highlight");
}
💡
NodeList — «почти массив»: for...of и forEach работают; методы map — уже нет.

Текст элемента: textContent

el.textContent читает/пишет весь текст внутри, включая «вложенные» узлы. Замена «как строка» — главный способ «перерисовать» тексты.

const el = document.querySelector("#title");
el.textContent = "Новый заголовок";
💡
textContent безопасен: никогда не «выполняет» HTML — в отличие от innerHTML, которому доверять не стоит.

Переключение класса: classList.toggle

el.classList.toggle("open") — добавить класс, если его нет, и убрать, если есть. Класс «open» в CSS делает остальное: панель, аккордеон, меню.

menuBtn.addEventListener("click", () => {
  menu.classList.toggle("open");
});
💡
«Один класс гоняет» — ингредиент почти всех интерактивных компонентов курса.

Спрятать и показать: скрытие через класс

Три способа: класс display, атрибут hidden, свойство style. Лучший на старте — класс в CSS (.hide { display: none }) и «перекидывание» его через classList.

.hide { display: none }
hint.classList.add("hide");
hint.classList.remove("hide");
💡
«Визуал — на CSS, скрипт — на класс»: единственный источник правды про внешность.

Атрибуты: get/setAttribute

Управлять атрибутами можно напрямую: getAttribute, setAttribute, removeAttribute. Полезно для ссылок, картинок (src), title, «дата-атрибутов».

const img = document.querySelector("img");
img.setAttribute("src", "photo.jpg");
console.log(img.getAttribute("title"));
💡
Для «стандартных» атрибутов есть короткая дорога: img.src = "..." тоже работает гораздо чаще.

Соседи и родители: шаги по дереву

Из элемента можно «шагнуть»: .parentElement (вверх), .children (дети), .nextElementSibling / .previousElementSibling (соседи). Полезно, когда вокруг «нерасмеченный» HTML.

const item = document.querySelector(".item");
const parent = item.parentElement;
const next = item.nextElementSibling;
💡
Хождение по дереву «хрупко»: чаще надёжнее «найти заново» querySelector, чем тянуться «вбок».

Практика: изменить заголовок по клику

Первая «интерактивность»: кнопка меняет заголовок. Три шага: найти кнопку, повесить слушатель, в обработчике поменять textContent. Всё — и консоль, DevTools, страница.

const title = document.querySelector("#greet");
const btn = document.querySelector("#go");
btn.addEventListener("click", () => {
  title.textContent = "Вы нажали!";
});
💡
Запускайте с defer/в конце body — скрипт должен встретить элементы в DOM.

Событие как «сигнал»

Клик, наведение, ввод текста, загрузка — браузер порождает события: «что-то произошло». JS «слушает» и реагирует. Это и есть интерактивность: страница отзывается на действия.

💡
Любое событие — объект: у него есть название, цель (target) и данные (координаты, клавиши).

addEventListener: встать на прослушку

el.addEventListener("click", обработчик) — «при нажатии выполни функцию». Обработчик — любая функция: заранее описанная или стрелочная прямо внутри.

button.addEventListener("click", () => {
  console.log("Клик!");
});
💡
Метод «принимает функцию», а не «результат»: без скобок после имени — это «передаём действие».

Несколько слушателей: не перезаписать

addEventListener можно повесить несколько раз — все сработают по очереди. Старый вариант onclick = function «перезаписывал»: последний съедал предыдущих.

btn.addEventListener("click", fnA);
btn.addEventListener("click", fnB);
// сработают оба — не «затёрлись»
💡
Разным частям приложения — свои слушатели: «ферма» не мешает «счётчику».

Отмена поведения: preventDefault

Некоторые события имеют «поведение браузера»: клик по ссылке — переход, отправка формы — перезагрузка. event.preventDefault() останавливает его, оставляя «наш» код.

link.addEventListener("click", (ev) => {
  ev.preventDefault();
  console.log("ссылка нажата, но не ушли");
});
💡
Классика: «своя» отправка формы без перезагрузки страницы строится на preventDefault.

Событие формы: когда нажата отправка

Форма «отправляется» событием submit: ловим его на самой форме, делаем preventDefault, читаем поля. Так страница никуда не перезагружается.

form.addEventListener("submit", (ev) => {
  ev.preventDefault();
  const value = input.value.trim();
  console.log("Отправлено:", value);
});
💡
Обработчик вешайте на <form>, а не на кнопку: Enter в поле тоже «отправляет».

Клавиши: как поймать нажатие

keydown/keyup слушают клавиатуру; у события есть .key ("Enter", "ArrowDown") и .code (физическая клавиша). Быстрые клавиши, игры, «горячие» сочетания — отсюда.

document.addEventListener("keydown", (ev) => {
  if (ev.key === "Escape") closeModal();
});
💡
Для «ввода текста» отдельный друг — событие input поля (срабатывает на каждое изменение).

Мышь и палец: click и компания

Кроме click есть mouseenter (наведение), mouseleave (уход), dblclick, и touch-аналоги для телефонов. Общее правило: «клик» работает и мышью, и пальцем — стройте на нём.

card.addEventListener("mouseenter", () => {
  card.classList.add("hover");
});
💡
Для «наведения» хватает CSS :hover; JS mouseenter носит «по ситуации» — скрыть тултип, подгрузить.

Объект события: event.target

В обработчике всегда доступен параметр-событие: ev.target — элемент, который «пустил» событие. querySelector находим «по имени», target — «по факту клика».

list.addEventListener("click", (ev) => {
  console.log(ev.target.textContent);
});
💡
По клику на вложенный тег target — «самый глубокий»: проверяйте closest() для «точки на карточке».

Фокусный приём: делегирование в списках

Вешать обработчик на КАЖДЫЙ пункт списка дорого: делегирование ставит один слушатель на контейнер, а «кто именно» — решает ev.target + closest(). Список дел, меню, таблицы — классика.

todoList.addEventListener("click", (ev) => {
  const li = ev.target.closest(".task");
  if (li) li.classList.toggle("done");
});
💡
closest() «поднимается» вверх и возвращает ближайший подходящий элемент: идеальная пара с делегированием.

Создание элемента: createElement

document.createElement("li") создаёт узел «в воздухе» — пока его нет в дереве. Наполняем (классы, текст), потом вставляем в документ. Так списки и карточки появляются «из скрипта».

const li = document.createElement("li");
li.textContent = "Новая задача";
li.classList.add("task");
💡
Созданный узел не виден, пока его не прикрепили: «соберите на столе — потом поставь на полку».

Вставить в документ: append

parent.append(node) кладёт узел в конец родителя. Для «первого места» — prepend; append может принять сразу несколько аргументов. Новый узел «физически» появляется на странице.

todoList.append(li);
todoList.prepend(firstLi);
💡
append/appendChild в основном одинаковы; append «дружелюбнее»: принимает строку и несколько узлов разом.

Горячая замена: innerHTML

el.innerHTML = "..." переписывает содержимое HTML-строкой — быстро «для своего кода», но опасно с недоверенными данными (вставился пользовательский HTML — выполнятся вложенные техники).

// для списка «своих» карточек — ничего лишнего
list.innerHTML = "<li>Пункт</li>";
⚠️
С данными от пользователя innerHTML-источник XSS: предпочитайте textContent и createElement.

Удаление: remove()

element.remove() убирает узел и всё его содержимое из дерева. «Корзина» в списке дел, скрытие всплывающих окон, очистка сообщений.

const task = document.querySelector(".task");
task.remove();
💡
Событие и «данные» узла исчезают вместе с ним — не читайте из удалённого, вернётся null.

Поиск по data-атрибуту: dataset

Атрибуты data-* — «кармашки для данных разметки»: data-id, data-status. В JS к ним дорога через .dataset: el.dataset.id. Храните маленькие индентификаторы прямо в HTML.

<button data-action="delete">x</button>
const act = btn.dataset.action;   // "delete"
💡
data-атрибуты + делегирование = «карта действий»: один слушатель, а кнопки говорят, что делать.

Отложенная работа: setTimeout

setTimeout(fn, ms) запустит функцию через миллисекунды; setInterval — повторно по расписанию. Анимации-«по таймеру», всплывающие подсказки, обновление часов.

setTimeout(() => {
  toast.classList.add("show");
}, 500);
💡
Обратный отсчёт или «отключить кнопку на 3 сек» — живой пример отложенных вызовов.

Хранение между визитами: localStorage

localStorage памяти браузера держит строки «навсегда» (до очистки): настройки, заготовки, счётчики. Работа: setItem / getItem / removeItem. JSON.stringify объекты «складывать строчкой».

localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme");
💡
Числа/объекты хранятся как текст: при чтении JSON.parse — «вернуть» себе объект.

Практика: список дел

Всё вместе: форма → событие submit, createElement+append добавляют пункт, делегирование click гасит выполненное, localStorage помнит список между перезагрузками. Это «мини-проект» курса.

form.addEventListener("submit", (ev) => {
  ev.preventDefault();
  const li = document.createElement("li");
  li.textContent = input.value.trim();
  todoList.append(li);
  input.value = "";
});
💡
Соберите поэтапно: сначала добавление, потом «выполнено», потом хранение — каждый шаг рабочий.

Итоги урока 9: собран мини-проект

Вы умеете находить элементы, слушать события, создавать/удалять узлы, делегировать, хранить данные. Лист дел, табы, счётчик, «живая» форма — за вами. Дальше — «Что дальше»: fetch, анимации, современные фреймворки.

💡
Откройте свой мини-проект на мобильном телефоне — «настоящая» веб-разработка именно такая.

Общая точка: document в JS

document — «вход» в страницу из JavaScript: doc-for поиска, создания и слушания. Код в