BH — это BEMJSON-процессор, который превращает BEMJSON в HTML. Одним словом, это шаблонизатор.
- Быстрый.
- Не требует компиляции.
- Удобен в отладке, т.к. не компилируется в другой код.
- Написан на чистом JavaScript, используется и расширяется через JavaScript.
- Прост для понимания, т.к. это обертка над обычными преобразованиями исходного BEMJSON в конечный BEMJSON / HTML.
- Компактен на клиенте (12,4 Кб после сжатия, 3,7 Кб после gzip).
BH-процессор можно найти в npm-пакете bh
, а ENB-технологии для его использования — в npm-пакете enb-bh
.
npm install bh
BH-файлы в проекте имеют суффикс bh.js
(например, page.bh.js
). Файл формируется в формате CommonJS для NodeJS:
module.exports = function(bh) {
// ...
};
Для преобразования исходного дерева BEMJSON в конечный HTML используется метод apply
.
Для получения промежуточного результата в виде развернутого BEMJSON-дерева нужно использовать метод processBemJson
.
Для получения конечного HTML без преобразования BEMJSON-дерева используется метод toHtml
.
Простой пример использования:
var bh = new (require('bh').BH);
bh.match('button', function(ctx) {
ctx.tag('button');
})
bh.processBemJson({ block: 'button' }); // { block: 'button', mods: {}, tag: 'button' }
bh.apply({ block: 'button' }); // '<button class="button"></button>'
bh.toHtml({ block: 'button' }); // '<div class="button"></div>'
Функции для работы с BEMJSON — шаблоны — объявляются через метод match
. В теле функций описывается логика преобразования BEMJSON.
В функцию-шаблон передаются два аргумента: ctx
— экземпляр класса Ctx
и json
— ссылка на текущий узел BEMJSON-дерева.
Замечание: Категорически не рекомендуется вносить изменения напрямую в объект json
. Вместо этого следует использовать методы объекта ctx
. Объект json
рекомендуется использовать только для «чтения» (см. также метод ctx.json()
).
Синтаксис:
{BH} bh.match({String} expression, function({Ctx} ctx, {Object} json) {
//.. actions
});
Также допустимо объявлять несколько шаблонов в одном вызове метода match
.
Синтаксис:
{BH} bh.match({Array} expressions, function({Ctx} ctx));
Где expressions
— массив вида:
[
{String} expression1,
...,
{String} expressionN
]
Или в виде объекта:
{BH} bh.match({Object} templates);
Где templates
представляет собой объект вида:
{
{String} expression1 : function({Ctx} ctx) {
//.. actions1
},
...,
{String} expressionN : function({Ctx} ctx) {
//.. actionsN
},
}
Ниже в этом документе можно найти перечень методов класса Ctx
. Дальше пойдем по примерам.
Например, зададим блоку button
тег button
, а блоку input
тег input
:
bh.match('button', function(ctx) {
ctx.tag('button');
});
bh.match('input', function(ctx) {
ctx.tag('input');
});
Теперь нам нужна псевдо-кнопка. То есть, если у кнопки модификатор pseudo
равен yes
, то нужен тег a
и атрибут role="button"
:
bh.match('button_pseudo_yes', function(ctx) {
ctx
.tag('a')
.attr('role', 'button');
});
В данном примере мы матчимся не просто на блок button
, а на блок button
с модификатором pseudo
, имеющим значение yes
.
Рассмотрим синтаксис строки матчинга для функций преобразования (в квадратных скобках указаны необязательные параметры):
'block[_blockModName[_blockModVal]][__elemName][_elemModName[_elemModVal]]'
По-русски:
'блок[_имяМодификатораБлока[_значениеМодификатораБлока]][__имяЭлемента][_имяМодификатораЭлемента[_значениеМодификатораЭлемента]]'
Метод setOptions
позволяет задавать параметры шаблонизации.
Позволяет задать имя атрибута для хранения поля js
. Значение по умолчанию — onclick
.
bh.setOptions({ jsAttrName: 'data-bem' });
bh.apply({ block: 'button', js: true });
<div class="button i-bem" data-bem='return {"button":{}}'></div>
Формат хранения данных в атрибуте. По умолчанию js
bh.setOptions({ jsAttrScheme: 'json' });
bh.apply({ block: 'button', js: { foo: bar } });
<div class="button i-bem" onclick='{"button":{"foo":"bar"}}'></div>
Имя дополнительного класса для узлов, имеющих js
. По умолчанию i-bem
.
Если передать значение false
, дополнительный класс не будет добавляться.
bh.setOptions({ jsCls: false });
bh.apply({ block: 'button', js: true });
<div class="button" onclick='{"button":{}}'></div>
Регулирует установку дополнительного класса, указанного в параметре jsCls, для элемента с js
-реализацией. По умолчанию true
. Если задать значение false
, дополнительный класс добавляться не будет.
bh.setOptions({ jsElem: false });
bh.apply({ block: 'button', elem: 'box', js: true });
<div class="button__box" onclick='return {"button__box":{}}'></div>
Включает эскейпинг содержимого. По умолчанию выключен.
bh.setOptions({ escapeContent: true });
bh.apply({ content: '<script>' });
<div><script></div>
Удаляет имя блока и/или элемента из имен модификаторов в классе. По умолчанию false
.
bh.setOptions({ clsNobaseMods: true });
bh.apply({
block: 'button',
mods: { disabled: true, theme: 'new' },
mix: [
{ block: 'clearfix' },
{ elem: 'box', elemMods: { pick: 'left' } }
],
content: {
elem: 'control',
elemMods: { disabled: true }
}
});
<div class="button _disabled _theme_new clearfix button__box _pick_left">
<div class="button__control _disabled"></div>
</div>
Задает разделитель между блоком и элементом. По умолчанию __
.
bh.setOptions({ delimElem: '_' });
bh.apply({ block: 'button', elem: 'text' });
<div class="button_text"></div>
Задает разделитель между блоком или элементом и их модификатором. По умолчанию _
.
bh.setOptions({ delimMod: '--' });
bh.apply({ block: 'button', mods: { disabled: true } });
<div class="button button--disabled"></div>
shortTags
расширяет стандартный набор коротких тегов.
bh.setOptions({ shortTags: ['rect'] });
xhtml
определяет, нужно ли рендерить слеш для коротких тегов. По умолчанию true
.
bh.setOptions({ xhtml: false });
Например, мы хотим установить модификатор state
со значением closed
для всех блоков popup
:
bh.match('popup', function(ctx) {
ctx.mod('state', 'closed');
});
Замиксуем form
с search-form
:
bh.match('search-form', function(ctx) {
ctx.mix({ block: 'form' });
});
Установим класс для page
:
bh.match('page', function(ctx) {
ctx.cls('ua_js_no ua_css_standard');
});
Кроме модификации элемента, функция-преобразователь может вернуть новый BEMJSON. Здесь мы воспользуемся методами ctx.json()
(возвращает текущий элемент BEMJSON «как есть») и ctx.content()
(возвращает или устанавливает контент).
Например, обернем блок header
блоком header-wrapper
:
bh.match('header', function(ctx) {
return {
block: 'header-wrapper',
content: ctx.json()
};
});
Замечание: Любое не-undefined значение вставляется в конечное BEMJSON-дерево вместо текущего узла. Соответственно, удалить текущий узел можно просто вернув значение null
.
Обернем содержимое button
элементом content
:
bh.match('button', function(ctx) {
ctx.content({
elem: 'content',
content: ctx.content()
}, true);
});
Метод ctx.content
принимает первым аргументом BEMJSON, который надо выставить для содержимого, а вторым — флаг force (выставить содержимое, даже если оно уже существует).
Добавим элемент before
в начало, а after
— в конец содержимого блока header
:
bh.match('header', function(ctx) {
ctx.content([
{ elem: 'before' },
ctx.content(),
{ elem: 'after' }
], true);
});
Добавим блок before-button
перед блоком button
:
bh.match('button', function(ctx) {
return [
{ block: 'before-button' },
ctx.json()
];
});
Метод enableInfiniteLoopDetection
позволяет включать и выключать механизм определения зацикливаний.
Замечание: Рекомендуется включать этот механизм только для отладки, так как он замедляет работу шаблонизатора.
bh.enableInfiniteLoopDetection(true);
bh.match('button', function(ctx) {
ctx.content({ block: 'button' });
});
Error: Infinite matcher loop detected at "button".
Инстанции класса Ctx
передаются во все шаблоны. Все методы класса в set-режиме возвращают инстанцию класса, то есть реализуют чейнинг.
Применяет шаблоны для переданного BEMJSON-дерева в текущем контексте. Возвращает результат преобразований.
bh.match('button', function(ctx) {
bh.toHtml(ctx.process({ elem: 'control' }));
});
Возвращает/устанавливает тег в зависимости от аргументов. force — задать значение тега, даже если оно было задано ранее.
bh.match('input', function(ctx) {
ctx.tag('input');
});
Замечание: Если передать в качестве значения false
или пустую строку, текущий узел не будет выведен в конечный HTML, выведется только его содержимое, если оно есть.
Возвращает/устанавливает модификатор в зависимости от аргументов. force — задать модификатор, даже если он был задан ранее.
bh.match('input', function(ctx) {
ctx.mod('native', 'yes');
ctx.mod('disabled', true);
});
bh.match('input_islands_yes', function(ctx) {
ctx.mod('native', '', true);
ctx.mod('disabled', false, true);
});
Возвращает/устанавливает модификаторы в зависимости от аргументов. force — задать модификаторы, даже если они были заданы ранее.
bh.match('paranja', function(ctx) {
ctx.mods({
theme: 'normal',
disabled: true
});
});
Возвращает/устанавливает значение атрибута в зависимости от аргументов. force — задать значение атрибута, даже если оно было задано ранее.
bh.match('input_disabled_yes', function(ctx) {
ctx.attr('disabled', 'disabled');
});
Замечание: Если необходимо удалить сам атрибут, а не просто обнулить значение атрибута, то вторым параметром надо передать null
:
bh.match('link', function(ctx) {
ctx.attr('href', null);
});
Замечание: Чтобы задать булевый атрибут, следует передать вторым параметром true
:
bh.match('link_hidden_yes', function(ctx) {
ctx.attr('hidden', true);
});
<div class="link link_hidden_yes" hidden></div>
Возвращает/устанавливает атрибуты в зависимости от аргументов. force — задать атрибуты, даже если они были заданы ранее.
bh.match('input', function(ctx) {
ctx.attrs({
name: ctx.param('name'),
autocomplete: 'off'
});
});
Возвращает/устанавливает значение mix в зависимости от аргументов.
При установке значения если force равен true, то переданный микс заменяет прежнее значение, в противном случае миксы складываются.
bh.match('button_pseudo_yes', function(ctx) {
ctx.mix({ block: 'link', mods: { pseudo: 'yes' } });
ctx.mix([
{ elem: 'text' },
{ block: 'ajax' }
]);
});
Возвращает/устанавливает значение bem
в зависимости от аргументов. force — задать значение bem
, даже если оно было задано ранее.
Если bem
имеет значение false, то для элемента не будут генерироваться БЭМ-классы.
bh.match('meta', function(ctx) {
ctx.bem(false);
});
Возвращает/устанавливает значение js
в зависимости от аргументов. force — задать значение js
, даже если оно было задано ранее.
Значение js
используется для инициализации блоков в браузере через BEM.DOM.init()
.
bh.match('input', function(ctx) {
ctx.js(true);
});
Возвращает/устанавливает дополнительное значение CSS-класса в зависимости от аргументов. force — задать значение cls
, даже если оно было задано ранее.
bh.match('field_type_email', function(ctx) {
ctx.cls('validate');
});
<div class="field field_type_email validate"></div>
Возвращает/устанавливает содержимое в зависимости от аргументов. force — задать содержимое, даже если оно было задано ранее.
bh.match('input', function(ctx) {
ctx.content({ elem: 'control' });
});
Возвращает текущий фрагмент BEMJSON-дерева. Может использоваться в связке с return
для враппинга и подобных целей. Для сокращения можно использовать второй аргумент функции-шаблона — json
.
Замечание: После вызова ctx.applyBase()
нарушается цепочка естественного применения шаблонов. Из-за этого json
перестает указывать на актуальный узел в BEMJSON-дереве. В этом случае следует использовать ctx.json()
.
bh.match('input', function(ctx, json) {
return {
elem: 'wrapper',
attrs: { name: json.name },
content: ctx.json()
};
});
ctx.position() возвращает позицию текущего BEMJSON-элемента в рамках родительского.
См. пример для ctx.position()
, ctx.isFirst()
и ctx.isLast()
.
ctx.isFirst() возвращает true
, если текущий BEMJSON-элемент — первый в рамках родительского BEMJSON-элемента.
См. пример для ctx.position()
, ctx.isFirst()
и ctx.isLast()
.
ctx.isLast() возвращает true
, если текущий BEMJSON-элемент — последний в рамках родительского BEMJSON-элемента.
Пример для ctx.position()
, ctx.isFirst()
и ctx.isLast()
:
bh.match('list__item', function(ctx) {
ctx.mod('pos', ctx.position());
if (ctx.isFirst()) {
ctx.mod('first', 'yes');
}
if (ctx.isLast()) {
ctx.mod('last', 'yes');
}
});
Проверяет, что объект является примитивом.
bh.match('link', function(ctx) {
ctx.tag(ctx.isSimple(ctx.content()) ? 'span' : 'div');
});
Аналог функции extend
в jQuery.
Выполняет преобразования данного BEMJSON-элемента остальными шаблонами. Может понадобиться, например, чтобы добавить элемент в самый конец содержимого, если в базовых шаблонах в конец содержимого добавляются другие элементы.
Пример:
bh.match('header', function(ctx) {
ctx.content([
ctx.content(),
{ elem: 'under' }
], true);
});
bh.match('header_float_yes', function(ctx) {
ctx.applyBase();
ctx.content([
ctx.content(),
{ elem: 'clear' }
], true);
});
Останавливает выполнение прочих шаблонов для данного BEMJSON-элемента.
Пример:
bh.match('button', function(ctx) {
ctx.tag('button', true);
});
bh.match('button', function(ctx) {
ctx.tag('span');
ctx.stop();
});
Возвращает уникальный идентификатор. Может использоваться, например, чтобы задать соответствие между label
и input
.
Возвращает/устанавливает параметр текущего BEMJSON-элемента. force — задать значение параметра, даже если оно было задано ранее. Например:
bh.match('search', function(ctx) {
ctx.attr('action', ctx.param('action') || '/');
});
Получает / передает параметр вглубь BEMJSON-дерева. force — задать значение параметра, даже если оно было задано ранее.
bh.match('input', function(ctx) {
ctx.content({ elem: 'control' });
ctx.tParam('value', ctx.param('value'));
});
bh.match('input__control', function(ctx) {
ctx.attr('value', ctx.tParam('value'));
});