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.

Что дальше

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

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