Добавление и удаление узлов

Изменение DOM – ключ к созданию «живых» страниц.

В этой главе мы рассмотрим, как создавать новые элементы «на лету» и заполнять их данными.

Пример: показ сообщения

В качестве примера рассмотрим добавление сообщения на страницу, чтобы оно было оформленно красивее чем обычный alert.

HTML-код для сообщения:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<div class="alert">
  <strong>Ура!</strong> Вы прочитали это важное сообщение.
</div>

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

Для создания элементов используются следующие методы:

document.createElement(tag)

Создает новый элемент с указанным тегом:

var div = document.createElement('div');

document.createTextNode(text)

Создает новый *текстовый* узел с данным текстом:
var textElem = document.createTextNode('Тут был я');

Создание сообщения

В нашем случае мы хотим сделать DOM-элемент div, дать ему классы и заполнить текстом:

var div = document.createElement('div');
div.className = "alert alert-success";
div.innerHTML = "<strong>Ура!</strong> Вы прочитали это важное сообщение.";

После этого кода у нас есть готовый DOM-элемент. Пока что он присвоен в переменную div, но не виден, так как никак не связан со страницей.

Добавление элемента: appendChild, insertBefore

Чтобы DOM-узел был показан на странице, его необходимо вставить в document.

Для этого первым делом нужно решить, куда мы будем его вставлять. Предположим, что мы решили, что вставлять будем в некий элемент parentElem, например var parentElem = document.body.

Для вставки внутрь parentElem есть следующие методы:

parentElem.appendChild(elem)

Добавляет elem в конец дочерних элементов parentElem.

Следующий пример добавляет новый элемент в конец <ol>:

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  var newLi = document.createElement('li');
  newLi.innerHTML = 'Привет, мир!';

  list.appendChild(newLi);
</script>
parentElem.insertBefore(elem, nextSibling)

Вставляет elem в коллекцию детей parentElem, перед элементом nextSibling.

Следующий код вставляет новый элемент перед вторым <li>:

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>
<script>
  var newLi = document.createElement('li');
  newLi.innerHTML = 'Привет, мир!';

  list.insertBefore(newLi, list.children[1]);
</script>

Для вставки элемента в начало достаточно указать, что вставлять будем перед первым потомком:

list.insertBefore(newLi, list.firstChild);

У читателя, который посмотрит на этот код внимательно, наверняка возникнет вопрос: «А что, если list вообще пустой, в этом случае ведь list.firstChild = null, произойдёт ли вставка?»

Ответ – да, произойдёт.

Дело в том, что если вторым аргументом указать null, то insertBefore сработает как appendChild:

parentElem.insertBefore(elem, null);
// то же, что и:
parentElem.appendChild(elem)

Так что insertBefore универсален.

На заметку:

Все методы вставки возвращают вставленный узел.

Например, parentElem.appendChild(elem) возвращает elem.

Пример использования

Добавим сообщение в конец <body>:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<body>
  <h3>Моя страница</h3>
</body>

<script>
  var div = document.createElement('div');
  div.className = "alert alert-success";
  div.innerHTML = "<strong>Ура!</strong> Вы прочитали это важное сообщение.";

  document.body.appendChild(div);
</script>

…А теперь – в начало <body>:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<body>
  <h3>Моя страница</h3>
</body>

<script>
  var div = document.createElement('div');
  div.className = "alert alert-success";
  div.innerHTML = "<strong>Ура!</strong> Вы прочитали это важное сообщение.";

  document.body.insertBefore(div, document.body.firstChild);
</script>

Клонирование узлов: cloneNode

А как бы вставить второе похожее сообщение?

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

Вызов elem.cloneNode(true) создаст «глубокую» копию элемента – вместе с атрибутами, включая подэлементы. Если же вызвать с аргументом false, то копия будет сделана без дочерних элементов. Это нужно гораздо реже.

Пример со вставкой копии сообщения:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<body>
  <h3>Моя страница</h3>
</body>

<script>
  var div = document.createElement('div');
  div.className = "alert alert-success";
  div.innerHTML = "<strong>Ура!</strong> Вы прочитали это важное сообщение.";

  document.body.insertBefore(div, document.body.firstChild);

  // создать копию узла
  var div2 = div.cloneNode(true);
  // копию можно подправить
  div2.querySelector('strong').innerHTML = 'Супер!';
  // вставим её после текущего сообщения
  div.parentNode.insertBefore(div2, div.nextSibling);
</script>

Обратите внимание на последнюю строку, которая вставляет div2 после div:

div.parentNode.insertBefore(div2, div.nextSibling);
  1. Для вставки нам нужен будущий родитель. Мы, возможно, не знаем, где точно находится div (или не хотим зависеть от того, где он), но если нужно вставить рядом с div, то родителем определённо будет div.parentNode.
  2. Мы хотели бы вставить после div, но метода insertAfter нет, есть только insertBefore, поэтому вставляем перед его правым соседом div.nextSibling.

Удаление узлов: removeChild

Для удаления узла есть два метода:

parentElem.removeChild(elem)
Удаляет elem из списка детей parentElem.
parentElem.replaceChild(newElem, elem)
Среди детей parentElem удаляет elem и вставляет на его место newElem.

Оба этих метода возвращают удаленный узел, то есть elem. Если нужно, его можно вставить в другое место DOM тут же или в будущем.

На заметку:

Если вы хотите переместить элемент на новое место – не нужно его удалять со старого.

Все методы вставки автоматически удаляют вставляемый элемент со старого места.

Конечно же, это очень удобно.

Например, поменяем элементы местами:

<div>Первый</div>
<div>Второй</div>
<script>
  var first = document.body.children[0];
  var last = document.body.children[1];

  // нет необходимости в предварительном removeChild(last)
  document.body.insertBefore(last, first); // поменять местами
</script>
Метод remove

В современном стандарте есть также метод elem.remove(), который удаляет элемент напрямую, не требуя ссылки на родителя. Это зачастую удобнее, чем removeChild.

Он поддерживается во всех современных браузерах, кроме IE11-. Впрочем, легко подключить или даже сделать полифилл.

Удаление сообщения

Сделаем так, что через секунду сообщение пропадёт:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<body>
  <h3>Сообщение пропадёт через секунду</h3>
</body>

<script>
  var div = document.createElement('div');
  div.className = "alert alert-success";
  div.innerHTML = "<strong>Ура!</strong> Вы прочитали это важное сообщение.";

  document.body.appendChild(div);

  setTimeout(function() {
    div.parentNode.removeChild(div);
  }, 1000);
</script>

Текстовые узлы для вставки текста

При работе с сообщением мы использовали только узлы-элементы и innerHTML.

Но и текстовые узлы тоже имеют интересную область применения!

Если текст для сообщения нужно показать именно как текст, а не как HTML, то можно обернуть его в текстовый узел.

Например:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<script>
  var div = document.createElement('div');
  div.className = "alert alert-success";
  document.body.appendChild(div);

  var text = prompt("Введите текст для сообщения", "Жили были <a> и <b>!");

  // вставится именно как текст, без HTML-обработки
  div.appendChild(document.createTextNode(text));
</script>

В современных браузерах (кроме IE8-) в качестве альтернативы можно использовать присвоение textContent.

Итого

Методы для создания узлов:

  • document.createElement(tag) – создает элемент
  • document.createTextNode(value) – создает текстовый узел
  • elem.cloneNode(deep) – клонирует элемент, если deep == true, то со всеми потомками, если false – без потомков.

Вставка и удаление узлов:

  • parent.appendChild(elem)
  • parent.insertBefore(elem, nextSibling)
  • parent.removeChild(elem)
  • parent.replaceChild(newElem, elem)

Все эти методы возвращают elem.

Методы для изменения DOM также описаны в спецификации DOM Level 1.

Задачи

важность: 5

Есть пустой узел DOM elem.

Одинаковый ли результат дадут эти скрипты?

Первый:

elem.appendChild(document.createTextNode(text));

Второй:

elem.innerHTML = text;

Если нет – дайте пример значения text, для которого результат разный.

Результат выполнения может быть разный: innerHTML вставит именно HTML, а createTextNode интерпретирует теги как текст.

Запустите следующие примеры, чтобы увидеть разницу:

  • createTextNode создает текст „<b>текст</b>“:

    <div id="elem"></div>
    <script>
      var text = '<b>текст</b>';
    
      elem.appendChild(document.createTextNode(text));
    </script>
  • innerHTML присваивает HTML <b>текст</b>:

    <div id="elem"></div>
    <script>
      var text = '<b>текст</b>';
    
      elem.innerHTML = text;
    </script>
важность: 5

Напишите полифилл для метода remove для старых браузеров.

Вызов elem.remove():

  • Если у elem нет родителя – ничего не делает.
  • Если есть – удаляет элемент из родителя.
<div>Это</div>
<div>Все</div>
<div>Элементы DOM</div>

<script>
  /* ваш код полифилла */

  var elem = document.body.children[0];

  elem.remove(); // <-- вызов должен удалить элемент
</script>

Родителя parentNode можно получить из elem.

Вот так выглядит решение:

<div>Это</div>
<div>Все</div>
<div>Элементы DOM</div>

<script>
  if (!Element.prototype.remove) {
    Element.prototype.remove = function remove() {
      if (this.parentNode) {
        this.parentNode.removeChild(this);
      }
    };
  }

  var elem = document.body.children[0];

  elem.remove();
</script>
важность: 5

Напишите функцию insertAfter(elem, refElem), которая добавит elem после узла refElem.

<div>Это</div>
<div>Элементы</div>

<script>
  var elem = document.createElement('div');
  elem.innerHTML = '<b>Новый элемент</b>';

  function insertAfter(elem, refElem) { /* ваш код */ }

  var body = document.body;

  // вставить elem после первого элемента
  insertAfter(elem, body.firstChild); // <--- должно работать

  // вставить elem за последним элементом
  insertAfter(elem, body.lastChild); // <--- должно работать
</script>

Для того, чтобы добавить элемент после refElem, мы можем, используя insertBefore, вставить его перед refElem.nextSibling.

Но что если nextSibling нет? Это означает, что refElem является последним потомком своего родителя и можем использовать appendChild.

Код:

function insertAfter(elem, refElem) {
  var parent = refElem.parentNode;
  var next = refElem.nextSibling;
  if (next) {
    return parent.insertBefore(elem, next);
  } else {
    return parent.appendChild(elem);
  }
}

Но код может быть гораздо короче, если вспомнить, что insertBefore со вторым аргументом null работает как appendChild:

function insertAfter(elem, refElem) {
  return refElem.parentNode.insertBefore(elem, refElem.nextSibling);
}

Если нет nextSibling, то второй аргумент insertBefore становится null и тогда insertBefore(elem, null) осуществит вставку в конец, как и требуется.

В решении нет проверки на существование refElem.parentNode, поскольку вставка после элемента без родителя – уже ошибка, пусть она возникнет в функции, это нормально.

важность: 5

Напишите функцию removeChildren, которая удаляет всех потомков элемента.

<table id="table">
  <tr>
    <td>Это</td>
    <td>Все</td>
    <td>Элементы DOM</td>
  </tr>
</table>

<ol id="ol">
  <li>Вася</li>
  <li>Петя</li>
  <li>Маша</li>
  <li>Даша</li>
</ol>

<script>
  function removeChildren(elem) { /* ваш код */ }

  removeChildren(table); // очищает таблицу
  removeChildren(ol); // очищает список
</script>

Неправильное решение

Для начала рассмотрим забавный пример того, как делать не надо:

function removeChildren(elem) {
  for (var k = 0; k < elem.childNodes.length; k++) {
    elem.removeChild(elem.childNodes[k]);
  }
}

Если вы попробуете это на практике, то увидите, что это не сработает.

Не сработает потому, что коллекция childNodes всегда начинается с индекса 0 и автоматически обновляется, когда первый потомок удален(т.е. тот, что был вторым, станет первым). А переменная k в цикле всё время увеличивается, поэтому такой цикл пропустит половину узлов.

Решение через DOM

Правильное решение:

function removeChildren(elem) {
  while (elem.lastChild) {
    elem.removeChild(elem.lastChild);
  }
}

Альтернатива через innerHTML

Можно и просто обнулить содержимое через innerHTML:

function removeChildren(elem) {
  elem.innerHTML = '';
}

Это не будет работать в IE8- для таблиц, так как на большинстве табличных элементов (кроме ячеек TH/TD) в старых IE запрещено менять innerHTML.

Впрочем, можно завернуть innerHTML в try/catch:

function removeChildren(elem) {
  try {
    elem.innerHTML = '';
  } catch (e) {
    while (elem.firstChild) {
      elem.removeChild(elem.firstChild);
    }
  }
}
важность: 1

Запустите этот пример. Почему вызов removeChild не удалил текст "aaa"?

<table>
  aaa
  <tr>
    <td>Test</td>
  </tr>
</table>

<script>
  var table = document.body.children[0];

  alert( table ); // таблица, пока всё правильно

  document.body.removeChild(table);
  // почему в документе остался текст?
</script>

HTML в задаче некорректен. В этом всё дело. И вопрос легко решится, если открыть отладчик.

В нём видно, что браузер поместил текст aaa перед таблицей. Поэтому он и остался в документе.

Вообще, в стандарте HTML5 описано, как браузеру обрабатывать некорректный HTML, так что такое действие браузера является правильным.

важность: 4

Напишите интерфейс для создания списка.

Для каждого пункта:

  1. Запрашивайте содержимое пункта у пользователя с помощью prompt.
  2. Создавайте пункт и добавляйте его к UL.
  3. Процесс прерывается, когда пользователь нажимает ESC или вводит пустую строку.

Все элементы должны создаваться динамически.

Если посетитель вводит теги – пусть в списке они показываются как обычный текст.

Демо в новом окне

Делаем цикл, пока посетитель что-то вводит – добавляет <li>.

Содержимое в <li> присваиваем через document.createTextNode, чтобы правильно работали <, > и т.д.

Открыть решение в песочнице.

важность: 5

Напишите функцию, которая создаёт вложенный список UL/LI (дерево) из объекта.

Например:

var data = {
  "Рыбы": {
    "Форель": {},
    "Щука": {}
  },

  "Деревья": {
    "Хвойные": {
      "Лиственница": {},
      "Ель": {}
    },
    "Цветковые": {
      "Берёза": {},
      "Тополь": {}
    }
  }
};

Синтаксис:

var container = document.getElementById('container');
createTree(container, data); // создаёт

Результат (дерево):

Выберите один из двух способов решения этой задачи:

  1. Создать строку, а затем присвоить через container.innerHTML.
  2. Создавать узлы через методы DOM.

Если получится – сделайте оба.

P.S. Желательно, чтобы в дереве не было лишних элементов, в частности – пустых <ul></ul> на нижнем уровне.

Открыть песочницу для задачи.

Решения через рекурсию.

  1. Через innerHTML.
  2. Через DOM.
важность: 5

Есть дерево, организованное в виде вложенных списков <ul>/<li>.

Напишите код, который добавит каждому элементу списка <li> количество вложенных в него элементов. Узлы нижнего уровня, без детей – пропускайте.

Результат:

Открыть песочницу для задачи.

Подсказки

  1. Получить количество вложенных узлов можно через elem.getElementsByTagName('*').length.
  2. Текст в начале <li> доступен как li.firstChild, его содержимое – li.firstChild.data.
важность: 4

Напишите функцию, которая умеет генерировать календарь для заданной пары (месяц, год).

Календарь должен быть таблицей, где каждый день – это TD. У таблицы должен быть заголовок с названиями дней недели, каждый день – TH.

Синтаксис: createCalendar(id, year, month).

Такой вызов должен генерировать текст для календаря месяца month в году year, а затем помещать его внутрь элемента с указанным id.

Например: createCalendar("cal", 2012, 9) сгенерирует в <div id=„cal“></div> следующий календарь:

P.S. Достаточно сгенерировать календарь, кликабельным его делать не нужно.

Открыть песочницу для задачи.

Для решения задачи сгенерируем таблицу в виде строки: "<table>...</table>", а затем присвоим в innerHTML.

Алгоритм:

  1. Создать объект даты d = new Date(year, month-1). Это первый день месяца month (с учетом того, что месяцы в JS начинаются от 0, а не от 1).
  2. Ячейки первого ряда пустые от начала и до дня недели d.getDay(), с которого начинается месяц. Создадим их.
  3. Увеличиваем день в d на единицу: d.setDate(d.getDate()+1), и добавляем в календарь очередную ячейку, пока не достигли следующего месяца. При этом последний день недели означает вставку перевода строки «</tr><tr>».
  4. При необходимости, если календарь окончился не на воскресенье – добавить пустые TD в таблицу, чтобы было все ровно.

Открыть решение в песочнице.

важность: 4

Создайте цветные часики как в примере ниже:

Открыть песочницу для задачи.

Для начала, придумаем подходящую 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, вызываемая каждую секунду:

function update() {
  var clock = document.getElementById('clock');

  var date = new Date(); // (*)

  var hours = date.getHours();
  if (hours < 10) hours = '0' + hours;
  clock.children[0].innerHTML = hours;

  var minutes = date.getMinutes();
  if (minutes < 10) minutes = '0' + minutes;
  clock.children[1].innerHTML = minutes;

  var seconds = date.getSeconds();
  if (seconds < 10) seconds = '0' + seconds;
  clock.children[2].innerHTML = seconds;
}

В строке (*) каждый раз мы получаем текущую дату. Мы должны это сделать, несмотря на то, что, казалось бы, могли бы просто увеличивать счетчик каждую секунду.

На самом деле мы не можем опираться на счетчик для вычисления даты, т.к. setInterval не гарантирует точную задержку. Если в другом участке кода будет вызван alert, то часы остановятся, как и любые счетчики.

Функция clockStart для запуска часов:

var timerId;

function clockStart() { // запустить часы
  timerId = setInterval(update, 1000);
  update(); // (*)
}

function clockStop() {
  clearInterval(timerId);
  timerId = null;
}

Обратите внимание, что вызов update не только запланирован, но и тут же производится в строке (*). Иначе посетителю пришлось бы ждать до первого выполнения setInterval, то есть целую секунду.

Открыть решение в песочнице.

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.