Что доступно в теле шаблона?
В процессе обхода входных данных bem-xjst строит контекст, который будет содержать:
Функции шаблонов
В случае, когда тело шаблона является функцией, она вызывается с двумя аргументами:
контекст выполнения шаблона (доступный как
this
в теле шаблона);текущий узел BEMJSON, на который сматчился шаблон (доступный в теле шаблона как
this.ctx
).
Пример
block('link')({
attrs: function(node, ctx) {
return {
// тоже самое что и this.ctx.url
href: ctx.url,
// тоже самое что и this.position
'data-position': node.position
};
}
});
Такие же аргументы доступны в функции подпредикате match()
.
match(function(node, ctx) {
// тоже самое что и this.mods.disabled
return !node.mods.disabled &&
// тоже самое что и this.ctx.target
ctx.target;
})
Кроме того, функции шаблонов поддерживают ES6 arrow functions, поэтому вы можете писать везде в таком стиле:
match((node, ctx) => ctx.target)
addAttrs: (node, ctx) => ({ href: ctx.url })
Нормализованные сведения о текущей БЭМ-сущности
Шаблонизатор нормализует сведения о текущей БЭМ-сущности. В текущем BEMJSON-узле могут быть неполные сведения о БЭМ-сущности:
{
block: 'page',
content: {
elem: 'top'
// У узла нет поля `block`.
// Но шаблонизатор поймёт, в контексте какого блока находится элемент `top`.
}
}
Поля с нормализованными данными:
this.block {String}
— блок в текущем узле, либо блок БЭМ-сущности, в контексте которой находится текущий узел.this.elem {String}
— элемент в текущем узлеthis.mods {Object}
— модификаторы блока, явно указанные в текущем узлеthis.elemMods {Object}
— модификаторы элемента, явно указанные в текущем узле
Обратите внимание, что объекты this.mods
и this.elemMods
всегда присутствуют, поэтому проверки на их наличие в теле шаблона избыточные:
block('page').match((node, ctx) => {
// Избыточно:
return node.mods && node.mods.type === 'index' && ctx.weather;
// Достаточно:
return node.mods.type === 'index' && ctx.weather;
})({ def: () => ({ … }) });
Текущий узел BEMJSON
В поле this.ctx
доступен текущий узел BEMJSON.
{
block: 'company',
name: 'yandex'
}
block('company')({
attrs: (node, ctx) => ({
id: ctx.name,
name: ctx.name
})
});
Результат шаблонизации:
<div class="company" id="yandex" name="yandex"></div>
Хелперы
Методы для экранирования
xmlEscape
/**
* @param {String} str
* @returns {String}
*/
this.xmlEscape(str)
Возвращает переданную строку str
с заэкранированными символами XML: &
, <
, >
. Ожидается, что str
это строка. Но если str
это undefined
, Null
или NaN
, то функция возвратит пустую строку. Если str
любого другого типа, то перед экранированием этот тип будет приведен к строке.
Пример использования:
{ block: 'button' }
Шаблон:
block('button')({
def: (node) => node.xmlEscape('<b>&</b>')
});
Результат шаблонизации:
<b>&</b>
attrEscape
/**
* @param {String} str
* @returns {String}
*/
this.attrEscape(str)
Возвращает переданную строку str
с заэкранированными символами XML- и HTML-атрибутов: "
и &
. Ожидается, что str
это строка. Но если str
это undefined
, Null
или NaN
, то функция возвратит пустую строку. Если str
любого другого типа, то перед экранированием этот тип будет приведен к строке.
jsAttrEscape
/**
* @param {String} str
* @returns {String}
*/
this.jsAttrEscape(str)
Возвращает переданную строку str
с заэкранированными символами: '
и &
. Ожидается, что str
это строка. Но если str
это undefined
, Null
или NaN
, то функция возвратит пустую строку. Если str
любого другого типа, то перед экранированием этот тип будет приведен к строке.
По умолчанию входные данные из поля js
и данные из режима js
экранируются этой функцией.
Хелперы определения позиции
this.position
Позиция в БЭМ-дереве (поле контекста this.position
) представляет собой натуральное число, соответствующее порядковому номеру текущей (контекстной) БЭМ-сущности среди её соседей в дереве (одноранговых сущностей).
При вычислении позиции:
Нумеруются только те узлы обрабатываемого BEMJSON, которые соответствуют БЭМ-сущностям.
Прочим узлам не соответствует никакой номер позиции.
Позиции нумеруются начиная с 1.
Пример нумерации позиций во входном БЭМ-дереве:
{
block: 'page', // this.position === 1
content: [
{ block: 'head' }, // this.position === 1
'text', // this.position === undefined
{
block: 'menu', // this.position === 2
content: [
{ elem: 'item' }, // this.position === 1
'text', // this.position === undefined
{ elem: 'item' }, // this.position === 2
{ elem: 'item' } // this.position === 3
]
}
]
}
БЭМ-дерево может быть достроено в процессе выполнения шаблонов с помощью шаблонов в режиме def
или content
. Такое динамическое изменение БЭМ-дерева учитывается при вычислении позиции.
Функция определения последней БЭМ-сущности среди соседей isLast
возвратит false
, если в массиве, содержащем узлы, последний элемент не является БЭМ-сущностью.
block('list')({
content: [
{ block: 'item1' },
{ block: 'item2' }, // this.isLast() === false
'text'
]
});
Такое поведение объясняется тем, что в целях оптимизации BEMHTML не выполняет
предварительного обхода БЭМ-дерева. Поэтому в момент обработки блока item2
уже
известна длина массива (item2
не является последним элементом), но еще неизвестно, что последний элемент не является БЭМ-сущностью и не получит номера позиции.
На практике описанный случай не должен порождать ошибок, так как проверка на первую/последнюю БЭМ-сущность обычно применяется к автоматически сгенерированным спискам, в которые не имеет смысла включать данные других типов.
isFirst
/**
* @returns {Boolean}
*/
this.isFirst()
Проверяет, является ли узел первым среди своих соседей во входном дереве.
isLast
/**
* @returns {Boolean}
*/
this.isLast()
Проверяет, является ли узел последним среди своих соседей во входном дереве.
Генератор уникальных идентификаторов
this.generateId()
Генерирует id для текущего узла.
Пример использования:
// BEMJSON
{ block: 'input', label: 'Имя', value: 'Иван' }
Шаблон:
block('input')({
content: (node, ctx) => {
var id = node.generateId();
return [
{
tag: 'label',
attrs: { for: id },
content: ctx.label
},
{
tag: 'input',
attrs: {
id: id,
value: ctx.value
}
}
];
}
});
Результат шаблонизации:
<div class="input">
<label for="uniq14563433829878">Имя</label>
<input id="uniq14563433829878" value="Иван" />
</div>
Другие хелперы
this.isSimple({*} arg)
Проверяет, является лиarg
примитивным JavaScript-типом.this.isShortTag({String} tagName)
Проверяет, является лиtagName
тегом, не требующим закрывающего элемента.this.extend({Object} o1, {Object} o2)
Создаёт новый объект в который складывает все поля из объектов-аргументов: сначала изo1
, затем изo2
. Обратите внимание, что если аргументы не будут объектами, то будет возвращен первый аргумент если он не falsy, иначе на второй.
this.reapply()
Это возможность отшаблонизировать произвольные BEMJSON-данные, находясь в теле шаблона и получить в результате строку.
BEMJSON:
{ block: 'a' }
Шаблон:
block('a')({
js: (node) => ({
template: node.reapply({ block: 'b', mods: { m: 'v' } })
})
});
Результат шаблонизации:
<div class="a i-bem" data-bem='{
"a":{"template":"<div class=\"b b_m_v\"></div>"}}'></div>
Кастомные пользовательские поля
Контекст, доступный в теле шаблона, может быть расширен пользователем.
При помощи функции oninit
в коде шаблонов:
var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile(() => {
// Внимание: oninit сработает только при первой компиляции шаблонов.
oninit((exports, shared) => {
shared.BEMContext.prototype.hi = function(username) {
return 'Hello, ' + username;
};
});
block('b')({
content: (node) => node.hi('username')
});
});
var bemjson = { block: 'b' };
// Применяем шаблоны
var html = templates.apply(bemjson);
html
будет содержать строку:
<div class="b">Hello, username</div>
При помощи прототипа BEMContext:
var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile('');
// Расширяем прототип контекста
templates.BEMContext.prototype.hi = function(name) {
return 'Hello, ' + username;
};
// Добавляем шаблоны
templates.compile(() => {
block('b')({
content: (node) => node.hi('templates')
});
});
var bemjson = { block: 'b' };
// Применяем шаблоны
var html = templates.apply(bemjson);
В результате html
будет содержать строку:
<div class="b">Hello, templates</div>
Таннелинг флагов или любых данных шаблонам дочерних узлов
Предположим, что необходимо прокинуть какие-либо данные всем шаблонам для дочерних узлов формы вопросов и ответов 'qa-form'
.
[
{
block: 'qa-form',
content: [
{ block: 'input' },
…
]
},
{ block: 'input' }
]
В этом случае можно дополнить контекст шаблонов при помощи режима extend
:
// выставляем флаг _inQaForm, который будет доступен
// из всех узлов внутри данного
block('qa-form')({ extend: { _inQaForm: true } });
Теперь можно добавить проверку на наличие этого флага:
block('input')
// подпредикат, который проверяет, что флаг взведен
.match((node) => node._inQaForm)
.mix()({ mods: { inside: 'qa' } });
Результат шаблонизации будет таким:
<div class="qa-form">
<div class="input input_inside_qa"></div>
</div>
<div class="input"></div>
Методы для управления шаблонизацией
В теле шаблонов доступны методы apply
, applyNext
и applyCtx
. Подробнее о них читайте в следующей секции про runtime.
Читать далее: runtime