Урок по треку Браузер и DOM 6 мин

DOM на практике: типовые задачи

Задачи с живым DOM: делегирование, рендер списков, работа с формами и обработчиками.

На практической части просят «без фреймворка» сделать список с удалением, табы, модалку, поиск с задержкой. Проверяют не знание редких методов, а несколько привычек. Разберём их на примерах.

1. Делегирование вместо сотни обработчиков

Не вешайте обработчик на каждый элемент списка. Один обработчик на контейнер, а нужный элемент ищется через closest.

list.addEventListener('click', (e) => {
  const btn = e.target.closest('[data-action="remove"]');
  if (!btn || !list.contains(btn)) return;
  btn.closest('li').remove();
});

Почему closest, а не e.target: клик мог прийти по иконке внутри кнопки, и e.target будет <svg>. Работает и для элементов, добавленных позже.

2. Данные в data-атрибутах

<li data-id="42">Купить хлеб <button data-action="remove">×</button></li>
const id = Number(li.dataset.id); // dataset всегда строки

3. Рендер списка без XSS и лишних перерисовок

function render(items) {
  const frag = document.createDocumentFragment();
  for (const item of items) {
    const li = document.createElement('li');
    li.dataset.id = item.id;
    li.textContent = item.title; // не innerHTML с пользовательским текстом
    frag.append(li);
  }
  list.replaceChildren(frag);
}
  • textContent вместо innerHTML для пользовательских данных.
  • DocumentFragment или один replaceChildren, чтобы вставить всё за одну операцию.
  • <template> удобен для сложной разметки: template.content.cloneNode(true).

4. Не смешивать чтение и запись раскладки

// плохо: на каждой итерации браузер пересчитывает layout
for (const el of items) el.style.width = container.offsetWidth + 'px';

// хорошо: прочитать один раз, потом писать
const w = container.offsetWidth;
for (const el of items) el.style.width = w + 'px';

5. Формы

form.addEventListener('submit', (e) => {
  e.preventDefault();
  const data = Object.fromEntries(new FormData(form));
  if (!data.title.trim()) return;
  add(data);
  form.reset();
});

Слушайте submit у формы, а не click у кнопки: так работает и Enter в поле. Для живого поиска событие input, а не keyup (вставка мышью и автозаполнение не дают keyup).

6. Debounce для поиска

function debounce(fn, ms) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), ms);
  };
}
input.addEventListener('input', debounce((e) => search(e.target.value), 300));

Плюс отменять устаревший запрос через AbortController, чтобы старый ответ не перезаписал новый.

7. Модалка и клавиатура

  • Встроенный <dialog> с showModal() даёт фон, закрытие по Esc, ловушку фокуса и инертность остальной страницы.
  • Закрытие по клику на фон: dialog.addEventListener('click', (e) => { if (e.target === dialog) dialog.close(); }).
  • После закрытия фокус возвращается на кнопку, которая открыла модалку (для <dialog> браузер делает это сам).

8. Убирать за собой

Обработчики на window и document, таймеры и наблюдатели снимайте, когда компонент удаляется. Удобно через AbortController:

const ac = new AbortController();
window.addEventListener('resize', onResize, { signal: ac.signal });
document.addEventListener('keydown', onKey, { signal: ac.signal });
// при уничтожении
ac.abort(); // снимет оба обработчика

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

  • e.target вместо closest в делегировании.
  • innerHTML += в цикле: каждый раз пересоздаёт всё содержимое и теряет обработчики и состояние полей.
  • Сравнивать dataset.id с числом через ===.
  • Слушать click по кнопке отправки вместо submit.

Что спросят на собесе

  • Сделай список задач с добавлением и удалением на чистом DOM.
  • Почему делегирование и как оно работает с вложенными элементами.
  • Как отрисовать тысячу элементов быстро.
  • Как сделать поиск, который не шлёт запрос на каждую букву.
  • Как сделать доступную модалку.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.