Skip to content
Permalink
v4.1.1
Go to file
 
 
Cannot retrieve contributors at this time
646 lines (473 sloc) 22.3 KB

BH NPM version Build Status Dependency Status Coverage Status

Что это?

BH — это BEMJSON-процессор, который превращает BEMJSON в HTML. Одним словом, это шаблонизатор.

Онлайн-демо.

Преимущества

  1. Быстрый.
  2. Не требует компиляции.
  3. Удобен в отладке, т.к. не компилируется в другой код.
  4. Написан на чистом JavaScript, используется и расширяется через JavaScript.
  5. Прост для понимания, т.к. это обертка над обычными преобразованиями исходного BEMJSON в конечный BEMJSON / HTML.
  6. Компактен на клиенте (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 позволяет задавать параметры шаблонизации.

jsAttrName

Позволяет задать имя атрибута для хранения поля js. Значение по умолчанию — onclick.

bh.setOptions({ jsAttrName: 'data-bem' });
bh.apply({ block: 'button', js: true });
<div class="button i-bem" data-bem='return {"button":{}}'></div>

jsAttrScheme

Формат хранения данных в атрибуте. По умолчанию js

bh.setOptions({ jsAttrScheme: 'json' });
bh.apply({ block: 'button', js: { foo: bar } });
<div class="button i-bem" onclick='{"button":{"foo":"bar"}}'></div>

jsCls

Имя дополнительного класса для узлов, имеющих js. По умолчанию i-bem. Если передать значение false, дополнительный класс не будет добавляться.

bh.setOptions({ jsCls: false });
bh.apply({ block: 'button', js: true });
<div class="button" onclick='{"button":{}}'></div>

jsElem

Регулирует установку дополнительного класса, указанного в параметре 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>

escapeContent

Включает эскейпинг содержимого. По умолчанию выключен.

bh.setOptions({ escapeContent: true });
bh.apply({ content: '<script>' });
<div>&lt;script&gt;</div>

clsNobaseMods

Удаляет имя блока и/или элемента из имен модификаторов в классе. По умолчанию 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>

delimElem

Задает разделитель между блоком и элементом. По умолчанию __.

bh.setOptions({ delimElem: '_' });
bh.apply({ block: 'button', elem: 'text' });
<div class="button_text"></div>

delimMod

Задает разделитель между блоком или элементом и их модификатором. По умолчанию _.

bh.setOptions({ delimMod: '--' });
bh.apply({ block: 'button', mods: { disabled: true } });
<div class="button button--disabled"></div>

shortTags

shortTags расширяет стандартный набор коротких тегов.

bh.setOptions({ shortTags: ['rect'] });

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

Например, мы хотим установить модификатор 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-дерева

Кроме модификации элемента, функция-преобразователь может вернуть новый 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

Инстанции класса Ctx передаются во все шаблоны. Все методы класса в set-режиме возвращают инстанцию класса, то есть реализуют чейнинг.

ctx.process(bemJson)

Применяет шаблоны для переданного BEMJSON-дерева в текущем контексте. Возвращает результат преобразований.

bh.match('button', function(ctx) {
    bh.toHtml(ctx.process({ elem: 'control' }));
});

ctx.tag([value[, force]])

Возвращает/устанавливает тег в зависимости от аргументов. force — задать значение тега, даже если оно было задано ранее.

bh.match('input', function(ctx) {
    ctx.tag('input');
});

Замечание: Если передать в качестве значения false или пустую строку, текущий узел не будет выведен в конечный HTML, выведется только его содержимое, если оно есть.

ctx.mod(key[, value[, force]])

Возвращает/устанавливает модификатор в зависимости от аргументов. 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);
});

ctx.mods([values[, force]])

Возвращает/устанавливает модификаторы в зависимости от аргументов. force — задать модификаторы, даже если они были заданы ранее.

bh.match('paranja', function(ctx) {
    ctx.mods({
        theme: 'normal',
        disabled: true
    });
});

ctx.attr(key[, value[, force]])

Возвращает/устанавливает значение атрибута в зависимости от аргументов. 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);
});

ctx.attrs([values[, force]])

Возвращает/устанавливает атрибуты в зависимости от аргументов. force — задать атрибуты, даже если они были заданы ранее.

bh.match('input', function(ctx) {
    ctx.attrs({
        name: ctx.param('name'),
        autocomplete: 'off'
    });
});

ctx.mix([value[, force]])

Возвращает/устанавливает значение mix в зависимости от аргументов.

При установке значения если force равен true, то переданный микс заменяет прежнее значение, в противном случае миксы складываются.

bh.match('button_pseudo_yes', function(ctx) {
    ctx.mix({ block: 'link', mods: { pseudo: 'yes' } });
    ctx.mix([
        { elem: 'text' },
        { block: 'ajax' }
    ]);
});

ctx.bem([value[, force]])

Возвращает/устанавливает значение bem в зависимости от аргументов. force — задать значение bem, даже если оно было задано ранее.

Если bem имеет значение false, то для элемента не будут генерироваться БЭМ-классы.

bh.match('meta', function(ctx) {
    ctx.bem(false);
});

ctx.js([value[, force]])

Возвращает/устанавливает значение js в зависимости от аргументов. force — задать значение js, даже если оно было задано ранее.

Значение js используется для инициализации блоков в браузере через BEM.DOM.init().

bh.match('input', function(ctx) {
    ctx.js(true);
});

ctx.cls([value[, force]])

Возвращает/устанавливает дополнительное значение CSS-класса в зависимости от аргументов. force — задать значение cls, даже если оно было задано ранее.

bh.match('field_type_email', function(ctx) {
    ctx.cls('validate');
});
<div class="field field_type_email validate"></div>

ctx.content([value[, force]])

Возвращает/устанавливает содержимое в зависимости от аргументов. force — задать содержимое, даже если оно было задано ранее.

bh.match('input', function(ctx) {
    ctx.content({ elem: 'control' });
});

ctx.json()

Возвращает текущий фрагмент 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()

ctx.position() возвращает позицию текущего BEMJSON-элемента в рамках родительского. См. пример для ctx.position(), ctx.isFirst() и ctx.isLast().

ctx.isFirst()

ctx.isFirst() возвращает true, если текущий BEMJSON-элемент — первый в рамках родительского BEMJSON-элемента. См. пример для ctx.position(), ctx.isFirst() и ctx.isLast().

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');
    }
});

ctx.isSimple()

Проверяет, что объект является примитивом.

bh.match('link', function(ctx) {
    ctx.tag(ctx.isSimple(ctx.content()) ? 'span' : 'div');
});

ctx.extend()

Аналог функции extend в jQuery.

ctx.applyBase()

Выполняет преобразования данного 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);
});

ctx.stop()

Останавливает выполнение прочих шаблонов для данного BEMJSON-элемента.

Пример:

bh.match('button', function(ctx) {
    ctx.tag('button', true);
});
bh.match('button', function(ctx) {
    ctx.tag('span');
    ctx.stop();
});

ctx.generateId()

Возвращает уникальный идентификатор. Может использоваться, например, чтобы задать соответствие между label и input.

ctx.param(key[, value[, force]])

Возвращает/устанавливает параметр текущего BEMJSON-элемента. force — задать значение параметра, даже если оно было задано ранее. Например:

bh.match('search', function(ctx) {
    ctx.attr('action', ctx.param('action') || '/');
});

ctx.tParam(key[, value[, force]])

Получает / передает параметр вглубь 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'));
});
You can’t perform that action at this time.