Новый тег <template>: введение стандарта шаблонизации на стороне клиента

Введение

Понятие шаблонизации в веб-разработке не является чем-то новым. Более того, серверные языки шаблонов и шаблонизаторы вроде Django (Python), ERB/Haml (Ruby), и Smarty (PHP) существуют уже далеко не первый день. Последние несколько лет можно наблюдать волну возникновения MVC-фреймворков. Они все немного отличаются друг от друга, но в большинстве своем в их основе лежит общий механизм воспроизведения слоя представления: шаблоны.

Посмотрим правде в глаза. Шаблоны — это прекрасно. Не стесняйтесь, порасспрашивайте мнение коллег на их счет. Даже само определение шаблона оставляет тёплые и приятные чувства:

шаблон (сущ.) — документ или файл с заданным форматом, который используется в качестве отправной точки для определённой цели, чтобы избежать необходимости воссоздания формата при повторном использовании.

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

Спецификация для HTML-шаблонов от W3C должна заполнить этот пробел. В ней определён новый элемент <template>, который является реализацией стандарта шаблонизации для DOM на стороне клиента. Шаблоны позволяют объявлять фрагменты разметки, которые парсятся как HTML, игнорируются при загрузке страницы, но могут быть инстанциированы позже. Цитата от Рафаеля Вайнштайна (Rafael Weinstein) (автора спецификации):

«Они обозначают место, куда можно поместить большой кусок HTML, который вы хотите оградить от какого-либо влияния со стороны браузера…какой бы ни была причина для этого»

Как определить поддерживается ли элемент?

Для выявления поддержки <template>, создайте объект DOM и проверьте наличие свойства .content:

function supportsTemplate() {
  return 'content' in document.createElement('template');
}

if (supportsTemplate()) {
  // Всё в норме.
} else {
  // Используйте старые приёмы или библиотеки шаблонизации 
}

Объявление содержимого шаблона

Элемент <template> представляет шаблон. В него помещено «содержимое шаблона»; по сути инертные куски DOM, которые можно использовать многократно. Шаблоны можно рассматривать как фрагменты скаффолдинга, которые можно многократно использовать в приложении.

Чтобы создать шаблонный контент, напишите код разметки и оберните его в элемент <template>:

<template id="mytemplate">
  <img src="" alt="красивая картинка">
  <div class="comment"></div>
</template>

Наблюдательный читатель должно быть заметил что изображение пустое. Это нас вполне устраивает и было сделано преднамеренно. Мы не получим ошибку 404 или ошибки в консоли потому, что ссылка на изображение битая, так как изображение не будет вызвано при загрузке страницы. Позже можно динамически сгенерировать URL-адрес изображения. Читайте основные принципы.

Основные принципы

Помещение содержимого в <template> даёт нам несколько важных свойств:

  1. Содержимое <template> фактически инертно, пока его не активировать. По сути, соответствующая разметка спрятана и не воспроизводится.
  2. Содержимое шаблона не может привести к каким-либо побочным эффектам. Скрипты не выполняются, изображения не загружаются, аудио не проигрывается… пока шаблон не активирован.
  3. Содержимое шаблона не считается частью страницы. Использование document.getElementById() или querySelector() на странице не возвратит дочерние элементы шаблона.
  4. Шаблоны можно помещать куда угодно: в <head>, <body> или <frameset>; и помещать в них любой тип содержимого, который может располагаться в этих частях страницы. Обратите внимание что «куда угодно» значит что <template> можно без проблем использовать в местах, запрещённых парсером HTML… всех кроме дочерних элементов модели содержимого.

Его также можно поместить в качестве дочернего элемента <table> или <select>:

<table>
<tr>
  <template id="cells-to-repeat">
    <td>какое-то содержимое</td>
  </template>
</tr>
</table>

Активация шаблона

Чтобы использовать шаблон, нужно его активировать. Иначе его содержимое не будет воспроизводиться. Наиболее простой способ — это создать глубокую копию его содержимого .content используя cloneNode(). .content — это неизменимое свойство, которое обозначает фрагмент документа с содержимым шаблона.

var t = document.querySelector('#mytemplate');
// Во время выполнения заполняем src.
t.content.querySelector('img').src = 'logo.png';
document.body.appendChild(t.content.cloneNode(true));

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

Демо

Пример: инертный скрипт

В этом примере продемонстрировано бездействие содержимого шаблона. <script> выполняется только после нажатия на кнопку и извлечения шаблона.

<button onclick="useIt()">Нажми на меня</button>
<div id="container"></div>
<script>
  function useIt() {
    var content = document.querySelector('template').content;
    // Обновление чего-нибудь в DOM шаблона.
    var span = content.querySelector('span');
    span.textContent = parseInt(span.textContent) + 1;
    document.querySelector('#container').appendChild(
        content.cloneNode(true));
  }
</script>

<template>
  <div>Количество раз, которое использован шаблон: <span>0</span></div>
  <script>alert('Спасибо!')</script>
</template>

Пример: Создание теневого дерева из шаблона

Большинство разработчиков прикрепляет теневое дерево к ведущему элементу изменяя строку разметки через .innerHTML:

<div id="host"></div>
<script>
  var shadow = document.querySelector('#host').webkitCreateShadowRoot();
  shadow.innerHTML = '<span>Ведущий элемент</span>';
</script>

Проблема такого подхода состоит в том, что чем сложнее становится ваш теневой DOM, тем чаще вам приходится прибегать к конкатенации строк. Он не масштабируется, очень быстро получается путаница, все в печали. Благодаря именно таким подходам возник межсайтовый скриптинг! <template> приходит на помощь.

Более разумным было бы напрямую присоединять содержимое шаблона к корневому элементу теневого дерева:

<template>
<style>
  @host {
    * {
      background: #f8f8f8;
      padding: 10px;
      -webkit-transition: all 400ms ease-in-out;
      box-sizing: border-box;
      border-radius: 5px;
      width: 450px;
      max-width: 100%;
    } 
    *:hover {
      background: #ccc;
    }
  }
  div {
    position: relative;
  }
  header {
    padding: 5px;
    border-bottom: 1px solid #aaa;
  }
  h3 {
    margin: 0 !important;
  }
  textarea {
    font-family: inherit;
    width: 100%;
    height: 100px;
    box-sizing: border-box;
    border: 1px solid #aaa;
  }
  footer {
    position: absolute;
    bottom: 10px;
    right: 5px;
  }
</style>
<div>
  <header>
    <h3>Добавление комментария</h3>
  </header>
  <content select="p"></content>
  <textarea></textarea>
  <footer>
    <button>Опубликовать</button>
  </footer>
</div>
</template>

<div id="host">
  <p>Здесь должны быть инструкции</p>
</div>

<script>
  var shadow = document.querySelector('#host').webkitCreateShadowRoot();
  shadow.appendChild(document.querySelector('template').content);
</script>

Инструкции для пользователя

Нюансы

Вот несколько нюансов, с которыми я столкнулся используя <template> в полевых условиях:

Например:

<template>
  <ul>
    <template>
      <li>Всякая всячина</li>
    </template>
  </ul>
</template>

Активация внешнего шаблона не означает активацию внутренних. То есть, во вложенных шаблонах дочерние шаблоны должны быть активированы вручную.

Путь к стандарту

Не стоит забывать с чего всё начиналось. Путь к стандартизации HTML-шаблонов был долгим. На протяжении многих лет мы придумывали ловкие способы создания шаблонов многократного использования. Ниже представлены два из них, с которыми столкнулся я. Они представлены в этой статье для сравнения.

Метод 1: Скрытый DOM

Один из подходов, который использовался разработчиками продолжительное время предусматривает создание «скрытого» DOM, который не отображается благодаря атрибуту hidden или display:none.

<div id="mytemplate" hidden>
  <img src="logo.png">
  <div class="comment"></div>
</div>

Хотя этот приём работает, у него есть ряд недостатков. Вот краткий обзор этого приёма:

Метод 2: Перегрузка скрипта

Ещё один приём предусматривает перегрузку <script> и управление его содержимым как строкой. Первым кто так сделал был Джон Резиг (John Resig), представивший в 2008 году свой микро-шаблонизатор. Сегодня их существует множество, в том числе шаблонизаторы нового поколения вроде handlebars.js.

Пример:

<script id="mytemplate" type="text/x-handlebars-template">
  <img src="logo.png">
  <div class="comment"></div>
</script>

Обзор этого приёма:

Заключение

Помните как упростилась работа с DOM благодаря jQuery? В результате в платформу был добавлен querySelector()/querySelectorAll(). Безусловная победа, не так ли? Благодаря этой библиотеке обращения к DOM с помощью CSS-селекторов стали общепринятыми и затем были включены в стандарты. Так происходит не всегда, но я обожаю такие случаи.

Думаю с <template> дело обстоит так же. Он стандартизирует шаблонизацию на стороне клиента, и что более важно, исключает необходимость в сумасшедших трюках, которые мы использовали в 2008 году. На мой взгляд привнесение в процесс веб-разработки большей доли здравомыслия, возможностей поддержки кода и большей функциональности — это в любом случае хорошо.

Дополнительные материалы