JS и DOM: события, манипуляция страницей
DOM (Document Object Model) - это представление HTML-страницы в виде дерева объектов. Каждый тег - узел. JS может находить узлы, менять их, вешать обработчики событий. Это и есть «оживление» статичного HTML.
DOM как дерево
<body>
<header>
<h1>BackendStart</h1>
</header>
<main>
<p class="lead">Учим бэкенд.</p>
</main>
</body>
В дереве:
document
└── html
└── body
├── header
│ └── h1 ("BackendStart")
└── main
└── p.lead ("Учим бэкенд.")
JS видит это как объекты. document - корень. От него вниз по дереву.
Поиск элементов
querySelector и querySelectorAll
// Один элемент (первый совпавший) или null
const title = document.querySelector('h1');
const lead = document.querySelector('.lead'); // по классу
const email = document.querySelector('#email'); // по id
const btn = document.querySelector('button[type="submit"]'); // CSS-селектор любой сложности
// Все совпавшие - NodeList (похож на массив)
const items = document.querySelectorAll('.item');
items.forEach(el => console.log(el.textContent));
items.length;
querySelector принимает любой CSS-селектор - то, что ты учил в css-basics. Это удобнее, чем старые getElementById / getElementsByClassName, потому что один API на все случаи.
Поиск внутри элемента
const form = document.querySelector('form');
const email = form.querySelector('input[name="email"]'); // только внутри формы
Не обязательно искать от document - любой элемент знает свой поддерево.
Чтение и изменение содержимого
const title = document.querySelector('h1');
// Текст
title.textContent; // "BackendStart"
title.textContent = 'Привет'; // меняет текст
// HTML (опасно! см. ниже)
title.innerHTML; // "BackendStart"
title.innerHTML = '<em>Привет</em>'; // вставит <em>, его браузер распарсит как тег
// Атрибуты
const link = document.querySelector('a');
link.href; // через геттер
link.getAttribute('href'); // или явный метод
link.setAttribute('target', '_blank');
link.removeAttribute('target');
// Свойства типа checked, value, disabled
const input = document.querySelector('input');
input.value; // текст в поле
input.value = 'новое значение';
input.disabled = true; // дизаблит поле
textContent vs innerHTML - это про безопасность
const name = '<img src=x onerror="alert(1)">';
title.textContent = name; // безопасно: вставит как текст
title.innerHTML = name; // ОПАСНО: браузер выполнит onerror
Если значение приходит от пользователя (комментарий, поле формы, URL-параметр), всегда используй textContent. innerHTML оставь для безопасного контента, который ты сам генеришь, или для шаблонизатора.
Классы и стили
const card = document.querySelector('.card');
// Классы
card.classList.add('active');
card.classList.remove('hidden');
card.classList.toggle('expanded');
card.classList.contains('active'); // true/false
// Стили (для разовых правок; для логики держи всё в CSS)
card.style.color = 'red';
card.style.backgroundColor = '#fff'; // в CSS background-color, в JS camelCase
Правило: для смены состояния (visible/hidden, active/disabled, expanded/collapsed) - всегда classList. Стили лежат в CSS, JS только переключает класс. Прямое назначение element.style.x - для одноразовых динамических вычислений (например, рассчитанная высота).
События
Событие - это «что-то произошло»: клик, наведение, ввод символа, отправка формы, загрузка картинки, нажатие клавиши. JS вешает обработчик и реагирует.
addEventListener
const btn = document.querySelector('#save');
btn.addEventListener('click', (event) => {
console.log('Клик!', event);
});
Сигнатура: element.addEventListener(тип, обработчик). Обработчик получает объект event - что именно случилось.
Самые частые типы событий
click - клик мыши / тап на мобильном
input - каждое изменение значения <input> или <textarea>
change - изменение и потеря фокуса (для select, checkbox)
submit - отправка формы
keydown - нажата клавиша
keyup - клавиша отпущена
focus / blur - поле получило / потеряло фокус
mouseenter / mouseleave - наведение / уход мыши
load - что-то загрузилось (картинка, страница)
DOMContentLoaded - HTML распарсен (но картинки могли ещё не загрузиться)
Объект event
form.addEventListener('submit', (event) => {
event.preventDefault(); // отменить стандартное поведение (для submit - перезагрузку)
event.target; // элемент, который послал событие (форма)
event.currentTarget; // элемент, на который повесили обработчик (та же форма)
event.stopPropagation(); // не пускать событие вверх по дереву
});
input.addEventListener('input', (event) => {
console.log(event.target.value); // что в поле сейчас
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeModal();
}
});
preventDefault - отменить дефолтное поведение браузера. Для submit - не перезагружать страницу. Для клика по ссылке - не переходить. Для keydown - не вводить символ.
Снятие обработчика
function onClick() { console.log('клик'); }
btn.addEventListener('click', onClick);
btn.removeEventListener('click', onClick); // нужна та же ссылка на функцию
Стрелочную функцию-литерал снять нельзя - она каждый раз новая ссылка. Если нужно снимать, объяви функцию отдельно.
Делегирование событий
Допустим, на странице 100 кнопок «удалить»:
<ul>
<li>Задача 1 <button class="delete">x</button></li>
<li>Задача 2 <button class="delete">x</button></li>
... ещё 98
</ul>
Плохо - вешать 100 обработчиков. Хорошо - один на родителя:
document.querySelector('ul').addEventListener('click', (event) => {
if (event.target.classList.contains('delete')) {
event.target.closest('li').remove();
}
});
Событие всплывает вверх по дереву (bubble). Обработчик на <ul> ловит клик с любого <button> внутри. event.target - кто реально кликнут. Это и есть делегирование - экономит память и работает для динамически добавленных элементов.
Создание и удаление элементов
// Создать элемент
const li = document.createElement('li');
li.textContent = 'Новая задача';
li.classList.add('task');
// Добавить в DOM
const list = document.querySelector('ul');
list.appendChild(li); // в конец
list.prepend(li); // в начало
list.insertBefore(li, list.firstChild); // перед первым
// Удалить
li.remove();
В современном коде используется appendChild для одного элемента и append(a, b, c) для нескольких (или для текста + элементов вперемешку).
Работа с формами из JS
<form id="signup">
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Регистрация</button>
</form>
const form = document.querySelector('#signup');
form.addEventListener('submit', (event) => {
event.preventDefault();
// Способ 1: руками
const email = form.elements.email.value;
const password = form.elements.password.value;
// Способ 2: через FormData (видели в forms)
const data = new FormData(form);
const obj = Object.fromEntries(data);
// obj = { email: '...', password: '...' }
console.log(obj);
// Отправка через fetch - следующий урок.
});
form.elements.<name> достаёт поле по атрибуту name. FormData собирает все поля одним вызовом.
Когда выполнять JS
<!-- JS до того, как DOM появился - querySelector вернёт null -->
<head>
<script>
const btn = document.querySelector('button'); // null
</script>
</head>
<body>
<button>Жми</button>
</body>
Решения:
<!-- Скрипт в конце body -->
<body>
<button>Жми</button>
<script>
const btn = document.querySelector('button'); // OK
</script>
</body>
<!-- Атрибут defer - выполнится после парсинга HTML -->
<head>
<script src="app.js" defer></script>
</head>
<!-- DOMContentLoaded -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const btn = document.querySelector('button');
});
</script>
В реальных проектах используется defer или модули (type="module" - они тоже defer по умолчанию).
Что пропустили намеренно
- Шаблонизаторы (
<template>, lit, htm) - пригодится в крупном проекте. - Virtual DOM и реактивность - это уже React/Vue, отдельная история.
- Web Components (Custom Elements, Shadow DOM) - нишевая тема.
Для бэкендера достаточно уметь: найти элемент, повесить обработчик, изменить текст или классы, прочитать форму. Этим и закроем 90% случаев.
Мини-задание
- Сделай HTML со списком из 3 пунктов и кнопкой «Добавить»
- По клику на «Добавить» добавь новый пункт в список (через
createElement+appendChild) - По клику на любой пункт списка (делегирование!) - удали его
- Добавь поле ввода, и пусть кнопка «Добавить» брала текст из него; если пусто - ничего не добавляй
- Открой DevTools → Elements, посмотри, как DOM меняется в реальном времени
На следующем уроке научимся отправлять данные в твой backend через fetch и обрабатывать ответ - без перезагрузки страницы.