EN RU
Форум

Методология

Технологии

Инструментарий

Библиотеки

Учебные материалы

Синтаксис шаблонов

Пользовательские шаблоны — основная часть программы на bem-xjst. Шаблон состоит из предиката и тела.

Предикат шаблона

Для каждого узла входного дерева шаблонизатор проверяет условия, указанные в шаблонах. Эти условия называются подпредикатами и составляют предикат шаблона.

Условия могут быть простыми — проверка имени блока/элемента или более сложными и составными — проверка значений произвольных полей в текущем узле BEMJSON.

Список подпредикатов

block

/**
 * @param {String} name имя блока
 */
block(name)

В качестве name можно использовать '*'.

Каждый шаблон должен содержать подпредикат имени блока, иначе шаблонизатор бросит ошибку: BEMHTML error: block('…') not found in one of the templates.

Пример

Подпредикат блока link:

block('link')

Входные данные:

[
    // на этот блок предикат вернет `true` и шаблон будет применён
    { block: 'link' },

    // на все следующие сущности предикат вернет `false` и шаблон не будет применён
    { block: 'para' },
    'link',
    { block: 'link', elem: 'text' }
]

elem

/**
 * @param {String} name имя элемента
 */
elem(name)

Проверка элемента. В качестве name можно использовать '*'.

mod

/**
 * @param {String} modName имя модификатора блока
 * @param {String|Boolean} [modVal] значение модификатора блока
 */
mod(modName, modVal)

Проверка значения модификатора блока.

На узел применятся шаблоны: как на блок, так и на соответствующие модификаторы.

Пример

{ block: 'page', mods: { type: 'index' } }

Шаблоны:

block('page')({ tag: 'body' });
block('page').mod('type', 'index')({ mix: { block: 'mixed' } });

Оба шаблона будут применены.

Результат шаблонизации:

<body class="page page_type_index mixed"></body>

modVal проверяются на соответствие после приведения к строке.

Пример

{
  block: 'item',
  mods: {
      size: 1 // Обратите внимание, что тип значения модификатора size — Number
  }
}

Шаблоны:

block('item')
  .mod('size', '1') // Здесь тип значения модификатора — String
  ({ tag: 'small' });

Шаблон будет применен, так как bem-xjst проверит значения modVal на соответствие после приведения их к строке.

Результат шаблонизации:

<small class="item item_size_1"></small>

Если второй аргумент mod() отсутствует тогда к узлу будут применены шаблоны для соответствующего модификатора с любым значением.

block('a').mod('size')({ tag: 'span' });

Шаблон будет применен к узлам BEMJSON-а, у которых блок равен 'a' и присутствует модификатор 'size' (со значением отличным от undefined, '', false, null).

{ block: 'a', mods: { size: 's' } },
{ block: 'a', mods: { size: 10 } },

Но шаблоны не будут применены к узлам:

{ block: 'a', mods: { size: '', theme: 'dark' } }
{ block: 'a', mods: { theme: 'dark' } },
{ block: 'a', mods: { size: undefined } },
{ block: 'a', mods: {} }

elemMod

/**
 * @param {String} elemModName имя модификатора элемента
 * @param {String|Boolean} [elemModVal] значение модификатора элемента
 */
elemMod(elemModName, elemModVal)

Проверка значения модификатора элемента.

На узел применятся шаблоны: как на элемент, так и на соответствующие модификаторы.

Пример

{ block: 'page', elem: 'content', elemMods: { type: 'index' } }

Шаблоны:

block('page').elem('content')({ tag: 'body' });
block('page').elem('content').elemMod('type', 'index')({ mix: { block: 'mixed' } });

Оба шаблона будут применены.

Результат шаблонизации:

<body class="page__content page__content_type_index mixed"></body>

elemModVal проверяются на соответствие после приведения к строке. Это поведение аналогично поведению проверки modVal.

Второй аргумент elemMod() также может отстуствовать, в этом случае поведение аналогичное подпредикату mod() — шаблоны будут применены к узлам с модификатором любого значения.

match

/**
 * @param {Function} Проверка произвольного условия.
 *                   Результат будет приведен к `Boolean`.
 */
match((node, ctx) => { return … })

Проверка произвольного условия. В контексте функции будут доступны все поля, доступные в теле шаблона. Результат выполнения функции будет приведён к Boolean.

Порядок проверки match гарантируется. Порядок проверки остальных предикатов не важен.

block('*').match(() => false)(
    // Тело этого шаблона не будет вызвано, потому что условие возвратило `false`
    ...
);

В теле функции-колбека match вы можете использовать apply() для вызова любого режима, относящегося к данному узлу.

Цепочки подпредикатов

Подпредикаты можно выстраивать в цепочки:

block('page')
    .mod('theme', 'white')
    .elem('content')
    .match((node, ctx) => ctx.weather === 'hot')

Следующие два шаблона эквивалентны с точки зрения bem-xjst:

block('link').elem('icon')
elem('icon').block('link')

Вложенные подпредикаты

Чтобы не повторять одинаковые подпредикаты, например, для блока link:

block('link').elem('icon')
block('link').elem('text')

…можно использовать вложенную структуру: подпредикаты помещаются в тело общего подпредиката и отделяются друг от друга запятыми.

block('link')(
    elem('icon')(тело_шаблона_1),
    elem('text')(тело_шаблона_2)
);

Уровень вложенности подпредикатов не ограничен.

Тело шаблона

Тело шаблона — инструкции по генерации результата работы шаблонизатора над текущим узлом BEMJSON-а.

Шаблонизация каждого узла входных данных состоит из фаз, называемых режимами. Каждый режим отвечает за генерацию отдельного фрагмента результата.

Например, для BEMHTML это может быть HTML-тег, HTML-класс, HTML-атрибуты, содержание тега и т.д.

<!-- Режим `def` -->
<div <!-- открывающий и закрывающий элемент HTML-тега зависит от режима `tag` -->
    class="
        link   <!-- имя строится из полей block, mods, elem, elemMods -->
        mixed  <!-- режим `mix` -->
        cls    <!-- режим `cls` -->
        i-bem  <!-- режим `js` -->
    "

    <!-- режим `bem` -->
    data-bem='{"link":{}}'

    <!-- режим `attr` -->
    id="my-dom-node"
>
    Содержимое тега <!-- режим `content` -->
</div> <!-- закрывающий элемент тега тоже зависит от режима `tag` -->

Подробное описание каждого режима мы рассмотрим ниже. А пока уделим внимание их синтаксису.

Каждый режим — это вызов функции. Нельзя передавать аргументы в сам режим.

Тело шаблона — это отдельный вызов функции, которая ожидает аргумент.

// Неправильно:
block('b').content('test');
// Будет брошена ошибка BEMHTML error: Predicate should not have arguments

// Правильно:
block('b').content()('test');

Удобнее всего использовать сокращенный синтаксис и задавать значения каждого режима в виде объекта, где ключами будут имена режимов, а значениями тело шаблона:

// Сокращенный синтаксис:
block('b')({ content: 'test' });

Для входных данных:

{ block: 'link', url: 'https://yandex.ru', content: 'Яндекс' }

И шаблона:

block('link')({
    tag: 'a',
    attrs: (node, ctx) => ({ href: ctx.url })
});

Результат шаблонизации:

<a class="link" href="https://yandex.ru">Яндекс</a>

Описание стандартных режимов

def

/**
 * @param {*} value
 */
def: value

Особый статус имеет режим def, который отвечает за генерацию результата в целом. В рамках этого режима задан набор и порядок прохождения остальных режимов, а также определена процедура сборки финального представления HTML-элемента или BEMJSON из фрагментов, сгенерированных в остальных режимах.

Режим является особым и не стоит использовать его без особой надобности. Пользовательский шаблон, переопределяющий def, отключает вызовы остальных режимов по умолчанию.

tag

/**
 * @param {Function|String} name
 */
tag: name

HTML-тег. false или '' укажет движку BEMHTML пропустить этап генерации HTML-тега. По умолчанию div.

attrs

/**
 * @param {function|Object} value
 */
attrs: value

Хеш с HTML-атрибутами. Значения атрибутов будут экранированны функцией attrEscape.

Для того, чтобы добавить attrs, вы можете использовать режим addAttrs, который является сокращением режима attrs и выглядит более лаконично:

addAttrs: { id: 'test', name: 'test' } // Это полностью эквивалентно следующему:
attrs: (node) => {
    var attrs = applyNext() || {}; // атрибуты из предыдущих шаблонов или BEMJSON-а
    return node.extend(attrs, { id: 'test', name: 'test' });
}

content

/**
 * @param {*} value
 */
content: value

Дочерние узлы. По умолчанию будет взято из поля content текущего узла BEMJSON.

Чтобы добавить дочерний узел в содержимое вы можете воспользоваться режимами appendContent и prependContent.

block('quote')(
    {
        prependContent: '«',
        appendContent: '»'
    },
    {
        appendContent: () => ({ block: 'link' })
    }
);
{ block: 'quote', content: 'Пришел, увидел, отшаблонизировал' }

Результатом шаблонизации будет строка:

<div class="quote">«Пришел, увидел, отшаблонизировал»<div class="link"></div></div>

appendContent и prependContent это синтаксический сахар над content + applyNext:

// appendContent: 'еще' тоже самое что и:
content: () => [
    applyNext(),
    'еще'
]

// prependContent: 'еще' тоже самое что и:
content: () => [
    'еще',
    applyNext()
]

mix

/**
 * @param {function|Object|Object[]|String} mixed
 */
mix: mixed

БЭМ-сущности, которые нужно примиксовать к текущей.

Пример использования:

block('link')({ mix: { block: 'mixed' } });
block('button')({ mix: [ { block: 'mixed' }, { block: 'control' } ] });
block('header')({ mix: () => ({ block: 'mixed' }));

Для того, чтобы добавить mix, вы можете использовать режим addMix, который является сокращением режима mix и выглядит более лаконично:

addMix: 'my-new-mix' // Это полностью эквивалентно следующему:
mix: () => {
    var mixes = applyNext();
    if (!Array.isArray(mixes)) mixes = [ mixes ];
    return mixes.concat('my-new-mix');
}

mods

/**
 * @param {function|Object} mods
 */
mods: mods

Хеш модификаторов блока.

Пример использования:

block('link')({ mods: { type: 'download' } });
block('link')({ mods: () => ({ type: 'download' }) });

Значение, вычисленное в режиме mods, переопределит значение, указанное в BEMJSON-е.

По умолчанию возвращает значение this.mods.

// BEMJSON:
{ block: 'b' }

// Шаблон:
block('b')({
    def: () => apply('mods')
});

Возвратит {}.

Для того, чтобы добавить модификаторы, вы должны использовать режим addMods, который является сокращением режима mods и выглядит более лаконично:

addMods: { theme: 'dark' } // Это полностью эквивалентно следующему:
mods: (node) => {
    node.mods = node.extend(applyNext(), { theme: 'dark' });
    return node.mods;
}

elemMods

/**
 * @param {function|Object} elemMods
 */
elemMods: elemMods

Хеш модификаторов элемента.

Пример

block('link')({ elemMods: { type: 'download' } });
block('link')({ elemMods: () => ({ type: 'download' }) });

Значение, вычисленное в режиме elemMods, переопределит значение, указанное в BEMJSON-е.

По умолчанию возвращает значение this.elemMods.

// BEMJSON:
{ block: 'b', elem: 'e' }

// Шаблон:
block('b').elem('e')({
    def: () => apply('elemMods')
});

Возвратит {}.

Для того, чтобы добавить модификаторы, вы можете использовать режим addElemMods, который является сокращением режима elemMods и выглядит более лаконично:

addElemMods: { theme: 'dark' } // Это полностью эквивалентно следующему:
elemMods: (node) => {
    node.elemMods = node.extend(applyNext(), { theme: 'dark' });
    return node.elemMods;
}

js

/**
 * @param {function|Boolean|Object} value
 */
js: value

JS-параметры. Если значение не falsy, то миксует i-bem и добавляет содержимое в JS-параметры. Подробнее про i-bem и JS-параметры. Данные будут экранированны функцией jsAttrEscape.

bem

/**
 * @param {function|Boolean} value
 */
bem: value

Указывает шаблонизатору, нужно ли добавлять классы и JS-параметры для самой БЭМ-сущности и её миксов. По умолчанию true.

cls

/**
 * @param {function|String} value
 */
cls: value

Добавить произвольный HTML-класс, не относящийся к предметной области БЭМ.

Режимы-хелперы

replace

Для подмены текущего узла (сматчились на узел, а рендерим произвольную сущность).

// BEMJSON
{ block: 'resource' }

Шаблоны:

block('link')({ tag: 'a' });
block('resource')({ replace: { block: 'link' } });

Результат шаблонизации:

<a class="link"></a>

replace нельзя использовать для замены себя с обёрткой, иначе будет бесконечный цикл.

wrap

Обернуть текущий узел в дополнительную разметку.

Пример

// BEMJSON
{
    block: 'quote',
    content: 'Docendo discimus'
}

Шаблон:

block('quote')({
    wrap: (node, ctx) => ({
        block: 'wrap',
        content: ctx
    })
});

Результат шаблонизации:

<div class="wrap">
    <div class="quote">Docendo discimus</div>
</div>

extend

Доопределить контекст исполнения шаблонов.

Пример

// BEMJSON
{ block: 'action' }

Шаблоны:

block('action')({
    extend: { 'ctx.type': 'Sale', sale: '50%' }
});
block('action')({
    content: (node, ctx) => ctx.type + ' ' + node.sale
});

Результат шаблонизации:

<div class="action">Sale 50%</div>

extend может использоваться для прокидывания данных во все дочерние узлы через контекст выполнения шаблонов.

Пример

block('page')({ extend: { meaning: 42 } });
block('*')({ attrs: (node) => ({ life: node.meaning }) });
// BEMJSON
{ block: 'page', content: { block: 'wrap', content: { block: 'para' } }
}
<div class="page" life="42"><div class="wrap" life="42"><div class="para"
life="42"></div></div></div>

Пользовательские режимы

Вы можете определить свой режим и использовать его в теле шаблона.

Пример

// BEMJSON
{ block: 'control', name: 'username', value: 'miripiruni' }

Шаблон:

block('control')(
    {
        id: 'username-control', // Пользовательский режим с именем id
        content: (node, ctx) => {
            return [
                {
                    elem: 'label',
                    attrs: { for: apply('id') } // Вызов пользовательского режима
                },
                {
                    elem: 'input',
                    attrs: {
                        name: ctx.name,
                        value: ctx.value,
                        id: apply('id')  // Вызов пользовательского режима
                    }
                }
            ];
        }
    },
    elem('input')({ tag: 'input' }),
    elem('label')({ tag: 'label' })
);

Результат шаблонизации:

<div class="control">
    <label class="control__label" for="username-control"></label>
    <input class="control__input" name="username"
        value="miripiruni" id="username-control" />
</div>

Подробнее про apply().

BEMTREE

В движке BEMTREE используются только режимы ориентированные на данные: def, js, mix, mods, elemMods, content и режимы-хелперы replace, extend и wrap.

Пользовательские режимы тоже могут быть использованы. Остальные режимы, ориентированные на HTML, описанные в документации выше, применимы только к BEMHTML.

Читать далее: что доступно в теле шаблона?