Синтаксис шаблонов
Пользовательские шаблоны — основная часть программы на 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.
Читать далее: что доступно в теле шаблона?