Манипуляция DOM

Манипуляция DOM — создание, вставка, удаление и изменение HTML-элементов и их содержимого через JavaScript API.

Зачем нужно

Динамические веб-приложения постоянно меняют содержимое страницы: показывают данные с сервера, реагируют на действия пользователя, строят интерфейс на лету. Все эти задачи решаются через манипуляцию DOM.

Где используется

  • Отрисовка данных из API
  • Создание динамических форм
  • Модальные окна и уведомления
  • Списки задач, чаты, комментарии
  • Любой SPA-фреймворк внутри делает то же самое

Предпосылки

DOM дерево, Поиск элементов, Навигация по DOM

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

// createElement — создаёт элемент (ещё НЕ в DOM!)
const div = document.createElement('div');
div.className = 'card';
div.id = 'card-1';
div.textContent = 'Новая карточка';

// createTextNode — текстовый узел
const text = document.createTextNode('Просто текст');

// createDocumentFragment — лёгкий контейнер
// Не создаёт лишних оберток в DOM
const fragment = document.createDocumentFragment;
for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = `Элемент ${i}`;
  fragment.appendChild(li);
}
document.querySelector('ul').appendChild(fragment); // один reflow!

// cloneNode — клонирование
const clone = div.cloneNode(false);  // без детей (shallow)
const deep = div.cloneNode(true);    // с детьми (deep)

Вставка элементов

const parent = document.querySelector('.container');
const newEl = document.createElement('div');
newEl.textContent = 'Новый элемент';

// Современные методы (рекомендуются)
parent.append(newEl);            // в конец (дети)
parent.prepend(newEl);           // в начало (дети)
parent.before(newEl);            // перед parent (сосед)
parent.after(newEl);             // после parent (сосед)

// append/prepend принимают несколько аргументов и строки
parent.append('Текст', document.createElement('br'), 'Ещё текст');

// replaceWith — замена элемента
const old = document.querySelector('.old');
const replacement = document.createElement('div');
replacement.textContent = 'Новый';
old.replaceWith(replacement);

// Старые методы (для совместимости)
parent.appendChild(newEl);                      // в конец
parent.insertBefore(newEl, parent.firstChild);  // перед элементом
parent.replaceChild(newEl, oldEl);              // замена

// insertAdjacentHTML — вставка HTML-строки
const card = document.querySelector('.card');
card.insertAdjacentHTML('beforebegin', '<p>Перед card</p>');   // <!-- перед -->
card.insertAdjacentHTML('afterbegin', '<p>В начало card</p>'); // в начало
card.insertAdjacentHTML('beforeend', '<p>В конец card</p>');   // в конец
card.insertAdjacentHTML('afterend', '<p>После card</p>');      // <!-- после -->

// insertAdjacentElement / insertAdjacentText — аналоги
card.insertAdjacentElement('afterend', newEl);
card.insertAdjacentText('beforeend', 'Текст');

Удаление элементов

// Современный способ
const el = document.querySelector('.remove()-me');
el.remove();

// Старый способ (через родителя)
const parent = el.parentNode;
parent.removeChild(el);

// Очистка всех дочерних элементов
const container = document.querySelector('.container');

// Способ 1: innerHTML (быстро, но уничтожает обработчики)
container.innerHTML = '';

// Способ 2: replaceChildren (чисто)
container.replaceChildren;

// Способ 3: цикл (контроль)
while (container.firstChild) {
  container.removeChild(container.firstChild);
}

Изменение содержимого

const el = document.querySelector('.content');

// textContent — безопасно (экранирует HTML)
el.textContent = 'Привет <b>мир</b>'; // теги отобразятся как текст

// innerHTML — парсит HTML (осторожно с XSS!)
el.innerHTML = 'Привет <b>мир</b>';   // <b> станет жирным

// outerHTML — заменяет сам элемент
el.outerHTML = '<section>Замена</section>'; // el больше не существует!
// Важно: переменная el всё ещё указывает на старый (отсоединённый) элемент

// innerText — учитывает CSS (скрытый текст не виден)
el.innerText = 'Видимый текст';

Работа с атрибутами

const img = document.querySelector('img');

// Чтение
img.getAttribute('src');
img.getAttribute('alt');
img.getAttribute('data-id');

// Установка
img.setAttribute('alt', 'Описание');
img.setAttribute('loading', 'lazy');

// Удаление
img.removeAttribute('title');

// Проверка
img.hasAttribute('alt'); // true

// Все атрибуты (NamedNodeMap)
for (const attr of img.attributes) {
  console.log(`${attr.name} = ${attr.value}`);
}

// dataset — удобный доступ к data-*
// <div data-user-id="42" data-role="admin">
const div = document.querySelector('div');
div.dataset.userId;        // "42" (data-user-id → camelCase)
div.dataset.role;          // "admin"
div.dataset.newProp = 'x'; // создаёт data-new-prop="x"
delete div.dataset.role;   // удаляет data-role

Работа с классами

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

// classList — DOMTokenList (рекомендуется)
el.classList.add('active');           // добавить
el.classList.add('highlight', 'big'); // несколько сразу
el.classList.remove('old');           // удалить
el.classList.toggle('visible');       // переключить
el.classList.toggle('dark', isDark);  // force: true/false
el.classList.contains('active');      // проверить (boolean)
el.classList.replace('old', 'new');   // заменить

// className — строка (перезаписывает ВСЕ классы)
el.className = 'card active';        // задать
el.className += ' highlight';        // добавить (неудобно)

Работа со стилями

const el = document.querySelector('.box');

// Inline-стили (свойство style)
el.style.width = '200px';
el.style.backgroundColor = 'blue';   // camelCase!
el.style.fontSize = '16px';
el.style.cssText = 'width: 200px; color: red;'; // строка (перезаписывает)

// Удаление inline-стиля
el.style.width = '';                  // сброс к CSS-значению

// Чтение вычисленного стиля (из CSS)
const computed = getComputedStyle(el);
console.log(computed.width);          // "200px"
console.log(computed.fontSize);       // "16px"
console.log(computed.display);        // "block"
// getComputedStyle — только чтение!

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

1. innerHTML и XSS

// Опасно — пользовательский ввод может содержать скрипты
const userInput = '<img src=x onerror="alert(1)">';
el.innerHTML = userInput; // XSS-атака!

// Безопасно — textContent экранирует HTML
el.textContent = userInput; // покажет как текст

2. Перерисовка при каждом изменении (layout thrashing)

// Плохо — каждая запись вызывает reflow
const list = document.querySelector('ul');
for (let i = 0; i < 1000; i++) {
  list.innerHTML += `<li>${i}</li>`; // перестраивает DOM 1000 раз!
}

// Хорошо — DocumentFragment или массив строк
const fragment = document.createDocumentFragment;
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = i;
  fragment.appendChild(li);
}
list.appendChild(fragment); // один reflow

// Или через innerHTML одним разом
list.innerHTML = Array.from({ length: 1000 }, (_, i) => `<li>${i}</li>`).join('');

3. outerHTML не обновляет переменную

const el = document.querySelector('.old');
el.outerHTML = '<div class="new">Новый</div>';

console.log(el.className); // "old" — переменная ссылается на старый элемент!
// Нужно заново найти элемент
const newEl = document.querySelector('.new');

Практика

  1. Создай список из 10 элементов через createElement и fragment
  2. Реализуй функцию, которая оборачивает выбранный элемент в <div class="wrapper">
  3. Построй таблицу из массива объектов [{ name, age, city }]
  4. Реализуй drag-and-drop перестановку элементов списка

Связанные темы

Ресурсы