Изменение документа
Модификации DOM – это ключ к созданию «живых» страниц.
Здесь мы увидим, как создавать новые элементы «на лету» и изменять уже существующие.
Пример: показать сообщение
Рассмотрим методы на примере – а именно, добавим на страницу сообщение, которое будет выглядеть получше, чем alert.
Вот такое:
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<div class="alert">
<strong>Всем привет!</strong> Вы прочитали важное сообщение.
</div>
Это был пример HTML. Теперь давайте создадим такой же div, используя JavaScript (предполагаем, что стили в HTML или во внешнем CSS-файле).
Создание элемента
DOM-узел можно создать двумя методами:
document.createElement(tag)-
Создаёт новый элемент с заданным тегом:
let div = document.createElement('div'); document.createTextNode(text)-
Создаёт новый текстовый узел с заданным текстом:
let textNode = document.createTextNode('А вот и я');
Большую часть времени нам нужно создавать узлы элементов, такие как div для сообщения.
Создание сообщения
В нашем случае сообщение – это div с классом alert и HTML в нём:
let div = document.createElement('div');
div.className = "alert";
div.innerHTML = "<strong>Всем привет!</strong> Вы прочитали важное сообщение.";
Мы создали элемент, но пока он только в переменной. Мы не можем видеть его на странице, поскольку он не является частью документа.
Методы вставки
Чтобы наш div появился, нам нужно вставить его где-нибудь в document. Например, в document.body.
Для этого есть метод append, в нашем случае: document.body.append(div).
Вот полный пример:
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
let div = document.createElement('div');
div.className = "alert";
div.innerHTML = "<strong>Всем привет!</strong> Вы прочитали важное сообщение.";
document.body.append(div);
</script>
Вот методы для различных вариантов вставки:
node.append(...nodes or strings)– добавляет узлы или строки в конецnode,node.prepend(...nodes or strings)– вставляет узлы или строки в началоnode,node.before(...nodes or strings)–- вставляет узлы или строки доnode,node.after(...nodes or strings)–- вставляет узлы или строки послеnode,node.replaceWith(...nodes or strings)–- заменяетnodeзаданными узлами или строками.
Вот пример использования этих методов, чтобы добавить новые элементы в список и текст до/после него:
<ol id="ol">
<li>0</li>
<li>1</li>
<li>2</li>
</ol>
<script>
ol.before('before'); // вставить строку "before" перед <ol>
ol.after('after'); // вставить строку "after" после <ol>
let liFirst = document.createElement('li');
liFirst.innerHTML = 'prepend';
ol.prepend(liFirst); // вставить liFirst в начало <ol>
let liLast = document.createElement('li');
liLast.innerHTML = 'append';
ol.append(liLast); // вставить liLast в конец <ol>
</script>
Наглядная иллюстрация того, куда эти методы вставляют:
Итоговый список будет таким:
before
<ol id="ol">
<li>prepend</li>
<li>0</li>
<li>1</li>
<li>2</li>
<li>append</li>
</ol>
after
Эти методы могут вставлять несколько узлов и текстовых фрагментов за один вызов.
Например, здесь вставляется строка и элемент:
Весь текст вставляется как текст.
Поэтому финальный HTML будет:
Другими словами, строки вставляются безопасным способом, как делает это elem.textContent.
Поэтому эти методы могут использоваться только для вставки DOM-узлов или текстовых фрагментов.
А что, если мы хотим вставить HTML именно «как html», со всеми тегами и прочим, как делает это elem.innerHTML?
insertAdjacentHTML/Text/Element
С этим может помочь другой, довольно универсальный метод: elem.insertAdjacentHTML(where, html).
Первый параметр – это специальное слово, указывающее, куда по отношению к elem производить вставку. Значение должно быть одним из следующих:
"beforebegin"– вставитьhtmlнепосредственно передelem,"afterbegin"– вставитьhtmlв началоelem,"beforeend"– вставитьhtmlв конецelem,"afterend"– вставитьhtmlнепосредственно послеelem.
Второй параметр – это HTML-строка, которая будет вставлена именно «как HTML».
Например:
…Приведёт к:
Так мы можем добавлять произвольный HTML на страницу.
Варианты вставки:
Мы можем легко заметить сходство между этой и предыдущей картинкой. Точки вставки фактически одинаковые, но этот метод вставляет HTML.
У метода есть два брата:
elem.insertAdjacentText(where, text)– такой же синтаксис, но строкаtextвставляется «как текст», вместо HTML,elem.insertAdjacentElement(where, elem)– такой же синтаксис, но вставляет элементelem.
Они существуют, в основном, чтобы унифицировать синтаксис. На практике часто используется только insertAdjacentHTML. Потому что для элементов и текста у нас есть методы append/prepend/before/after – их быстрее написать, и они могут вставлять как узлы, так и текст.
Так что, вот альтернативный вариант показа сообщения:
Удаление узлов
Для удаления узла есть методы node.remove().
Например, сделаем так, чтобы наше сообщение удалялось через секунду:
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<script>
let div = document.createElement('div');
div.className = "alert";
div.innerHTML = "<strong>Всем привет!</strong> Вы прочитали важное сообщение.";
document.body.append(div);
setTimeout(() => div.remove(), 1000);
</script>
Если нам нужно переместить элемент в другое место – нет необходимости удалять его со старого.
Все методы вставки автоматически удаляют узлы со старых мест.
Например, давайте поменяем местами элементы:
Клонирование узлов: cloneNode
Как вставить ещё одно подобное сообщение?
Мы могли бы создать функцию и поместить код туда. Альтернатива – клонировать существующий div и изменить текст внутри него (при необходимости).
Иногда, когда у нас есть большой элемент, это может быть быстрее и проще.
- Вызов
elem.cloneNode(true)создаёт «глубокий» клон элемента – со всеми атрибутами и дочерними элементами. Если мы вызовемelem.cloneNode(false), тогда клон будет без дочерних элементов.
Пример копирования сообщения:
<style>
.alert {
padding: 15px;
border: 1px solid #d6e9c6;
border-radius: 4px;
color: #3c763d;
background-color: #dff0d8;
}
</style>
<div class="alert" id="div">
<strong>Всем привет!</strong> Вы прочитали важное сообщение.
</div>
<script>
let div2 = div.cloneNode(true); // клонировать сообщение
div2.querySelector('strong').innerHTML = 'Всем пока!'; // изменить клонированный элемент
div.after(div2); // показать клонированный элемент после существующего div
</script>
DocumentFragment
DocumentFragment является специальным DOM-узлом, который служит обёрткой для передачи списков узлов.
Мы можем добавить к нему другие узлы, но когда мы вставляем его куда-то, он «исчезает», вместо него вставляется его содержимое.
Например, getListContent ниже генерирует фрагмент с элементами <li>, которые позже вставляются в <ul>:
Обратите внимание, что на последней строке с (*) мы добавляем DocumentFragment, но он «исчезает», поэтому структура будет:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
DocumentFragment редко используется. Зачем добавлять элементы в специальный вид узла, если вместо этого мы можем вернуть массив узлов? Переписанный пример:
Мы упоминаем DocumentFragment в основном потому, что он используется в некоторых других областях, например, для элемента template, который мы рассмотрим гораздо позже.
Устаревшие методы вставки/удаления
Есть несколько других, более старых, методов вставки и удаления, которые существуют по историческим причинам.
Сейчас уже нет причин их использовать, так как современные методы append, prepend, before, after, remove, replaceWith более гибкие и удобные.
Мы упоминаем о них только потому, что их можно найти во многих старых скриптах:
parentElem.appendChild(node)-
Добавляет
nodeв конец дочерних элементовparentElem.Следующий пример добавляет новый
<li>в конец<ol>: parentElem.insertBefore(node, nextSibling)-
Вставляет
nodeпередnextSiblingвparentElem.Следующий пример вставляет новый элемент перед вторым
<li>:Чтобы вставить
newLiв начало, мы можем сделать вот так:list.insertBefore(newLi, list.firstChild); parentElem.replaceChild(node, oldChild)-
Заменяет
oldChildнаnodeсреди дочерних элементовparentElem. parentElem.removeChild(node)-
Удаляет
nodeизparentElem(предполагается, что он родительnode).Этот пример удалит первый
<li>из<ol>:
Все эти методы возвращают вставленный/удалённый узел. Другими словами, parentElem.appendChild(node) вернёт node. Но обычно возвращаемое значение не используют, просто вызывают метод.
Несколько слов о «document.write»
Есть ещё один, очень древний метод добавления содержимого на веб-страницу: document.write.
Синтаксис:
Вызов document.write(html) записывает html на страницу «прямо здесь и сейчас». Строка html может быть динамически сгенерирована, поэтому метод достаточно гибкий. Мы можем использовать JavaScript, чтобы создать полноценную веб-страницу и записать её в документ.
Этот метод пришёл к нам со времён, когда ещё не было ни DOM, ни стандартов… Действительно старые времена. Он всё ещё живёт, потому что есть скрипты, которые используют его.
В современных скриптах он редко встречается из-за следующего важного ограничения:
Вызов document.write работает только во время загрузки страницы.
Если вызвать его позже, то существующее содержимое документа затрётся.
Например:
Так что после того, как страница загружена, он уже непригоден к использованию, в отличие от других методов DOM, которые мы рассмотрели выше.
Это его недостаток.
Есть и преимущество. Технически, когда document.write запускается во время чтения HTML браузером, и что-то пишет в документ, то браузер воспринимает это так, как будто это изначально было частью загруженного HTML-документа.
Поэтому он работает невероятно быстро, ведь при этом нет модификации DOM. Метод пишет прямо в текст страницы, пока DOM ещё в процессе создания.
Так что, если нам нужно динамически добавить много текста в HTML, и мы находимся на стадии загрузки, и для нас очень важна скорость, это может помочь. Но на практике эти требования редко сочетаются. И обычно мы можем увидеть этот метод в скриптах просто потому, что они старые.
Итого
-
Методы для создания узлов:
document.createElement(tag)– создаёт элемент с заданным тегом,document.createTextNode(value)– создаёт текстовый узел (редко используется),elem.cloneNode(deep)– клонирует элемент, еслиdeep==true, то со всеми дочерними элементами.
-
Вставка и удаление:
node.append(...nodes or strings)– вставляет вnodeв конец,node.prepend(...nodes or strings)– вставляет вnodeв начало,node.before(...nodes or strings)– вставляет прямо передnode,node.after(...nodes or strings)– вставляет сразу послеnode,node.replaceWith(...nodes or strings)– заменяетnode.node.remove()– удаляетnode.
-
Устаревшие методы:
parent.appendChild(node)parent.insertBefore(node, nextSibling)parent.removeChild(node)parent.replaceChild(newElem, node)
Все эти методы возвращают
node. -
Если нужно вставить фрагмент HTML, то
elem.insertAdjacentHTML(where, html)вставляет в зависимости отwhere:"beforebegin"– вставляетhtmlпрямо передelem,"afterbegin"– вставляетhtmlвelemв начало,"beforeend"– вставляетhtmlвelemв конец,"afterend"– вставляетhtmlсразу послеelem.
Также существуют похожие методы
elem.insertAdjacentTextиelem.insertAdjacentElement, они вставляют текстовые строки и элементы, но они редко используются. -
Чтобы добавить HTML на страницу до завершения её загрузки:
document.write(html)
После загрузки страницы такой вызов затирает документ. В основном встречается в старых скриптах.
Задачи
У нас есть пустой DOM-элемент elem и строка text.
Какие из этих 3-х команд работают одинаково?
elem.append(document.createTextNode(text))elem.innerHTML = textelem.textContent = text
Создайте функцию clear(elem), которая удаляет всё содержимое из elem.
Сначала давайте посмотрим, как не надо это делать:
function clear(elem) {
for (let i=0; i < elem.childNodes.length; i++) {
elem.childNodes[i].remove();
}
}
Это не будет работать, потому что вызов remove() сдвигает коллекцию elem.childNodes, поэтому элементы начинаются каждый раз с индекса 0. А i увеличивается, и некоторые элементы будут пропущены.
Цикл for..of делает то же самое.
Правильным вариантом может быть:
function clear(elem) {
while (elem.firstChild) {
elem.firstChild.remove();
}
}
А также есть более простой способ сделать то же самое:
function clear(elem) {
elem.innerHTML = '';
}
В примере ниже вызов table.remove() удаляет таблицу из документа.
Но если вы запустите его, вы увидите, что текст "aaa" все еще виден.
Почему так происходит?
HTML в задаче некорректен. В этом всё дело.
Браузер исправил ошибку автоматически. Но внутри <table> не может быть текста: в соответствии со спецификацией допускаются только табличные теги. Поэтому браузер показывает "aaa" до <table>.
Теперь очевидно, что когда мы удаляем таблицу, текст остаётся.
На этот вопрос можно легко ответить, изучив DOM, используя инструменты браузера. Вы увидите "aaa" до элемента <table>.
Вообще, в стандарте HTML описано, как браузеру обрабатывать некорректный HTML, так что такое действие браузера является правильным.
Напишите интерфейс для создания списка.
Для каждого пункта:
- Запрашивайте содержимое пункта у пользователя с помощью
prompt. - Создавайте элемент
<li>и добавляйте его к<ul>. - Продолжайте до тех пор, пока пользователь не отменит ввод (нажатием клавиши Esc или введя пустую строку).
Все элементы должны создаваться динамически.
Если пользователь вводит HTML-теги, они должны обрабатываться как текст.
Обратите внимание на использование textContent для добавления содержимого в <li>.
Напишите функцию createTree, которая создаёт вложенный список ul/li из объекта.
Например:
let data = {
"Рыбы": {
"форель": {},
"лосось": {}
},
"Деревья": {
"Огромные": {
"секвойя": {},
"дуб": {}
},
"Цветковые": {
"яблоня": {},
"магнолия": {}
}
}
};
Синтаксис:
let container = document.getElementById('container');
createTree(container, data); // создаёт дерево в контейнере
Результат (дерево):
Выберите один из двух способов решения этой задачи:
- Создать строку, а затем присвоить через
container.innerHTML. - Создавать узлы через методы DOM.
Если получится – сделайте оба.
P.S. Желательно, чтобы в дереве не было лишних элементов, в частности -– пустых <ul></ul> на нижнем уровне.
Самый лёгкий способ – это использовать рекурсию.
Есть дерево, организованное в виде вложенных списков ul/li.
Напишите код, который добавит каждому элементу списка <li> количество вложенных в него элементов. Узлы нижнего уровня, без детей – пропускайте.
Результат:
Чтобы добавить текст к каждому <li>, мы можем изменить текстовый узел data.
Напишите функцию createCalendar(elem, year, month).
Вызов функции должен создать календарь для заданного месяца month в году year и вставить его в elem.
Календарь должен быть таблицей, где неделя – это <tr>, а день – это <td>. У таблицы должен быть заголовок с названиями дней недели, каждый день – <th>, первым днём недели должен быть понедельник.
Например, createCalendar(cal, 2012, 9) сгенерирует в cal следующий календарь:
P.S. В этой задаче достаточно сгенерировать календарь, кликабельным его делать не нужно.
Для решения задачи сгенерируем таблицу в виде строки: "<table>...</table>", а затем присвоим в innerHTML.
Алгоритм:
- Создать заголовок таблицы с
<th>и именами дней недели. - Создать объект даты
d = new Date(year, month-1). Это первый день месяцаmonth(с учётом того, что месяцы в JS начинаются от 0, а не от 1). - Ячейки первого ряда пустые от начала и до дня недели
d.getDay(), с которого начинается месяц. Заполним<td></td>. - Увеличить день в
d:d.setDate(d.getDate()+1). Еслиd.getMonth()ещё не в следующем месяце, то добавим новую ячейку<td>в календарь. Если это воскресенье, то добавим новую строку«</tr><tr>». - Если месяц закончился, но строка таблицы ещё не заполнена, добавим в неё пустые
<td>, чтобы сделать в календаре красивые пустые квадратики.
Создайте цветные часы как в примере ниже:
Для стилизации используйте HTML/CSS, JavaScript должен только обновлять время в элементах.
Для начала придумаем подходящую HTML/CSS-структуру.
Здесь каждый компонент времени удобно поместить в соответствующий <span>:
<div id="clock">
<span class="hour">hh</span>:<span class="min">mm</span>:<span class="sec">ss</span>
</div>
Каждый span раскрашивается при помощи CSS.
Функция update будет обновлять часы, setInterval вызывает её каждую секунду:
function update() {
let clock = document.getElementById('clock');
let date = new Date(); // (*)
let hours = date.getHours();
if (hours < 10) hours = '0' + hours;
clock.children[0].innerHTML = hours;
let minutes = date.getMinutes();
if (minutes < 10) minutes = '0' + minutes;
clock.children[1].innerHTML = minutes;
let seconds = date.getSeconds();
if (seconds < 10) seconds = '0' + seconds;
clock.children[2].innerHTML = seconds;
}
В строке (*) каждый раз мы получаем текущую дату. Вызовы setInterval не надёжны: они могут происходить с задержками.
Функция clockStart для запуска часов:
let timerId;
function clockStart() { // запустить часы
timerId = setInterval(update, 1000);
update(); // (*)
}
function clockStop() {
clearInterval(timerId);
timerId = null;
}
Обратите внимание, что вызов update() не только запланирован, но и тут же производится в строке (*). Иначе посетителю пришлось бы ждать до первого выполнения setInterval, то есть целую секунду.
Напишите код для вставки <li>2</li><li>3</li> между этими двумя <li>:
<ul id="ul">
<li id="one">1</li>
<li id="two">4</li>
</ul>
Когда нам необходимо вставить фрагмент HTML-кода, можно использовать insertAdjacentHTML, он лучше всего подходит для таких задач.
Решение:
one.insertAdjacentHTML('afterend', '<li>2</li><li>3</li>');
Вот таблица:
В ней может быть больше строк.
Напишите код для сортировки по столбцу "name".
Решение короткое, но может показаться немного сложным, поэтому здесь я предоставлю подробные комментарии:
let sortedRows = Array.from(table.rows)
.slice(1)
.sort((rowA, rowB) => rowA.cells[0].innerHTML > rowB.cells[0].innerHTML ? 1 : -1);
table.tBodies[0].append(...sortedRows);
-
Получим все
<tr>, какtable.querySelectorAll('tr'), затем сделаем массив из них, потому что нам понадобятся методы массива. -
Первый TR (
table.rows[0]) – это заголовок таблицы, поэтому мы берём.slice(1). -
Затем отсортируем их по содержимому в первом
<td>(по имени). -
Теперь вставим узлы в правильном порядке
.append(...sortedRows).Таблицы всегда имеют неявный элемент
<tbody>, поэтому нам нужно получить его и вставить в него: простойtable.append(...)потерпит неудачу.Обратите внимание: нам не нужно их удалять, просто «вставляем их заново», они автоматически покинут старое место.
- © 2007—2026 Илья Кантор
- о проекте
- связаться с нами
- пользовательское соглашение
- политика конфиденциальности
Комментарии
<code>, для нескольких строк кода — тег<pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)