What is available in the template body?
While traversing input data, bem-xjst builds a context, which contains:
Template function
When body of template is function, it calls with two arguments:
context of template (familiar to us as
this
)current BEMJSON node (familiar to us as
this.ctx
)
Example
block('link')({
attrs: function(node, ctx) {
return {
// the same as this.ctx.url
href: ctx.url,
// the same as this.position
'data-position': node.position
};
}
});
The same arguments available in function of match()
.
match(function(node, ctx) {
// the same as this.mods.disabled
return !node.mods.disabled &&
// the same as this.ctx.target
ctx.target;
})
Moreover, template functions can be arrow functions:
match((node, ctx) => ctx.target)
addAttrs: (node, ctx) => ({ href: ctx.url })
Normalized information about the current BEM entity
The template engine normalizes data about the current BEM entity. The current BEMJSON node might have incomplete information about the BEM entity. For example:
{
block: 'page',
content: {
elem: 'top'
// The node doesn’t have the `block` field.
// But the template engine understands which
// block context the `top` element is in.
}
}
Fields with normalized data:
this.block {String}
— the block in the current node, or the block of the BEM entity that provides the context for the current node.this.elem {String}
— element in the current nodethis.mods {Object}
— block modifiers that are explicitly defined in the current nodethis.elemMods {Object}
— element modifiers that are explicitly defined in the current node
Note that the this.mods
and this.elemMods
objects always exist, so checking for their presence in the template body is redundant:
block('page').match((node, ctx) => {
// Redundant:
return node.mods && node.mods.type === 'index' && ctx.weather;
// Sufficient:
return node.mods.type === 'index' && ctx.weather;
})({ def: () => ({ … }) });
Current BEMJSON node
The current BEMJSON node is available in the this.ctx
field.
{
block: 'company',
name: 'yandex'
}
block('link')({
attrs: (node, ctx) => ({
id: ctx.name,
name: ctx.name
})
});
Result of templating:
<div class="company" id="yandex" name="yandex"></div>
Helpers
Escape methods
xmlEscape
/**
* @param {String} str
* @returns {String}
*/
this.xmlEscape(str)
Returns the passed str
string with the following XML symbols escaped: &
, <
, >
. Normaly, expected that str
is a String
. But if str
is undefined
, Null
or NaN
an empty string returned. If str
is of any other type it will be casted to String before escaping.
Usage example:
{ block: 'button' }
Template:
block('button')({
def: (node) => node.xmlEscape('<b>&</b>')
});
Result of templating:
<b>&</b>
attrEscape
/**
* @param {String} str
* @returns {String}
*/
this.attrEscape(str)
Returns the passed str
string with the following characters for XML and HTML attributes escaped: "
and &
. Normaly, expected that str
is a String
. But if str
is undefined
, Null
or NaN
type you get empty string. If str
is any other type you get native casting from it type to String
before escaping.
jsAttrEscape
/**
* @param {String} str
* @returns {String}
*/
this.jsAttrEscape(str)
Returns the passed str
string with the following characters escaped: '
and &
. Normaly, expected that str
is a String
. But if str
is undefined
, Null
or NaN
type you get empty string. If str
is any other type you get native casting from it type to String
before escaping.
By default, input data from the js
field and data from the js
mode are escaped using this function.
Position helpers
this.position
The position in the BEM tree (the this.position
context field) is a natural number corresponding to the sequential number of the current (contextual) BEM entity in relation to its neighbors in the tree (peer entities).
When calculating the position:
Numbering applies only to nodes of processed BEMJSON that correspond to BEM entities.
Other nodes are not given a position number.
Positions are numbered starting from 1.
Example of position numbering in the input BEM tree:
{
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
]
}
]
}
The BEM tree may be filled in as templates are executing, by using templates in the def
or content
mode. This dynamic modification of the BEM tree is taken into account when calculating positions.
The isLast
function for determining the last BEM entity among peers returns false
if the last element in the array containing the nodes is not a BEM entity.
block('list')({
content: [
{ block: 'item1' },
{ block: 'item2' }, // this.isLast() === false
'text'
]
});
This behavior is explained by the fact that for optimization purposes, BEMHTML does not perform a preliminary traversal of the BEM tree. This means that at the time when the item2
block is processed, the length of the array is already known (item2
is not the last element). However, it is not yet known that the last element is not a BEM element and won’t get a position number.
In practice, this case shouldn’t generate errors, because the check for the first and last BEM entity is normally applied to automatically generated lists, and it doesn’t make sense to include other types of data in them.
isFirst
/**
* @returns {Boolean}
*/
this.isFirst()
Checks whether the node is the first among its peers in the input tree.
isLast
/**
* @returns {Boolean}
*/
this.isLast()
Checks whether the node is the last among its peers in the input tree.
Unique ID generator
this.generateId()
Generates an 'id' for the current node.
Usage example:
// BEMJSON
{ block: 'input', label: 'Name', value: 'John Malkovich' }
Template
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
}
}
];
}
});
Result of templating:
<div class="input">
<label for="uniq14563433829878">Name</label>
<input id="uniq14563433829878" value="John Malkovich" />
</div>
Other helpers
this.isSimple({*} arg)
Checks whetherarg
is a JavaScript primitive type.this.isShortTag({String} tagName)
Checks whethertagName
is a tag that doesn’t require a closing element.this.extend({Object} o1, {Object} o2)
Returns new object with all fields fromo1
ando2
.
this.reapply()
This is the ability to template any BEMJSON data located in the template body and get a string as the result.
BEMJSON:
{ block: 'a' }
Template:
block('a')({
js: (node) => ({
template: node.reapply({ block: 'b', mods: { m: 'v' } })
})
});
Result of templating:
<div class="a i-bem" data-bem='{
"a":{"template":"<div class=\"b b_m_v\"></div>"}}'></div>
User-defined custom fields
The context available in the template body can be extended by the user.
Using the oninit
function in the template code:
var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile(() => {
// Note: oninit only works for the first template compilation.
oninit((exports, shared) => {
shared.BEMContext.prototype.hi = function(username) {
return 'Hello, ' + username;
};
});
block('b')({
content: (node) => node.hi('username')
});
});
var bemjson = { block: 'b' };
// Applying templates
var html = templates.apply(bemjson);
html
contains the string:
<div class="b">Hello, username</div>
Using the BEMContext prototype:
var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile('');
// Extending the context prototype
templates.BEMContext.prototype.hi = function(name) {
return 'Hello, ' + username;
};
// Adding templates
templates.compile(() => {
block('b')({
content: (node) => node.hi('templates')
});
});
var bemjson = { block: 'b' };
// Applying templates
var html = templates.apply(bemjson);
As a result, html
contains the string:
<div class="b">Hello, templates</div>
Data tunneling for child’s templates
Suppose we want to provide some data from parent node to all child templates. E.g. provide _inQaForm
flag to all input
blocks inside qa-form
block.
[
{
block: 'qa-form',
content: [
{ block: 'input' },
…
]
},
{ block: 'input' }
]
We can extend context of templates:
// Set _inQaForm flag, which will be avaliable in qa-form child nodes
block('qa-form')({ extend: { _inQaForm: true } });
And check that the flag exists in match
subpredicate:
block('input')
// check if the flag exists
.match((node) => node._inQaForm)
.mix()({ mods: { inside: 'qa' } });
Result of templating:
<div class="qa-form">
<div class="input input_inside_qa"></div>
</div>
<div class="input"></div>
Methods for controlling the templating process
The methods apply
, applyNext
and applyCtx
are available in the body of templates. For more information, see the next section on runtime.
Read next: runtime