renovations

This commit is contained in:
Ilya Kantor 2015-02-19 21:28:10 +03:00
parent 718ab327f9
commit e706693c7e
12 changed files with 246 additions and 257 deletions

View file

@ -22,12 +22,14 @@
```html
<ul class="menu">
<li class="item"><a href="#">Главная</a></li>
<li class="vertical-splitter"></li>
<li class="item"><a href="#">Товары</a></li>
<li class="item"><a href="#">Фотографии</a></li>
<li class="item"><a href="#">Контакты</a></li>
<li class="menu__item"><a href="#">Главная</a></li>
<li class="menu__vertical-splitter"></li>
<li class="menu__item"><a href="#">Товары</a></li>
<li class="menu__item"><a href="#">Фотографии</a></li>
<li class="menu__item"><a href="#">Контакты</a></li>
</ul>
```
[edit src="solution"]Полное меню со стилями[/edit]
Дополнительно, классы помечены префиксом компонента, на тот случай, если в заголовках появится произвольный HTML.

View file

@ -8,15 +8,13 @@
padding: 0;
}
.menu li {
.menu__item {
list-style: none;
float: left;
display: inline-block;
padding: 6px;
margin: 0 2px;
}
.menu li.item {
border: 1px solid gray;
border-radius: 10px;
@ -25,16 +23,21 @@
background: #FFF5EE;
}
.menu li.vertical-splitter:before {
.menu__vertical-splitter {
list-style: none;
display: inline-block;
}
.menu__vertical-splitter:before {
content: "|";
}
.menu .item a {
.menu__item a {
color: black;
text-decoration: none;
}
.menu .item a:hover {
.menu__item a:hover {
text-decoration: underline;
}
@ -43,11 +46,11 @@
<body>
<ul class="menu">
<li class="item"><a href="#">Главная</a></li>
<li class="vertical-splitter"></li>
<li class="item"><a href="#">Товары</a></li>
<li class="item"><a href="#">Фотографии</a></li>
<li class="item"><a href="#">Контакты</a></li>
<li class="menu__item"><a href="#">Главная</a></li>
<li class="menu__vertical-splitter"></li>
<li class="menu__item"><a href="#">Товары</a></li>
<li class="menu__item"><a href="#">Фотографии</a></li>
<li class="menu__item"><a href="#">Контакты</a></li>
</ul>
</body>

View file

@ -44,6 +44,6 @@
[iframe src="source" border=1 height=50 edit link]
Что делает эту вёрстку несемантичной? Найдите 3 ошибки.
Что делает эту вёрстку несемантичной? Найдите 3 ошибки (или больше).
Как бы вы сверстали меню правильно?

View file

@ -1,29 +0,0 @@
<ul>
<li>Самая главная ошибка: классы без префиксов. Это означает, что если внутри содержимого будет что-либо с классом `.headers`, то стили оформят это как заголовок таба. Что, конечно же, будет неверно.</li>
<li>Класс `selected`, соответствующий "текущей" выбранной вкладке, находится на ссылке:
```html
<li><a href="#tabs-3" class="selected">Открытая вкладка</a></li>
```
...Но состояние "выбранности" относится к заголовку `LI` целиком. Для его нормального отображения класс должен быть на `LI`.</li>
<li>В использовании `.tabs > div` особо криминала нет, но нужно учесть, что добавить `DIV` с другим функционалом на этот уровень будет затруднительно. Иначе говоря, такая вёрстка усложняет расширение виджета. Лучше использовать для вкладки класс с префиксом `<div class="tabs-tab">` и, соответственно, селектор `.tabs-tab`.</li>
</ul>
Правильный вариант:
```html
<div id="tabs" class="tabs">
<ul class="*!*tabs-headers*/!*">
<li><a href="#tabs-1">Вкладка 1</a></li>
<li><a href="#tabs-2">Вкладка 2</a></li>
<li *!*class="tabs-selected"*/!*><a href="#tabs-3">Открытая..</a></li>
</ul>
<div *!*class="tabs-tab"*/!* id="tabs-1">Содержимое...</div>
<div *!*class="tabs-tab"*/!* id="tabs-2">Содержимое...</div>
<div *!*class="tabs-tab"*/!* id="tabs-3" class="*!*tabs-selected*/!*">
Посетитель видит содержимое третьей вкладки.
</div>
</div>
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -1,44 +0,0 @@
# Ошибки в вёрстке
[importance 5]
Посмотрите на вёрстку "табов" (вкладок). Какие ошибки вы в ней видите? Как правильно?
```html
<div id="tabs" class="tabs">
<ul class="headers">
<li><a href="#tabs-1">Вкладка 1</a></li>
<li><a href="#tabs-2">Вкладка 2</a></li>
<li><a href="#tabs-3" class="selected">Открытая вкладка</a></li>
</ul>
<div id="tabs-1">Содержимое в первой вкладке.</div>
<div id="tabs-2">Содержимое во второй вкладке.</div>
<div id="tabs-3" class="selected">
Посетитель видит содержимое третьей вкладки.
</div>
</div>
```
```css
.tabs .headers {
/* стиль для заголовков вкладок */
}
.tabs .headers .selected {
/* стиль для заголовка выбранной вкладки */
}
.tabs > div {
/* стиль для обычной вкладки */
}
.tabs > div.selected {
/* стиль для выбранной вкладки */
}
```
Примерный внешний вид:
<img src="tabs-example.png">

View file

@ -48,7 +48,7 @@ HTML-разметка и названия CSS-классов должны отр
</div>
```
**Семантическая верстка упрощает поддержку и развитие CSS, упрощает взаимодействие между членами команды.**
Семантическая верстка упрощает поддержку и развитие CSS, упрощает взаимодействие между членами команды.
Такая верстка удобна для организации JS-кода. В коде мы просто ставим нужный класс, остальное делает CSS.
@ -69,21 +69,15 @@ HTML-разметка и названия CSS-классов должны отр
</div>
```
Ещё пример -- индикатор загрузки:
Или, к примеру, разметка для индикатора загрузки может выглядеть так:
```html
<div class="loader">
<div class="indicator *!*loading*/!*">
<span class="progress">Тут показывается прогресс</span>
</div>
```
Состояние индикатора может быть "в процессе" (loading) или "загрузка завершена" (complete). С точки зрения оформления оно может влиять только на показ внутреннего `span`, но ставить его нужно всё равно на внешний элемент, ведь это -- состояние всего компонента:
```html
<div class="loader *!*loading*/!*">
<span class="progress">Тут показывается прогресс</span>
</div>
```
Состояние индикатора может быть "в процессе" (loading) или "загрузка завершена" (complete). С точки зрения оформления оно может влиять только на показ внутреннего `span`, но ставить его нужно всё равно на внешний элемент, ведь это -- состояние всего компонента.
Из примеров выше можно подумать, что классы, описывающие состояние, всегда ставятся на корневой элемент. Но это не так.
@ -103,80 +97,104 @@ HTML-разметка и названия CSS-классов должны отр
</ul>
```
На практике, даже если в начале разработки поставить класс не там -- то, при правильном понимании CSS, рано или поздно он всё равно переместится куда надо, поскольку стилизация открытого/закрытого меню касается также и заголовка.
## Префиксы компонента у классов
Но оптимальнее -- сразу ставить его на правильное место.
## Префиксы у классов
Посмотрите, пожалуйста, вёрстку для виджета диалогового окна.
Рассмотрим пример вёрстки "диалогового окна":
```html
<!--+ autorun height=100 -->
<div class="dialog">
<h2 class="title">Заголовок</h2>
<div class="content">
Содержимое. Имена классов в этой вёрстке опасны.
HTML-содержимое.
</div>
<div class="close">Закрыть</div>
</div>
<style>
.dialog .title { стиль заголовка }
.dialog .content { стиль содержимого окна }
.dialog {
background: lightgreen;
border: lime 2px solid;
border-radius: 10px;
padding: 4px;
position: relative;
}
*!*
.dialog .title {
margin: 0;
font-size: 24px;
color: darkgreen;
}
*/!*
.dialog .content {
padding: 10px 0 0 0;
}
.dialog .close {
position: absolute;
right: 4px;
top: 4px;
font-size: 10px;
}
</style>
```
В этой вёрстке есть серьёзная проблема, которая появится, если в содержимом окна будет элемент с классом `.title`:
Диалоговое окно может иметь любое HTML-содержимое.
```html
<div class="dialog">
<h1 class="*!*title*/!*">Заголовок</h1>
<div class="content">
А что будет, если в этом содержимом окажется меню -- да-да, то самое, которое рассмотрели выше, со `<span class="title">` ?
*!*
<h2 class="*!*title*/!*">Привет!</h2>
... текст диалога ...
*/!*
Правило `.dialog .title` применяется ко всем `.title` внутри `.dialog`, а значит -- и к нашему меню тоже. Будет конфликт стилей с непредсказуемыми последствиями.
</div>
</div>
```
Такое вполне возможно, ведь диалоговое окно может иметь любое содержимое.
**В этом случае CSS-правило `.dialog .title` будет применено и к `<h2 class="title">` в содержимом с непредсказуемыми последствиями.**
Конечно, можно попытаться бороться с этим. Например, нейтрализовать его действие, добавив дополнительное правило `.dialog .content .title`, но это скорее "заплатка", нежели полноценное решение проблемы.
Ещё один вариант -- жёстко задать вложенность. А именно, использовать класс `.dialog > .title`. Это сработает в данном конкретном примере, но как быть в тех местах, где нужен более глубокий потомок?
Конечно, можно попытаться бороться с этим. Например, жёстко задать вложенность -- использовать класс `.dialog > .title`. Это сработает в данном конкретном примере, но как быть в тех местах, где между `.dialog` и `.title` есть другие элементы? Длинные цепочки вида `.dialog > ... > .title` страшновато выглядят и делают вёрстку ужасно негибкой. К счастью, есть альтернативный путь.
**Чтобы избежать возможных проблем, все классы внутри виджета начинают с его имени.**
Подходящий вариант:
Здесь имя `dialog`, так что все, относящиеся к диалогу, будем начинать с `dialog__`
Получится так:
```html
<div class="*!*dialog*/!*">
<h1 class="*!*dialog-title*/!*">Заголовок</h1>
<div class="*!*dialog-content*/!*">Содержимое</div>
<h2 class="dialog__title">Заголовок</h2>
<div class="dialog__content">
HTML-содержимое.
</div>
<div class="dialog__close">Закрыть</div>
</div>
<style>
.dialog-title { стиль загловка }
.dialog-content { стиль содержимого окна }
.dialog { ... }
.dialog__title { стиль заголовка }
.dialog__content { стиль содержимого }
...
</style>
```
В этом случае внутрь `.dialog-content` можно смело помещать другие компоненты со своими классами `..-title`, `..-content` и т.п.
Здесь двойное подчёркивание `__` служит "стандартным" разделителем. Можно выбрать и другой разделитель, но при этом стоит иметь в виду, что иногда имя класса может состоять из нескольких слов. Например `title-picture`. С двойным подчёркиванием: `dialog__title-picture`, очень наглядно видно где что.
Кроме всего прочего, обработка такого CSS будет чуть-чуть быстрее ;) Так как один класс вместо каскада.
Есть ещё одно полезное правило, которое заключается в том, что стили должны вешаться на класс, а не на тег. То есть, не `h2 { ... }`, а `.dialog__title { ... }`, где `.dialog__title` -- класс на соответствующем заголовке.
[smart header="Когда префиксы не нужны?"]
Префиксы делают названия классов длиннее, поэтому иногда не хочется их ставить.
Это позволяет и избежать конфликтов на вложенных `h2`, и использовать всегда те теги, которые имеют правильный смысл, не оглядываясь на встроенные стили (которые можно обнулить своими).
Без них можно обойтись в тех случаях, когда внутри элемента заведомо не будет произвольного HTML и других компонент. То есть когда конфликты заведомо исключены.
С другой стороны, требования имеют свойство расти. Компоненты зачастую вставляются туда, где их не предполагалось. Ваш виджет, написанный для одной задачи или проекта, может быть потом использован совсем в другом месте, где потребуются вложенные компоненты. И тогда заранее предусмотренные префиксы сослужат хорошую службу.
[smart header="Без фанатизма"]
На практике из этих правил зачастую делают исключения. Можно "вешать" стили на теги и использовать CSS-каскады без префиксов, если мы при этом твёрдо понимаем, что конфликты заведомо исключены.
Например, когда мы точно знаем, что никакого произвольного HTML внутри элемента (или внутри данного поддерева DOM) не будет.
[/smart]
## БЭМ
Описанное выше правило именования элементов является частью более общей концепции "БЭМ", которая разработана в Яндексе.
БЭМ предлагает способ организации HTML/CSS/JS в виде независимых "блоков" -- компонент, которые можно легко перемещать по файловой системе и между проектами.
Можно как взять часть идеологии, например систему именования классов, так и полностью перейти на инструментарий БЭМ, который даёт инструменты сборки для HTML/JS/CSS, описанных по БЭМ-методу.
Более подробное описание основ БЭМ можно почитать в статье [](https://ru.bem.info/articles/bem-for-small-projects/), а о системе вообще -- на сайте [](http://ru.bem.info).
## Итого
<ul>
@ -186,8 +204,9 @@ HTML-разметка и названия CSS-классов должны отр
Это не всегда строго необходимо, но позволяет избежать проблем в случаях, когда компонент может содержать произвольный DOM, как например диалоговое окно с произвольным HTML-текстом.
Использование `.dialog-title` вместо `.dialog .title` гарантирует, что CSS не применится по ошибке к какому-нибудь другому `.title` внутри диалога.
Использование `.dialog__title` вместо `.dialog .title` гарантирует, что CSS не применится по ошибке к какому-нибудь другому `.title` внутри диалога.
</li>
</ul>

View file

@ -1,9 +1,10 @@
# Шаблонизатор LoDash
В этой главе мы рассмотрим *шаблонизацию* -- удобный способ генерации HTML-структуры для виджета и её реализацию при помощи библиотеки [LoDash](http://lodash.com).
В этой главе мы рассмотрим *шаблонизацию* -- удобный способ генерации HTML по "шаблону" и данным.
[cut]
Все виджеты можно условно разделить на три группы по генерации DOM-структуры:
Все виджеты можно условно разделить на три группы по генерации DOM:
<ol>
<li>**Получают готовый HTML/DOM и "оживляют" его.**
@ -15,15 +16,15 @@
В более сложных интерфейсах компоненты генерируют свой DOM на основе данных, полученных с сервера или из других источников.</li>
<li>**=1+2: должны уметь как "оживить" уже готовый DOM, так и создать свой.**
Бывает и так, что виджет должен уметь и то и другое. Например, так работает Twitter, который при загрузке сразу показывает текущие сообщения (HTML генерируется на сервере), но может динамически добавлять новые.
Бывает и так, что виджет должен уметь и то и другое. Например, так работает сервис сообщений Twitter.
Браузер быстро скачивает HTML и отображает его, посетитель видит текущие сообщения и рад этому. А затем уже подгружается объёмный JavaScript, который умеет загружать сообщения с сервера, создавать их и т.д.
При начальной загрузки сервер генерирует HTML с текущими сообщениями, а JavaScript лишь добавляет им "живости". Далее, когда страница уже загружена, он получает с сервера данные для новых сообщений и генерирует HTML уже динамически.
</li>
</ol>
С первым типом виджетов -- вопросов нет. Добавляем обработчики, и дело сделано.
С первой группой виджетов -- вопросов нет. Добавляем обработчики, и дело сделано.
Интересно начинается со второго типа, и совсем интересно -- с третьим типом.
Интересно начинается со второй группы, и совсем интересно -- с третьей группой.
## Зачем нужны шаблоны?
@ -34,38 +35,51 @@ function Menu(options) {
// ... приведены только методы для генерации DOM ...
function render() {
elem = $('<div class="menu"></div>');
elem.append( $('<span/>', { class: "title", text: options.title }))
elem = document.createElement('div');
elem.className = "menu";
elem.on('mousedown selectstart', false);
var titleElem = document.createElement('span');
elem.appendChild(titleElem);
titleElem.className = "title";
titleElem.textContent = options.title;
elem.onmousedown = function() {
return false;
};
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
}
elem.on('click', '.title', onTitleClick);
}
function renderItems() {
var items = options.items || [];
var list = $('<ul/>');
$.each(items, function(i, item) {
list.append( $('<li>').text(item) );
})
list.appendTo(elem);
var list = document.createElement('ul');
items.forEach(function(item) {
var li = document.createElement('li');
li.textContent = item;
list.appendChild(li);
});
elem.appendChild(list);
}
// ...
}
```
Понятен ли этот код? Очевидно ли, какой HTML он генерирует?
Понятен ли этот код? Очевидно ли, какой HTML он генерируют методы `render`, `renderItems`?
С первого взгляда -- вряд ли. Нужно как минимум внимательно посмотреть и продумать код, чтобы разобраться. Причём это -- с jQuery, если на обычном JavaScript написать, код ещё длиннее и сложнее будет.
С первого взгляда -- вряд ли. Нужно как минимум внимательно посмотреть и продумать код, чтобы разобраться, какая именно DOM-структура создаётся.
...А что, если нужно изменить создаваемый HTML? ...А что, если эта задача досталась не программисту, который написал этот код, а верстальщику, который c HTML/CSS проекта знаком отлично, но этот JS-код видит впервые? Вероятность ошибок при этом зашкаливает за все разумные пределы.
**К счастью, генерацию HTML можно упростить. Для этого воспользуемся шаблонами.**
К счастью, генерацию HTML можно упростить. Для этого воспользуемся библиотекой шаблонизации.
## Шаблон
## Пример шаблона
*Шаблон* -- это заготовка, которая путём подстановки значений (текст сообщения, цена и т.п.) превращается в DOM/HTML.
*Шаблон* -- это строка в специальном формате, которая путём подстановки значений (текст сообщения, цена и т.п.) и выполнения встроенных фрагментов кода превращается в DOM/HTML.
Пример шаблона для меню:
@ -80,14 +94,16 @@ function Menu(options) {
</div>
```
Как видно, это обычный HTML, со специальными вставками `<% ... %>`. Для работы с таким шаблоном используется специальная функция `_.template`, которая предоставляется фреймворком [LoDash](http://lodash.com/docs#template), её синтаксис мы подробно посмотрим далее.
Как видно, это обычный HTML, с вставками вида `<% ... %>`.
Взглянем, каким будет вывод меню с использованием шаблона:
Для работы с таким шаблоном используется специальная функция `_.template`, которая предоставляется фреймворком [LoDash](http://lodash.com/docs#template), её синтаксис мы подробно посмотрим далее.
Пример использования `_.template` для генерации HTML с шаблоном выше:
```js
// шаблон должен быть в переменной tmpl
*!*
// сгенерировать HTML, используя шаблон tmpl с данными title и items
// сгенерировать HTML, используя шаблон tmpl (см. выше)
// с данными title и items
*/!*
var html = _.template(tmpl, {
title: "Сладости",
@ -112,34 +128,34 @@ var html = _.template(tmpl, {
</div>
```
Этот гораздо проще, чем JS-код, не правда ли? Шаблон очень наглядно показывает, что в итоге должно получиться. Шаблоны можно использовать для описания как меню целиком, так и его частей.
Этот гораздо проще, чем JS-код, не правда ли? Шаблон очень наглядно показывает, что в итоге должно получиться. В отличие от кода, в шаблоне первичен текст, а вставок кода обычно мало.
## Шаблонизация через _.template [#tmpl]
Далее мы подробнее познакомимся с `_.template` и синтаксисом шаблонов.
Давайте подробнее познакомимся с `_.template` и синтаксисом шаблонов.
[warn header="Holy war detected!"]
Способов шаблонизации и, в особенности, синтаксисов шаблонов, примерно столько же, сколько способов [поймать льва в пустыне](http://lurkmore.to/%D0%9A%D0%B0%D0%BA_%D0%BF%D0%BE%D0%B9%D0%BC%D0%B0%D1%82%D1%8C_%D0%BB%D1%8C%D0%B2%D0%B0_%D0%B2_%D0%BF%D1%83%D1%81%D1%82%D1%8B%D0%BD%D0%B5). Иначе говоря... много.
Эта глава -- совершенно не место для священных войн на эту тему. Далее будет более полный обзор типов шаблонных систем, применяемых в JavaScript, но начнём мы с `_.template`, поскольку эта "шаблонка" проста, быстра и демонстрирует целый класс шаблонных систем, активно используемых в самых разных JS-проектах.
Эта глава -- совершенно не место для священных войн на эту тему.
Далее будет более полный обзор типов шаблонных систем, применяемых в JavaScript, но начнём мы с `_.template`, поскольку эта функция проста, быстра и демонстрирует приёмы, используемые в целом классе шаблонных систем, активно используемых в самых разных JS-проектах.
[/warn]
### Синтаксис шаблона
## Синтаксис шаблона
Шаблон представляет собой строку со специальными разделителями, которых всего три:
<dl>
<dt>`<% code %>` -- код</dt>
<dd>Код между разделителями `<% ... %>` будет выполнен "как есть"</dd>
<dt>`<%= expr %>` -- для вставки HTML</dt>
<dt>`<%= expr %>` -- для вставки `expr` как HTML</dt>
<dd>Переменная или выражение внутри `<%= ... %>` будет вставлено "как есть". Например: `<%=title %>` вставит значение переменной `title`, а `<%=2+2%>` вставит `4`.</dd>
<dt>`<%- expr %>` -- для вставки текста</dt>
<dt>`<%- expr %>` -- для вставки `expr` как текста</dt>
<dd>Переменная или выражение внутри `<%- ... %>` будет вставлено "как текст", то есть с заменой символов `< > & " '` на соответствующие HTML-entities.
Например, если `expr` содержит текст `<br>`, то при `<%-expr%>` в результат попадёт (в отличие от `<%=expr%>`) не HTML-тег `<br>`, а текст `&lt;br&gt;`.</dd>
Например, если `expr` содержит текст `<br>`, то при `<%-expr%>` в результат попадёт, в отличие от `<%=expr%>`, не HTML-тег `<br>`, а текст `&lt;br&gt;`.</dd>
</dl>
### Функция _.template
## Функция _.template
Для работы с шаблоном в библиотеке [LoDash](https://github.com/bestiejs/lodash) есть функция `_.template(tmpl, data, options)`.
@ -153,26 +169,33 @@ var html = _.template(tmpl, {
<dd>Необязательные настройки, например можно поменять разделители.</dd>
</dl>
Эта функция запускает "сборку" шаблона с объектом `data` и возвращает результат в виде строки.
Эта функция запускает "сборку" шаблона `tmpl` с объектом `data` и возвращает результат в виде строки.
Вот так:
```js
//+ run
// Шаблон*!*
*!*
// Шаблон
*/!*
var tmpl = '<span class="title"><%=title%></span>';
// Данные*!*
*!*
// Данные
*/!*
var data = {
title: "Заголовок"
};
// Результат подстановки*!*
var result = _.template(tmpl, data);
*!*
// Результат подстановки
*/!*
alert(result); // <span class="title">Заголовок</span>
```
Этот пример похож на операцию "поиск-и-замена": функция `_.template` просто заменила `<%=title%>` в шаблоне `tmpl` на значение свойства `data.title`.
Пример выше похож на операцию "поиск-и-замена": функция `_.template` просто заменила `<%=title%>` в шаблоне `tmpl` на значение свойства `data.title`.
Но возможность вставки JS-кода делает шаблоны сильно мощнее.
@ -180,6 +203,7 @@ alert(result); // <span class="title">Заголовок</span>
```js
//+ run
// используется \, чтобы объявить многострочную переменную-текст шаблона
var tmpl = '<ul>\
<% for (var i=1; i<=count; i++) { %> \
<li><%=i%></li> \
@ -190,13 +214,11 @@ alert( _.template(tmpl, {count: 5}) );
Здесь в результат попал сначала текст `<ul>`, потом выполнился код `for`, который последовательно сгенерировал элементы списка, и затем список был закрыт `</ul>`.
Скоро мы подробнее разберём, за счёт чего работает функция `_.template`.
### Хранение шаблона в документе
## Хранение шаблона в документе
Шаблон -- это многострочный HTML-текст. Объявлять его в скрипте, как сделано выше -- неудобно и некрасиво.
**Один из способов объявления шаблона -- в HTML, внутри <code>&lt;script&gt;</code> с нестандартным `type`, например `"text/template"`:**
Один из способов объявления шаблона -- записать его в HTML, в тег <code>&lt;script&gt;</code> с нестандартным `type`, например `"text/template"`:
```html
<script type="*!*text/template*/!*" id="menu-template">
@ -212,9 +234,9 @@ alert( _.template(tmpl, {count: 5}) );
var template = document.getElementById('menu-template').innerHTML;
```
В данном случае выбран `type="text/template"`, однако подошёл бы и любой другой нестандартный, например `text/html`.
В данном случае выбран `type="text/template"`, однако подошёл бы и любой другой нестандартный, например `text/html`. Главное, что браузер такой скрипт никак не обработает. То есть, это всего лишь способ передать строку шаблона в HTML.
Полный пример HTML с подключением библиотеки и шаблоном:
Полный пример цикла с подключением библиотеки и шаблоном в HTML:
```html
<!--+ run height=150 -->
@ -241,17 +263,21 @@ var template = document.getElementById('menu-template').innerHTML;
</script>
```
### Как работает функция _.template?
## Как работает функция _.template?
Понимание того, как работает `_.template`, очень важно для отладки ошибок в шаблонах.
Как обработка шаблонов устроена внутри? За счёт чего организована возможность перемежать с текстом произвольный JS-код?
Оказывается, очень просто.
Вызов `_.template(tmpl, data)` выполняется в два этапа:
<ol>
<li>Разбивает строку `tmpl` по разделителям и, при помощи `new Function` создаёт на её основе JavaScript-функцию.</li>
<li>Разбивает строку `tmpl` по разделителям и, при помощи `new Function` создаёт на её основе JavaScript-функцию, которая в специальную переменную-буфер записывает текст из шаблона и выполняет код.</li>
<li>Запускает эту функцию с данными `data`, так что она уже генерирует результат.</li>
</ol>
Эти два процесса можно разделить. Функцию из строки-шаблона можно получить в явном виде вызовом `_.template(tmpl)` (без второго аргумента).
Эти два процесса можно разделить. Функцию из строки-шаблона можно получить в явном виде вызовом `_.template(tmpl)`, без второго аргумента.
Пример:
@ -259,7 +285,7 @@ var template = document.getElementById('menu-template').innerHTML;
//+ run
var compiled = _.template("<h1><%=title%></h1>");
alert( compiled( {title: "Заголовок"} ) ); // <h1>Заголовок</h1>
alert( compiled );
```
Функция `compiled`, которую вернул вызов `_template` из этого примера, выглядит примерно так:
@ -286,8 +312,6 @@ function(obj) {
При вызове этой функции, например `compiled({title: "Заголовок"})`, она получает объект данных как `obj`, здесь это `{title: "Заголовок"}`, и если внутри `with(obj) { .. }` обратиться к `title`, то по правилам [конструкции with](/with) это свойство будет получено из объекта.
Поэтому в шаблоне мы можем обращаться к свойствам переданного объекта, напрямую.
[smart header="Можно и без `with`"]
Конструкция `with` является устаревшей, но в данном случае она полезна.
@ -296,7 +320,7 @@ function(obj) {
<li>Она работает в глобальной области видимости, не имеет доступа к внешним локальным переменным.</li>
<li>Внешний `use strict` на такую функцию не влияет, то есть даже в строгом режиме шаблон продолжит работать.</li>
</ul>
Если мы, по какой-то причине, не хотим использовать `with` -- это возможно. Для этого достаточно поставить третий параметр -- `options`, указав параметр `variable` (название переменной с данными).
Если мы всё же не хотим использовать `with` -- нужно поставить третий параметр -- `options`, указав параметр `variable` (название переменной с данными).
Например:
@ -358,8 +382,8 @@ var menu = new Menu({
*!*
// передаём также шаблоны
*/!*
template: _.template($('#menu-template').html()),
listTemplate: _.template($('#menu-list-template').html()),
template: _.template( document.getElementById('menu-template').innerHTML),
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML),
items: [
"Торт",
"Пончик",
@ -369,7 +393,7 @@ var menu = new Menu({
]
});
$(document.body).append(menu.getElem());
document.body.appendChild(menu.getElem());
```
JS код `Menu`:
@ -386,48 +410,54 @@ function Menu(options) {
function render() {
var elemHtml = options.template({title: options.title});
elem = $(elemHtml);
elem = document.createElement('div');
elem.innerHTML = elemHTML;
elem = elem.firstChild;
elem.on('mousedown selectstart', false);
elem.onmousedown = function() {
return false;
}
elem.on('click', '.title', onTitleClick);
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
}
}
function renderItems() {
if (elem.find('ul').length) return;
if (elem.querySelector('ul')) return;
var listHtml = options.listTemplate({items: options.items});
elem.append(listHtml);
}
function onTitleClick(e) {
toggle();
elem.insertAdjacentHTML("beforeEnd", listHtml);
}
function open() {
renderItems();
elem.addClass('open');
elem.classList.add('open');
};
function close() {
elem.removeClass('open');
elem.classList.remove('open');
};
function toggle() {
if (elem.hasClass('open')) close();
if (elem.classList.contains('open')) close();
else open();
};
this.getElem = getElem;
this.toggle = toggle;
this.close = close;
this.open = open;
}
```
Результат:
[iframe src="menu-3-template" edit border="1" height="160"]
[iframe src="menu-template" height="160"]
Здесь два шаблона. Первый, для меню, мы уже разобрали, посмотрим на список опций:
Здесь два шаблона. Первый мы уже разобрали, посмотрим теперь на список `ul/li`:
```html
<ul>
@ -448,7 +478,7 @@ function Menu(options) {
<li>`</ul>` -- текст</li>
</ul>
А вот функция, которую возвратит `_.template` для этого шаблона. Она практически один-в-один содержит эти инструкции:
Вот функция, которую возвратит `_.template(tmpl)` для этого шаблона:
```js
function(obj) {
@ -469,15 +499,17 @@ function(obj) {
}
```
Код попал в функцию "как есть", выражение в `<%-...%>` обёрнуто в вызов [_.escape](http://lodash.com/docs#escape).
Как видно, она один-в-один повторяет код и вставляет текст в переменную `__p`. При этом выражение в `<%-...%>` обёрнуто в вызов [_.escape](http://lodash.com/docs#escape), который заменяет спецсимволы HTML на их текстовые варианты.
### Отладка шаблонов
## Отладка шаблонов
Что, если в шаблоне ошибка? Например, синтаксическая. Конечно, ошибки будут возникать, куда же без них.
**Шаблон компилируется в функцию, ошибка будет либо при компиляции, либо позже, в процессе её выполнения.**
Шаблон компилируется в функцию, ошибка будет либо при компиляции, либо позже, в процессе её выполнения. В различных шаблонных системах есть свои средства отладки, `_.template` тут не блистатет.
Более сложным является второй случай, так как отладчик при этом останавливается где-то посередине "страшной" функции, и при этом, особенно если шаблонов много и компилируются они где-то раньше по коду -- бывает совершенно неочевидно, из какого шаблона она получена.
Нои здесь можно кое-что отладить. При ошибке, если она не синтаксическая, отладчик при этом останавливается где-то посередине "страшной" функции.
При этом, особенно если шаблонов много и компилируются они где-то раньше по коду -- бывает совершенно неочевидно, из какого шаблона она получена.
Попробуйте сами запустить пример с открытыми инструментами разработчика и включённой опцией "остановка при ошибке":

View file

@ -1,20 +0,0 @@
.menu ul {
display: none;
margin: 0;
}
.menu .title {
font-weight: bold;
cursor: pointer;
background: url(https://js.cx/clipart/arrow-right.png) left center no-repeat;
padding-left: 18px;
}
.menu.open ul {
display: block;
}
.menu.open .title {
background-image: url(https://js.cx/clipart/arrow-down.png);
}

View file

@ -3,7 +3,6 @@
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="menu.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.js"></script>
<script src="menu.js"></script>
</head>
@ -26,8 +25,8 @@
<script>
var menu = new Menu({
title: "Сладости",
template: _.template($('#menu-template').html()),
listTemplate: _.template($('#menu-list-template').html()),
template: _.template( document.getElementById('menu-template').innerHTML),
listTemplate: _.template( document.getElementById('menu-list-template').innerHTML),
items: [
"Торт",
"Пончик",
@ -37,9 +36,8 @@ var menu = new Menu({
]
});
$(document.body).append(menu.getElem());
document.body.appendChild(menu.getElem());
</script>
</body>
</html>
</html>

View file

@ -0,0 +1,24 @@
.menu ul {
display: none;
margin: 0;
}
.menu .title {
font-weight: bold;
cursor: pointer;
}
.menu .title:before {
content: '▶';
padding-right: 6px;
color: green;
}
.menu.open ul {
display: block;
}
.menu.open .title:before {
content: '▼';
}

View file

@ -9,35 +9,39 @@ function Menu(options) {
function render() {
var elemHtml = options.template({title: options.title});
elem = $(elemHtml);
elem = document.createElement('div');
elem.innerHTML = elemHTML;
elem = elem.firstChild;
elem.on('mousedown selectstart', false);
elem.onmousedown = function() {
return false;
}
elem.on('click', '.title', onTitleClick);
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
}
}
function renderItems() {
if (elem.find('ul').length) return;
if (elem.querySelector('ul')) return;
var listHtml = options.listTemplate({items: options.items});
elem.append(listHtml);
}
function onTitleClick(e) {
toggle();
elem.insertAdjacentHTML("beforeEnd", listHtml);
}
function open() {
renderItems();
elem.addClass('open');
elem.classList.add('open');
};
function close() {
elem.removeClass('open');
elem.classList.remove('open');
};
function toggle() {
if (elem.hasClass('open')) close();
if (elem.classList.contains('open')) close();
else open();
};
@ -45,4 +49,4 @@ function Menu(options) {
this.toggle = toggle;
this.close = close;
this.open = open;
}
}