Template syntax
User-defined templates are a major part of bem-xjst. Template contains predicate and body.
Template predicate
For each node of the input tree, the template engine checks the conditions specified in the templates. These conditions are called subpredicates. They make up the template predicate.
Conditions can be simple, such as checking the name of the block or element. They can also be complex, such as checking the values of user-defined modes in the current BEMJSON node.
List of subpredicates
block
/**
* @param {String} name block name
*/
block(name)
The name can be specified as'*'
.
Each template must contain the block name subpredicate. Otherwise, the template engine throws an error: BEMHTML error: block('…') not found in one of the templates
.
Example
Subpredicate for the link
block:
block('link')
Input data:
[
// for this block, the predicate returns `true` and the template is applied
{ block: 'link' },
// for all subsequent entities, the predicate returns `false` and
// the template isn’t applied
{ block: 'para' },
'link',
{ block: 'link', elem: 'text' }
]
elem
/**
* @param {String} name element name
*/
elem(name)
Checking the element. The name can be specified as '*'
.
mod
/**
* @param {String} modName name of the block modifier
* @param {String|Boolean} [modVal] value of the block modifier
*/
mod(modName, modVal)
Checking the value of the block modifier.
Templates are applied on the node, both to the block and to the corresponding modifiers.
Example
{ block: 'page', mods: { type: 'index' } }
Templates:
block('page')({ tag: 'body' });
block('page').mod('type', 'index')({ mix: { block: 'mixed' } });
Both templates are applied.
Result of templating:
<body class="page page_type_index mixed"></body>
modVal
checked for compliance after converting to String.
Example
{
block: 'item',
mods: {
size: 1 // Notice that value is Number
}
}
Template:
block('item')
.mod('size', '1') // Notice that value is String
({ tag: 'small' });
The template are applied.
Result of templating:
<small class="item item_size_1"></small>
If second argument of mod()
was omited then templates with any
non-empty value of modifier will be applied.
block('a').mod('size')({ tag: 'span' });
Template will be applied to BEMJSON node if block equals to 'a' and 'size' modifier exists (equals neither to undefined
nor to ''
nor to false
nor to null
).
{ block: 'a', mods: { size: 's' } },
{ block: 'a', mods: { size: 10 } },
But templates will not be applied to entities:
{ block: 'a', mods: { size: '', theme: 'dark' } }
{ block: 'a', mods: { theme: 'dark' } },
{ block: 'a', mods: { size: undefined } },
{ block: 'a', mods: {} }
elemMod
/**
* @param {String} elemModName name of the element modifier
* @param {String|Boolean} [elemModVal] value of the element modifier
*/
elemMod(elemModName, elemModVal)
Checking the value of the element modifier.
Templates are applied on the node, both to the element and to the corresponding modifiers.
Example
{ block: 'page', elem: 'content', elemMods: { type: 'index' } }
Templates:
block('page').elem('content')({ tag: 'body' });
block('page').elem('content').elemMod('type', 'index')({ mix: { block: 'mixed' } });
Both templates are applied.
Result of templating:
<body class="page__content page__content_type_index mixed"></body>
elemModVal
checked for compliance after converting to String. This behavior is similar to checking modVal
.
Second argument of elemMod()
can be omited. In this case
behavior of elemMods()
will be the same as mods()
without second argument.
The templates will be applied to BEMJSON nodes with modifier with any value.
match
/**
* @param {Function} Checking a custom condition.
* The result is converted to `Boolean`.
*/
match((node, ctx) { return … })
Checking a custom condition. In the context of the function, all s are accessible that are accessible in the template. The result of the function is converted to Boolean
.
The order for checking match
is guaranteed. The order for checking the other predicates isn’t important.
block('*').match(() => false)(
// The body of this template won’t be called because the condition
// returned `false`
// …
);
Inside match
callback function you can use apply()
to call any mode from this block.
Subpredicate chains
Subpredicates can be arranged as chains:
block('page')
.mod('theme', 'white')
.elem('content')
.match((node, ctx) => ctx.weather === 'hot')
The following two templates are the same in bem-xjst terms:
block('link').elem('icon')
elem('icon').block('link')
Nested subpredicates
To avoid repeating identical subpredicates, such as for the link
block:
block('link').elem('icon')
block('link').elem('text')
…you can use a nested structure. The subpredicates are placed in the body of a shared subpredicate and separated with commas.
block('link')(
elem('icon')(body_of_template_1),
elem('text')(body_of_template_2)
);
There is no restriction on the nesting level of subpredicates.
Template body
The body of a template represents instructions for generating a templating result for the current BEMJSON node.
The process of templating each node of the input data consists of phases called modes. Each mode is responsible for generating a separate part of the result.
For example, for BEMHTML this might be an HTML tag, HTML class, HTML attributes, tag contents, and so on.
<!-- `def` mode --> <div <!-- the opening and closing element of an HTML tag depends on the `tag` mode --> class=" link <!-- the name is formed from the block, mods, elem, elemMods --> mixed <!-- `mix` mode --> cls <!-- `cls` mode --> i-bem <!-- `js` mode --> " <!-- `bem` mode --> data-bem='{"link":{}}' <!-- `attr` mode --> id="my-dom-node" > Tag content <!-- `content` mode --> </div> <!-- the closing element of the tag also depends on the `tag` mode -->
We’ll cover each mode in detail later. Right now we’ll look at their syntax.
Each mode is a function call. You can’t pass arguments to the mode itself.
The template body is a separate call of a function that expects an argument.
// Incorrect:
block('b').content('test');
// This will throw the BEMHTML error: Predicate should not have arguments.
// Correct:
block('b').content()('test');
Much more easy to use object-like shortcut syntax for modes:
// Object-like shortcut syntax:
block('b')({ content: 'test' });
For input data:
{ block: 'link', url: 'https://yandex.ru', content: 'Yandex' }
And for the template:
block('link')({
tag: 'a',
attrs: (node, ctx) => ({ href: ctx.url })
});
Result of templating:
<a class="link" href="https://yandex.ru">Yandex</a>
Description of standard modes
def
/**
* @param {function|Array|Object[]} value
*/
def: value
The def
mode has a special status. It is responsible for generating the result as a whole. This mode defines the list of other modes and the order to go through them, as well as the build procedure for getting the final representation of the HTML element or BEMJSON from the parts generated in the other modes.
This is a special mode that shouldn’t be used unless truly necessary. A user-defined template that redefines def
disables calls of the other modes by default.
tag
/**
* @param {Function|String} name
*/
tag: name
HTML tag. false
or ''
tells the BEMHTML engine to skip the HTML tag generation stage. Default: div
.
attrs
/**
* @param {function|Object} value
*/
attrs: value
Hash with HTML attributes. The attribute values are escaped using the attrEscape function.
You can use addAttrs
mode to add attributes. addAttrs
is shortcut of attrs
mode:
addAttrs: { id: 'test', name: 'test' }
// This is equivalent to following:
attrs: (node) => {
var attrs = applyNext() || {}; // Get attrs from previous templates
return node.extend(attrs, { id: 'test', name: 'test' });
}
content
/**
* @param {*} value
*/
content: value
Child nodes. By default, it is taken from the content
of the current BEMJSON node.
You can use appendContent
and prependContent
modes to add child nodes to
content.
block('quote')({
prependContent: '“', // add some things before actual content
appendContent: '”', // add content to the end
appendContent: { block: 'link' } // add more content to the end
})
{ block: 'quote', content: 'I came, I saw, I templated.' }
Result of templating:
<div class="quote">“I came, I saw, I templated.”<div class="link"></div></div>
appendContent
and prependContent
is a shortcuts to content
+ applyNext()
:
// appendContent: 'additional content' is the same as:
content: () => [
applyNext(),
'additional content'
]
// prependContent: 'additional content' is the same as:
content: () => [
'additional content',
applyNext()
]
mix
/**
* @param {function|Object|Object[]|String} mixed
*/
mix: mixed
BEM entities to mix with the current one.
Usage example:
block('link')({ mix: { block: 'mixed' } });
block('button')({ mix: [ { block: 'mixed' }, { block: 'control' } ] });
block('header')({ mix: () => ({ block: 'mixed' }) });
You can use addMix
mode to add mix. addMix
is shortcut of mix
:
addMix: 'my_new_mix' // This is equivalent to following:
mix: () => {
var mixes = applyNext();
if (!Array.isArray(mixes)) mixes = [ mixes ];
return mixes.concat('my_new_mix');
}
mods
/**
* @param {function|Object} mods
*/
mods: mods
Hash for modifiers of block.
Example
block('link')({ mods: { type: 'download' } });
block('link')({ mods: () => ({ type: 'download' }) });
Value from mods
mode rewrite value from BEMJSON.
By default returns this.mods
.
// BEMJSON:
{ block: 'b' }
// Template:
block('b')({
def: () => apply('mods')
});
The result is {}
.
You can use addMods
mode to add modifiers. addMods
is shortcut of mods
:
addMods: { theme: 'dark' } // This is equivalent to following:
mods: (node) => {
node.mods = node.extend(applyNext(), { theme: 'dark' });
return node.mods;
}
elemMods
/**
* @param {function|Object} elemMods
*/
elemMods: elemMods
Hash for modifiers of element.
Example
block('link')({ elemMods: { type: 'download' } });
block('link')({ elemMods: () => ({ type: 'download' }) });
Value from elemMods
mode rewrite value from BEMJSON.
By default returns this.mods
.
// BEMJSON:
{ block: 'b', elem: 'e' }
// Template:
block('b').elem('e')({
def: () => apply('mods')
});
The result is {}
.
You can use addElemMods mode to add modifiers for element. addElemMods is shortcut of elemMods:
addElemMods: { theme: 'dark' } // This is equivalent to following:
elemMods: (node) => {
node.elemMods = node.extend(applyNext(), { theme: 'dark' });
return node.elemMods;
}
js
/**
* @param {function|Boolean|Object} value
*/
js: value
JavaScript parameters. If the value isn’t falsy, it mixes i-bem
and adds the
content to Javacript parameters. More information about i-bem and JavaScript parameters. Data is escaped using the jsAttrEscape function.
bem
/**
* @param {function|Boolean} value
*/
bem: value
Tells the template engine whether to add classes and JavaScript parameters for the BEM entity and its mixes. Default: true
.
cls
/**
* @param {function|String} value
*/
cls: value
Adds an HTML class unrelated to the BEM subject domain.
Helper modes
replace
For replacing the current node (matching the node and rendering some other entity).
// BEMJSON
{ block: 'resource' }
Templates:
block('link')({ tag: 'a' });
block('resource')({ replace: { block: 'link' } });
Result of templating:
<a class="link"></a>
You can’t use replace
for self-substitution with a wrapper, or it will loop endlessly.
wrap
Wrap the current node in additional markup.
Example
// BEMJSON
{
block: 'quote',
content: 'Docendo discimus'
}
Template:
block('quote')({
wrap: (node, ctx) => ({
block: 'wrap',
content: ctx
})
});
Result of templating:
<div class="wrap"><div class="quote">Docendo discimus</div></div>
extend
extend
mode allows you to extend context of template.
Example
// BEMJSON
{ block: 'action' }
Templates:
block('action')({
extend: { 'ctx.type': 'Sale', sale: '50%' },
content: (node, ctx) => ctx.type + ' ' + node.sale
});
Result of BEMHTML apply:
<div class="action">Sale 50%</div>
extend
may used as a data proxy to all child nodes.
Example
// Templates
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>
User-defined modes
You can define your own mode and use it in the template body.
Example
// BEMJSON
{ block: 'control', name: 'username', value: 'miripiruni' }
Template:
block('control')(
{
id: 'username-control', // User-defined mode named "id"
content: (node, ctx) => {
return [
{
elem: 'label',
attrs: { for: apply('id') } // Calling the user-defined mode
},
{
elem: 'input',
attrs: {
name: ctx.name,
value: ctx.value,
id: apply('id') // Calling the user-defined mode
}
}
];
},
},
elem('input')({ tag: 'input' }),
elem('label')({ tag: 'label' })
);
Result of templating:
<div class="control">
<label class="control__label" for="username-control"></label>
<input class="control__input" name="username"
value="miripiruni" id="username-control" />
</div>
More information about apply().
BEMTREE
In BEMTREE engine only data related modes are avaliable: def, js, mix, mods, elemMods, content, replace, extend and wrap modes are used by the BEMTREE engine.
User-defined modes can also be used. The other modes described in the documentation above can only be used in BEMHTML.
Read next: What is available in the template body?