Процессор для преобразования BEM-ориентированного JSON или XML в HTML. Умеет работать на клиенте, разворачивая декларации блоков внутри документа в HTML.
Перед подробным описанием сразу простой пример:
Содержимое index.html:
<script src="jblock.js"></script>
<script src="foo.jblock.js"></script>
<b:foo/>
Содержимое foo.jblock.js:
jBlock.match('foo', function ()
{
this.tag('h1')
.append({
e_wrap: 'Hello, world'
})
})
Результат после загрузки страницы:
<script src="jblock.js"></script>
<script src="foo.jblock.js"></script>
<h1 class="foo"><div class="foo__wrap">Hello, world</div></h1>
После загрузки документа ищутся узлы с префиксом «b:», конвертируются в JSON, который потом изменяется и дополнятеся шаблонами, а конечный JSON преобразуется обратно в HTML.
Все ребята знают или давно догадываются, что интерфейс семантически можно поделить на блоки, внутри которых могут находиться зависимые элементы и другие независимые блоки; вдобавок, у блоков и элементов бывают модификаторы, которые меняют их поведение, внешний вид и т.д.
jBlock умеет переваривать как HTML, так и JSON. Но, очевидно, что основным форматом все-таки является JSON, с него и начнем:
{
b_button: {e_label:'Найти'}
m_size:'L'
}
Смысл префиксов полей b_
, e_
, m_
— это имя блока, элемента и модификатора соответственно. Внутри поля с префиксом b_
или e_
расположены дочерние элементы, которые в свою очередь могут быть как блоками и элементами (в том числе, их массивами), так и простым текстом.
Для ситуаций, когда у блока объемное содержимое, а имя блока не хотелось бы визуально отделять от прочих деклараций (модификаторы, миксы и пр.), предусмотрен параметр content
:
{
b_button:'', m_size:'L',
content:[
{e_label:'Найти'},
...
]
}
Полный список стандартных параметров:
{
b_blockName: Object | Array | String, //имя блока
e_elemName: Object | Array | String, //либо имя элемента (не встречаются вместе)
m_modName: String | Boolean, //модификатор
content: Object | Array | String, //дублирующее поле содержимого
tag: String, //имя тега
attr: Object, //атрибуты, которые будут скопированы в html-узел
mix: Array | String, //классы, которые будут подмешаны к классам блока
css: Object, //список CSS-свойств html-узла
js: Boolean, //флаг активного блока (нужно для фреймворка i-bem.js)
block: Ctx, //явное указание блока, к которому принадлежит элемент (см. this.ctx())
}
JSON описанного формата преобразуется в HTML методом jBlock.json2html(<JSON>):String
.
На практике бывает удобно работать напрямую с HTML, если сложность проекта еще не зашкаливает, или стоит задача быстро собрать прототип интерфейса. Если подключить jblock.js к html-документу, начнет происходить следующая магия: узлы с неймспейсом b:
превратятся в развернутые html-блоки (к ним применится метод jBlock.json2html
).
В целом BEM-ориентированный HTML во-многом дублирует свойства описанного выше JSON:
<b:block m:mod="value" tag="span" mix="block2 block3__elem" param="value">
<elem>Содержимое</elem>
</b:block>
равно
{
b_block:{e_elem:'Содержимое'},
m_mod:'value',
tag:'span',
mix:['block2', 'block3__elem'],
param:'value'
}
Исходное содержимое:
<html>
<head>
<meta charset="utf8"/>
<script src="jblock.js"></script>
</head>
<body>
<h1>Hello, world</h1>
<b:button>OK</b:button>
</body>
</html>
После обработки:
<html>
<head>
<meta charset="utf8"/>
<script src="jblock.js"></script>
</head>
<body>
<h1>Hello, world</h1>
<div class="button">Кнопка</div>
</body>
</html>
Далее можно создать шаблон button.jblock.js (более подробно о шаблонах чуть позже), который правильно раскроет блок button:
jBlock.match('button', function ()
{
this.tag('button')
.append({
e_label: this.copy(),
tag: 'span'
})
})
Этот скрипт следует подключить к html-файлу, который, если лениво, можно упрощать до максимума (браузер стерпит):
<!-- Было: -->
<meta charset="utf8"/>
<script src="jblock.js"></script>
<script src="button.jblock.js"></script>
<h1>Hello, world</h1>
<b:button>OK</b:button>
<!-- Стало: -->
<meta charset="utf8"/>
<script src="jblock.js"></script>
<script src="button.jblock.js"></script>
<h1>Hello, world</h1>
<button class="button">
<span class="button__label">ОК</span>
</button>
По-хорошему, следует вообще уходить от HTML к собственной BEM-семантике:
<meta charset="utf8"/>
<script src="jblock-and-my-blocks.js"></script>
<b:page>
<b:header>Hello, world</b:header>
<b:button m:size="L">OK</b:button>
</b:page>
Важно: внутри узлов с префиксом b:
уже не может быть HTML-разметки (но если очень надо, для этой функции можно самим написать шаблон).
- json2html (json:Object):String — Преобразует BEM-ориентированный JSON в HTML
- json2xml (json:Object, isHtml:Boolean):String — Преобразует BEM-ориентированный JSON в соответсвующий ему XML (или HTML as is)
- match (selector:String, template:Function):jBlock — привязывает к селектору шаблон (детали ниже)
- onLoad (handler:Function) — handler, который будет выполнен после загрузки DOM и всех преобразований
Работа метода jBlock.json2html()
состоит из двух этапов:
- Дерево обходится в режиме
expand
— это внутренний термин, в этом режиме элементы дерева могут быть модифицированы, дополнены или заменены на новые. - Раскрытое дерево обходится во второй раз в режиме
html
, и, согласно описанным выше правилам, преобразуется в HTML.
Шаблоны — это функции, которые применяются к контекстам JSON-дерева во время обхода в режиме expand
. В теле шаблона могут вызываться методы контекста.
Определение шаблона:
jBlock.match('<СЕЛЕКТОР 1>', '<СЕЛЕКТОР 2>', ..., function () {
//Тело шаблона
})
Селектор имеет формат:
block
— применить шаблон к блоку с именем «block»block__elem
— применить к элементу «elem»block_mod_val
— к блоку «block» с модификатором mod со значением valblock__elem_mod_val
— аналогичное про элементblock__*
— все элементы блока
К одному экзепляру блока применяется только один шаблон. Это страхует от бесконечной рекурсии, многочисленных холостых обходов дерева, а главное — позволяет переопределять шаблоны уточняющими селекторами.
Приоритет селектора зависит, прежде всего, от его точности, а при прочих равных, от порядка упоминания — последний перекроет предыдущего.
- append ([content:Array | Object | String, ...]) — определяет содержимое контекста; без аргумента очищает содержимое
- attr (name:String | list:Object, [value]) — возвращает или устанавливает атрибуты
- blockMod (name:String) — возвращает значение модификатора родительского блока (для элементов)
- blockParam (name:String) — возвращает значение параметра родительского блока (для элементов)
- copy (name:String, ...):Ctx — возвращает копию дочерних элементов ('e_elemName', 'b_blockName')
- css (name:String | list:Object, [value]) — возвращает или устанавливает CSS-свойства
- ctx () — ссылка на контекст (используется при обертывании элемента, см. ниже)
- defMod (modifiers:Object) — устанавливает список допустимых модификаторов и их занчений по умолчанию
- escapeHtml () — преобразует символы содержимого
<
в текстовые - has ([name:String]) — проверяет контекст на наличие элемента или блока ('e_elemName', 'b_blockName'); без аргументов проверяет контекст на наличие какого-либо содержимого
- index () — позиция контекста среди соседей
- js () — взводит флаг js
- mix (class:String, ...) — добавляет классы для смешивания
- mod (name:String | list:Object, [value]) — возвращает или устанавливает модификаторы
- name () — возвращает имя элемента или блока (без префиксов
b_
илиe_
) - param (name:String | list:Object, [value]) — возвращает или устанавливает абстрактные параметры (незарезервированные поля контекста)
- replaceWith (content:Object) — заменить контекст новым содержимым
- tag (name:String) — устанавливает имя тега
- text ([selecor:String]) — возвращает текстовое содержимое контекста или его дочернего элемента
Метды можно связывать в цепочки:
jBlock.match('input', function ()
{
this.tag('span')
.defMod({
size:'M',
type:'normal',
state:'release'
})
.append({
e_input:'',
tag:'input',
attr: {
size:1,
autocomlete:'off',
placeholder:this.param('hint'),
value: this.param('value'),
readonly: this.param('readonly') && 'yes'
}
})
})
Метод append()
меняет содержимое контекста, добавляя указанные в аргументах узлы в его конец. Важное замечание: после первого вызова метода, текущее содержимое блока переопределяется. Для того, чтобы частично или полностью сохранить текущее содержимое есть метод copy()
. Теперь несколько поясняющих примеров.
Добавление элементов к текущему содержимому списка (copy()
без селектора копирует всё содержимое контекста):
jBlock.match('foo', function ()
{
this.append(
{e_before:''}, // до содержимого
this.copy(),
{e_after:''} // и после
)
})
Оборачивание элементов:
jBlock.match('foo', function ()
{
this.append({
e_wrap: this.copy('foo__bar')
})
})
Обнуление содержимого:
jBlock.match('foo', function ()
{
this.append()
})
Более сложный пример. Стоит задача в блоке «Письмо» зафиксировать положение элементов: сначала заголовок, потом текст с картинками, а потом подпись:
jBlock.match('letter', function ()
{
this.append(
this.copy('e_header'),
this.copy('e_text', 'e_attach'),
this.copy('e_sign')
)
})
И теперь, вне зависимости от последовательности указанных элементов, в тело письма скопируются только нужные и в нужном порядке.
Или, например, в статье сначала должен идти заголовок, а потом все остальное:
jBlock.match('article', function ()
{
this.append(
this.copy('e_header'),
this.copy('!e_header')
)
})
Метод replaceWith()
помещает на место контекста переданный в аргументе объект. Метод ctx()
внутри этого метода все еще возвращает ссылку на старый контекст:
jBlock.match('page', function ()
{
this.tag('body')
.replaceWith({
e_html:'',
tag:'html',
content: [
{e_head:'', tag:'head'},
this.ctx()
]
})
})
replaceWith()
нужно вызывать в конце шаблона, так как после него действия над текущим контекстом будут невозможны.