Skip to content
Vladimir Grinenko edited this page Mar 10, 2016 · 21 revisions

Templates converter

If you have 1.x templates there is a way to convert old templates to JS syntax: https://github.com/bem/bem-templates-converter

Install package:

npm install -g bemhtml-syntax

Convert old templates:

bemhtml-syntax --format bemhtml -Qr -i path/to/block.bemhtml -o path/to/block.bemhtml.js

To convert all templates at once and format the code according to your coding style (using jscs), use the command:

find *.blocks -name '*.bemhtml' -type f | while read f; do \
    bemhtml-syntax --format bemhtml -Qr -i "${f}" -o "${f}.js"; jscs --fix "${f}.js";\
done

After checking the operability of new templates (.bemhtml.js), the old templates (.bemhtml) can be removed using:

find *.blocks -name '*.bemhtml' -type f -delete

Major differences 1.x…2.x

Since 2.x version there is no more templates compilation (and as a result much faster build time). Also execution performance increased.

API changed

var bemxjst = require('bem-xjst');
var template = bemxjst.compile('... your source code ...');

template.apply(/* ... your input data here ... */);

// Or even better:
template = bem.compile(function() {
  block('b1').content()('yay');
});
template.apply({ block: 'b1' });

Possibility to add templates in runtime

var bemxjst = require('bem-xjst');
var template = bemxjst.compile(function() {
  block('main').tag()('body');
});

var html = template.apply({ block: 'main' });
// html = '<body class="page"></body>'

// Add more templates:
template = bem.compile(function() {
  block('main').tag()('article');
});

html = template.apply({ block: 'main' });
// html = '<article class="page"></article>'

New responsibility and changes

With new compiler came new responsibility, the following breaking (or just surprising) changes were made.

Contents:

return values

Default (def()) mode of templates MUST return a string now. This string will represent the result of template execution and will be added to the final HTML output.

block('b1').def()(function() {
  return applyNext();
});

this._ no more

All helpers have finally migrated to this from this._. From now on this._ will be undefined.

block('b1').content()(function() {
  return this.extend({
    block: 'b2'
  }, {
    tag: 'a'
  });
});

mode('') no more

Empty mode (mode('')) is no longer supported. Template authors are advised to use oninit instead:

oninit(function(exports) {
  var proto = exports.BEMContext.prototype;

  proto.cleverSubstring = function cleverSubstring() {
    // ...
  };
});

oninit is called only once during template compilation.

In case (ONLY IN THIS CASE) where template author may need to do something for every block, one should use wildcard block (block('*')):

// Will make all blocks have `a` tag
block('*').tag()('a');

// Will wrap all HTML tags in `{` and `}`
block('*').def()(function() {
  return '{' + applyNext() + '}';
});

NOTE: This should be used only as the last resort method. The price for using block('*') is reduced performance.

block(...) is required in all templates

It is now required to specify block predicate (block('...')) in every template.

VALID:

block('b1')(
  tag()('a'),
  content()({ block: 'b2' })
);

INVALID:

match(function() { return this.my.custom.match; }).tag()('a');

this._str is gone

HTML chunks are no longer pushed to this._str. If data needs to be flushed - special _flush method should be added to the BEMContext prototype.

oninit(function(exports) {
  var proto = exports.BEMContext.prototype;

  proto._flush = function _flush(string) {
    // Send chunk directly to a client
    // NOTE: This is just an example, no actual socket implementation
    // is provided by bem-xjst
    socket.send(string);

    // `_flush()` MUST return string value:
    //   * returning input string will make `_flush` act as no-op
    //   * any returned string will be present in resulting HTML
    return '';
  };
});

Blocks with overridden def() mode will be flushed in a single chunk (including their contents), unless .def().xjstOptions({ flush: true }) is used as a predicate.

Еxample of an element that forces flushing to stream:

oninit(function(exports, shared) {
  var proto = shared.BEMContext.prototype;

  proto.flush = function() {
    return _buf.splice(0).join('');
  };
  
  var _buf = [];
  
  proto._flush = function(chunk){
    _buf.push(chunk);
    return '';
  };
});
// and
block('page').elem('flusher').def()(function(){ return this.flush(); });

See: https://github.com/bem/bem-xjst/blob/678e33e19d2dc4289aee4f65ef80eeb393f7b4d9/test/runtime-test.js#L396-L434

elemMatch() and this.elem changes

ATM elemMatch() can be used after .elem() only since you can't test this.elem outside of .elem().

.elemMatch(F) later will be a synonym to .elem('*').match(F) or a similar thing. See also #135.

apply() behaves slightly different

Since a notable performance jump apply() can't call custom modes in other blocks.

block('b2').mode('custom')('in b2'); // won't be called
block('b1').mode('custom')('in b1'); // will be called instead
block('b1').content()(function() {
  return apply('custom', { block: 'b2' });
});

Also there is no more polymorphism in apply() and you should always explicitly pass a first argument, e.g. 'default'.

applyNext() don't use guard flags to prevent executing nested template

Every template now applies to the each block of the same name in nested tree when called in applyNext().

E.g. for template:

block('b1').content()(function() {
  return {
    elem: 'inner',
    content: applyNext()
  };
});

And BEMJSON:

{ block: 'b1', content: { block: 'b1' } }

Will result in:

<div class="b1"><div class="b1__inner"><div class="b1"><div class="b1__inner"></div></div></div></div>

Instead of:

<div class="b1"><div class="b1__inner"><div class="b1"></div></div></div>

Static objects shortcuts in mix, content, etc.

Because new templates are not compiled, variables will share references to the same objects and that can look like a memory leak.

Just switch to function generator if you're not sure how somebody will redefine it.

For bemjson:

([
    { block: 'b1', mods: { a: true } },
    { block: 'b1' },
    { block: 'b1' }
]);

And templates:

// Don't do that!
block('b1')(
  mix()([]), // Use function generator instead: `function() { return []; }`.
  mix()(function () {
    var res = applyNext();
    res.push({block: 'b2'});
    return res;
  }),

  attrs()([]), // Use function generator instead: `function() { return {}; }`.
  mod('a', true).attrs()(function () {
    var res = applyNext();
    res.withMod = 'a';
    return res;
  })
);

You'll see unnecessary attribute withMod and more duplicates in mixes for each next block:

<div class="b1 b1_a b2" withMod="a"></div>
<div class="b1 b2 b2" withMod="a"></div>
<div class="b1 b2 b2 b2" withMod="a"></div>

once() helper to apply template just once

Literally, it guards template block from being applied several times.

NB: It behaves like an old applyNext().

Code like:

block('b1').def()(function() {
  return applyNext();
});

now should be rewritten to:

block('b1').match(function() { return !this._guard; }).def()(function() {
  return applyNext({_guard: true});
});

Or with once():

block('b1').once().def()(function() {
  return applyNext();
});

once() will be removed in later versions of bem-xjst.

wrap() helper for wrapping

Literally, it sets guard flag into this.ctx and applies context. Sugar for def() and applyCtx().

Code like:

block('b1').def()(function() {
  return applyCtx({ block: 'wrapper', content: this.ctx });
});

can now be rewritten to:

block('b1').match(function() { return !this.ctx._guard; }).def()(function() {
    this.ctx._guard = true;
    return applyCtx({ block: 'wrapper', content: this.ctx });
});

Or with wrap() helper:

block('b1').wrap()(function() {
    return { block: 'wrapper', content: this.ctx };
});

replace() helper for replacing

block('b1').def()(function() {
  return applyCtx({ block: 'replacer' });
});
block('b1').replace()(function() {
  return { block: 'replacer' };
});

tag behaviour

In BEMHTML 1.x tag in templates has more priority than tag in BEMJSON. In bemhtml 4.x is the opposite. NOTE: Yes, we know this is a bug and we already fixed it in 6.x.

bem-xjst 1.x template:

block b1 {
   tag: 'a’
}

BEMJSON:

{ block: ‘b1’, tag: ‘b’ }

Result:

<a class=“b1”></a>

bem-xjst 4.x template:

block('b1').tag()('a');

bem-xjst 4.x result:

<b class="b1"></b>

4.x demo