renovations
This commit is contained in:
parent
718ab327f9
commit
e706693c7e
12 changed files with 246 additions and 257 deletions
|
@ -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.
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -44,6 +44,6 @@
|
|||
|
||||
[iframe src="source" border=1 height=50 edit link]
|
||||
|
||||
Что делает эту вёрстку несемантичной? Найдите 3 ошибки.
|
||||
Что делает эту вёрстку несемантичной? Найдите 3 ошибки (или больше).
|
||||
|
||||
Как бы вы сверстали меню правильно?
|
||||
|
|
|
@ -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 |
|
@ -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">
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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>`, а текст `<br>`.</dd>
|
||||
Например, если `expr` содержит текст `<br>`, то при `<%-expr%>` в результат попадёт, в отличие от `<%=expr%>`, не HTML-тег `<br>`, а текст `<br>`.</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><script></code> с нестандартным `type`, например `"text/template"`:**
|
||||
Один из способов объявления шаблона -- записать его в HTML, в тег <code><script></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` тут не блистатет.
|
||||
|
||||
Более сложным является второй случай, так как отладчик при этом останавливается где-то посередине "страшной" функции, и при этом, особенно если шаблонов много и компилируются они где-то раньше по коду -- бывает совершенно неочевидно, из какого шаблона она получена.
|
||||
Нои здесь можно кое-что отладить. При ошибке, если она не синтаксическая, отладчик при этом останавливается где-то посередине "страшной" функции.
|
||||
|
||||
При этом, особенно если шаблонов много и компилируются они где-то раньше по коду -- бывает совершенно неочевидно, из какого шаблона она получена.
|
||||
|
||||
Попробуйте сами запустить пример с открытыми инструментами разработчика и включённой опцией "остановка при ошибке":
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
10
2-ui/5-widgets/4-template-lodash/menu-3-template.view/index.html → 2-ui/5-widgets/4-template-lodash/menu-template.view/index.html
Executable file → Normal file
10
2-ui/5-widgets/4-template-lodash/menu-3-template.view/index.html → 2-ui/5-widgets/4-template-lodash/menu-template.view/index.html
Executable file → Normal 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>
|
24
2-ui/5-widgets/4-template-lodash/menu-template.view/menu.css
Normal file
24
2-ui/5-widgets/4-template-lodash/menu-template.view/menu.css
Normal 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: '▼';
|
||||
}
|
30
2-ui/5-widgets/4-template-lodash/menu-3-template.view/menu.js → 2-ui/5-widgets/4-template-lodash/menu-template.view/menu.js
Executable file → Normal file
30
2-ui/5-widgets/4-template-lodash/menu-3-template.view/menu.js → 2-ui/5-widgets/4-template-lodash/menu-template.view/menu.js
Executable file → Normal 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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue