Глиф Учебники

Урок 9. DOM: элементы страницы и события

Курс «JavaScript» · урок 9 из 10 · 5 минут чтения

Когда браузер открывает страницу, он превращает HTML в дерево объектов — DOM (Document Object Model, объектная модель документа). Каждый тег становится объектом-элементом: у него есть свойства (текст, классы, атрибуты) и методы. JavaScript меняет эти объекты, и браузер сразу перерисовывает страницу.

Всё в этом уроке работает только в браузере: в Node.js страницы и document нет. Примеры проверяйте в файле .html или прямо в консоли (F12) на открытой странице.

Где ставить скрипт

Скрипт может обращаться только к тем элементам, которые браузер уже прочитал. Если <script> стоит в <head> без особых пометок, он выполнится раньше, чем появится <body>, и элементов не найдёт. Надёжный способ — отдельный файл с атрибутом defer:

HTML
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Счётчик</title>
  <script src="counter.js" defer></script>
</head>
<body>
  <p>Нажато: <span id="count">0</span></p>
  <button id="plus">+1</button>
  <button id="reset">Сбросить</button>
</body>
</html>
document html head body meta title script p button#plus button#reset span#count 0 текст находит counter.js
Браузер превращает эту страницу в дерево объектов: document, в нём html с head и body, в body — абзац p с span#count и две кнопки; эти три элемента и находит скрипт.

defer велит загрузить файл сразу, а выполнить — когда вся страница прочитана. Если скрипт всё же выполнился слишком рано, в консоли появляется ошибка Cannot read properties of null — разбор в справке Ошибка Cannot read properties of null.

чтение страницы head body p, button, button конец <script> без defer скрипт выполняется сразу элементов ещё нет — null с defer файл загружается выполняется: элементы уже есть
Скрипт без defer в head выполняется, как только браузер до него дочитал, — элементов body ещё нет, и поиск даёт null; с defer файл грузится сразу, а выполняется, когда прочитана вся страница.

Найти элемент

JavaScript
const countEl = document.getElementById("count");
const firstButton = document.querySelector("button");
const plusBtn = document.querySelector("#plus");
const allButtons = document.querySelectorAll("button");

console.log(allButtons.length);

Если элемента нет, getElementById и querySelector возвращают null, а querySelectorAll — пустой список.

Изменить элемент

JavaScript
const title = document.querySelector("h1");

title.textContent = "Новый заголовок";
title.style.color = "darkblue";
title.classList.add("highlight");
title.classList.remove("hidden");
title.classList.toggle("active");

const link = document.querySelector("a");
link.href = "https://developer.mozilla.org/";
link.setAttribute("target", "_blank");

Есть ещё свойство innerHTML — оно принимает не текст, а HTML-разметку: list.innerHTML = "<li>Пункт</li>".

Не вставляйте через innerHTML то, что ввёл пользователь или прислал сервер. Если в тексте окажется <img src=x onerror="…">, браузер выполнит чужой код на вашей странице: так крадут входы и данные посетителей (атака XSS). Для текста всегда берите textContent — он показывает любые символы как текст.

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

JavaScript
const list = document.querySelector("#list");

const item = document.createElement("li");
item.textContent = "Купить хлеб";
item.classList.add("todo");
list.append(item);

const old = document.querySelector(".obsolete");
if (old) {
  old.remove();
}

document.createElement("li") создаёт элемент, но на странице его ещё нет. append добавляет его в конец указанного элемента, prepend — в начало. remove() убирает элемент со страницы. Проверка if (old) защищает от ошибки, если такого элемента нет.

События

Событие — то, что происходит на странице: щелчок, нажатие клавиши, ввод в поле, отправка формы. Чтобы отреагировать, элементу назначают обработчик — функцию, которую браузер вызовет, когда событие случится. Вот файл counter.js для страницы-счётчика из начала урока:

JavaScript
const countEl = document.getElementById("count");
const plusBtn = document.getElementById("plus");
const resetBtn = document.getElementById("reset");
let count = 0;

plusBtn.addEventListener("click", () => {
  count++;
  countEl.textContent = count;
});

resetBtn.addEventListener("click", () => {
  count = 0;
  countEl.textContent = count;
});

Откройте страницу: каждое нажатие «+1» увеличивает число на странице, «Сбросить» возвращает ноль.

addEventListener(событие, функция) — первый аргумент — имя события строкой, второй — обработчик. Функцию передают, а не вызывают: addEventListener("click", update), без скобок после update. Со скобками функция выполнится сразу, один раз, а обработчиком станет её результат.

Нажато: 0 +1 click обработчик — его вызывает браузер count++; countEl.textContent = count; Нажато: 1 +1 addEventListener("click", update) функция передана: вызов — при щелчке addEventListener("click", update()) вызвана сразу, один раз; обработчиком станет её результат
Щелчок по «+1» — событие click: браузер вызывает переданную функцию-обработчик, та увеличивает count и пишет его в span; со скобками update() функция выполнилась бы сразу, а не при щелчке.

Частые события:

СобытиеКогда происходит
clickщелчок по элементу
inputизменилось содержимое поля ввода
changeполе изменено и потеряло фокус; выбран пункт списка или флажок
submitотправка формы
keydownнажата клавиша
DOMContentLoadedстраница прочитана, элементы готовы (событие у document)

Обработчик получает объект события. В нём — подробности: event.target — элемент, с которым случилось событие, event.key — нажатая клавиша.

Форма: список дел

Обычный пример — форма, которая добавляет пункты в список. Разметка:

HTML
<form id="todo-form">
  <input id="todo-text" placeholder="Новое дело">
  <button>Добавить</button>
</form>
<ul id="todo-list"></ul>

Код:

JavaScript
const form = document.querySelector("#todo-form");
const input = document.querySelector("#todo-text");
const list = document.querySelector("#todo-list");

form.addEventListener("submit", (event) => {
  event.preventDefault();

  const text = input.value.trim();
  if (text === "") {
    return;
  }

  const item = document.createElement("li");
  item.textContent = text;
  item.addEventListener("click", () => item.classList.toggle("done"));
  list.append(item);

  input.value = "";
  input.focus();
});

Введите дело и нажмите «Добавить» или Enter: в списке появится пункт, поле очистится. Щелчок по пункту включает и выключает у него класс done — в CSS ему можно задать, например, зачёркнутый текст.

Частые ошибки

Коротко

Проверьте себя

1. Что вернёт document.querySelector("#menu"), если элемента с таким id нет?
2. Чем textContent отличается от innerHTML?
3. Зачем в обработчике submit вызывают event.preventDefault()?
4. Как правильно назначить функцию update обработчиком щелчка?