Live initialization
Before a block starts to function the core initializes it. At the end of this
process the block gets js_inited modifier, which you are already familiar
with.
While a block is initialized, there appears a JavaScript object corresponding to
the block instance. Then a callback for js_inited modifier runs, and there can
be coded all the primary actions.
In the previous examples all the blocks on a page were initialized after
domReady. Although on a page full of block it is not needed to initialize all
the components at once.
Sometimes a user loads a page just to press one button on it. So, a better way is to save calculation time and browser memory initializing block only when a user starts operating on them.
This is the so-called live (lazy) initialization.
'live' static method
The instructions to initialize a block lazy can be given in a predefined live
static method.
modules.define('my-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
...
},
...
}, {
live: function() {
// Here you can code when to initialize this block instance
}
}));
});
In the previous examples, there was not static methods at all and this is equal
to setting the live property as false.
Here, as it is a function, the core understands that the instances of this block should not be initialized before something special happens. This can be that a DOM event fires of the block DOM node or on an element.
Initializing a block on DOM event
pure.bundles/
010-live-init-on-event/
blocks/
text/
translate/
translate.bemhtml.js
translate.css
translate.js
010-live-init-on-event.bemjson.js
010-live-init-on-event.html
On the
010-live-init-on-event.html
(BEMJSON)
page you can see the text in Dutch. Actually, this text is divided into a lot of
pieces phrase by phrase. Then, they are framed with a translate block.
If a user reading the text does not understand its meaning he/she can see a translation for an unclear phrase by clicking on the text.
<span
class="translate i-bem"
data-bem="{'translate':{'prompt':'One man comes in a post office;'}}">
Een man gaat een postkantor binnen
<i class="translate__prompt"></i>
</span>
As you can see from its HTML structure, the translate block holds a piece of
text in Dutch inside and its English translation in the block parameters (inside
the data-bem attribute). Also, there is a prompt element not displayed by
default, which is used to place the translation into it when needed.
Note that there is no translate_js_inited class on a block DOM node even after
the page is completely loaded. This means that there is no JavaScript object
related to the block yet.
In the
translate.js
file of the block it is said to initialize it only after a click launches on
the block DOM node.
modules.define('translate', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
...
},{
live: function() {
this.liveInitOnEvent('click');
}
}));
});
When clicked, the core applies js_inited modifier to the block instance and
runs constructor, the function set to this modifier.
modules.define('translate', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
'js' : {
'inited' : function() {
this.setMod(this.elem('prompt'), 'visible', true);
}
}
},
onElemSetMod: {
'prompt': {
'visible': function(elem) {
elem.text(this.params['prompt']);
}
}
}
},{
...
}));
});
It makes the contained prompt element visible by setting on it the visible
modifier into true. And this means to take the corresponding translation from
the block parameters by getting the this.params.paramName value.
In face, the translation could be placed into the prompt at the beginning since
it was invisible for a user anyway. But just to illustrate how the parameters can
be taken, its was placed into the data-bem.
Coming back to the live initialization, you can see that on a page with many blocks of the kind the core initializes only those on which the event runs. This approach saves the browser memory and makes the page function faster.
There is an event delegation idea
behind live initialization. Thus, there is only one listener for the click
event on the document object, not a lot of them for every block on a
page.
Besides saving browser forces, this way provides some flexibility for dynamically changed pages. This you can see with the following example.
Delegated initialization
This page provides absolutely the same translate block as the previous one.
But there is also a piece of crazy inline JavaScript on a page which works when a
user clicks the pink button and dynamically append a few of new translate
blocks to the page. Then, with clicking on the phrases of this fresh joke you can
see that it work absolutely the same as the other translate blocks being on the
page at the beginning.
The core of i-bem framework listens to the events on the document object. So,
when a user clicks any translate block, this click bubbles up to the document
and core initializes the block as it was instructed it its live section.
Binding to live events
pure.bundles/
011-live-bind-to/
blocks/
button/
button.bemhtml.js
button.css
button.js
page/
011-live-bind-to.bemjson.js
011-live-bind-to.html
The next example with 100 BonBon buttons (BEMJSON) shows that live events can be reacted not once when initializing a block but every time.
This button block is again equipped with live initialization instructions since
it would be madness to initialize all the 100 buttons at once and then listen to
the clicks on each of them.
modules.define('button', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
'js' : {
'inited' : function() {
var button = this.domElem[0].innerHTML;
console.log('Here an object of ' + button + ' comes. Just once.');
}
}
},
...
},{
live: function() {
this.liveBindTo('click');
}
}));
});
Similar to the examples with liveInitOnEvent this code initializes a block
instance and runs the js_inited modifier callback.
Unlike the liveInitOnEvent the liveBindTo method runs its callback not
just once but every time a user clicks the button.
modules.define('button', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
...
},
onClick: function() {
console.log('Here I can track clicks');
}
},{
live: function() {
this.liveBindTo('click', function(e) {
this.onClick();
});
}
}));
});
Live initialization on many events
pure.bundles/
012-live-init-many-events/
blocks/
checkbox/
checkbox.bemhtml.js
checkbox.css
checkbox.js
page/
012-live-init-many-events.bemjson.js
012-live-init-many-events.html
In the previous examples the core watched only one click event to decide if a
block should start working or not. But sometimes reacting just one event is not
enough. This is illustrated with the
012-live-init-many-events
(BEMJSON)
example, where you can see customized checkboxes.
<span
class="checkbox i-bem"
data-bem="{'checkbox':{}}">
<input class="checkbox__control" id="remember1" type="checkbox" value="on">
<label class="checkbox__label" for="remember1"></label>
</span>
It is obvious an instance of this block has to be initialized when a user clicks
its label element.
modules.define('checkbox', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
...
_onClick : function() {
this.setMod('focused', true);
},
...
},{
live: function() {
this.liveBindTo('label', 'click', function() {
this._onClick();
});
}
}));
});
The same liveBindTo method is used here to initialized the block and listen to
its next clicks. Notice that here it is provided with an additional parameter
(the first one) with the name of a block element whose clicks we are interested
in.
But more than that, the control can be changed with a keyboard (or from another JavaScript piece) and this must also be taken into account.
You can put in the live method as many instructions about how to initialize as
you need. Here it happens after a click event on the label element and also
after a change event on the embedded control element, which is native input.
modules.define('checkbox', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
...
_onClick : function() {
this.setMod('focused', true);
},
_onChange : function(e) {
this.setMod('checked', e.target.checked);
}
},{
live: function() {
this.liveBindTo('label', 'click', function() {
this._onClick();
});
this.liveBindTo('control', 'change', function(e){
this._onChange(e);
});
}
}));
});
The block should also be inited when focused in or focused out.
modules.define('checkbox', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
...
},{
live: function() {
this.liveBindTo('label', 'click', function() {
this._onClick();
});
this.liveBindTo('control', 'change', function(e) {
this._onChange(e);
});
this.liveBindTo('control', 'focusin focusout', function(e) {
this.setMod('focused', e.type == 'focusin');
});
}
}));
});
As you can see, it is possible to bind to more than one event with the same callback if list their names separated with a space.
Then, with adding modifiers functionality to the components, it can be finished.
modules.define('checkbox', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
'focused' : {
'true' : function() {
this.elem('control').focus();
},
'' : function() {
this.elem('control').blur();
}
},
'checked' : function(modName, modVal) {
this.elem('control').attr('checked', modVal ? 'checked' : false);
}
},
...
},{
live: function() {
...
}
}));
});
This approach makes the control behaviour consistent. No matter how a user or another piece of JavaScript or a browser start to interact with the component, it will work fine.
Getting the focused modifier from something, it would focus the embedded input
control. Having the control focused, it would set focused modifier to itself
providing the proper view.
When changed either manually or automatically the block would get checked modifier
and a checked attribute for the control or loose them.
Why not :checked?
As you might notice, in this example an internal control element (the input)
is indicated to be checked with the checked modifier on its parent block.
<span
class="checkbox i-bem checkbox_js_inited checkbox_checked"
data-bem="{'checkbox':{}}">
<input
class="checkbox__control"
id="remember2"
type="checkbox"
value="on"
checked="checked">
<label class="checkbox__label" for="remember2"></label>
</span>
.checkbox_checked .checkbox__label {
left: 54px;
}
.checkbox_checked .checkbox__label:after {
background: #00bf00;
}
Indeed, it would be possible to use :checked pseudo selector as it was done in
the control prototype.
.checkbox input[type=checkbox]:checked + label {
left: 54px;
}
.checkbox input[type=checkbox]:checked + label:after {
background: #00bf00;
}
However the modifier approach supplies more flexibility making the whole block be able to change appearance if checked
.checkbox_checked
{
background-image: linear-gradient(0deg, #333, #333 4px, #555 4px, #555 6px);
background-size: 6px 6px;
}
as well as saves time for parsing selectors and bringing architectural consistency to the code.
BEM events
Besides DOM events, i-bem.js operates with custom JavaScript events on the JavaScript objects corresponding to the blocks. These events are named BEM events and usually serve to normalize a component API.
The link block
of bem-components block library can
provide an example of firing a custom BEM event.
Its JavaScript functionality is to trigger the click event on the corresponding
JavaScript object whenever a user clicks the left button if the current link is not
disabled.
An event is triggered with the help of emit method of the block.
_onClick : function(e) {
e.preventDefault();
this.hasMod('disabled') || this.emit('click');
}
Thus, the link block has an API which can be used by other blocks on a page.
Another example is the menu
block
of this tutorial. It is represets a list of menu items in HTML, one of those can be
selected at the moment.
<div class="menu i-bem" data-bem="{"menu":{}}">
<ul class="menu__layout">
<li class="menu__layout-unit menu__layout-unit_position_first">
<div class="menu__item menu__item_state_current">
Item 1
</div>
</li>
<li class="menu__layout-unit">
<div class="menu__item menu__item_state_current">
Item 2
</div>
</li>
<li class="menu__layout-unit">
<div class="menu__item menu__item_state_current">
Item 3
</div>
</li>
</ul>
</div>
The menu listens to the DOM clicks on its item-selector elements and emits the
current event which signals about changing the current item and provides the
data.
this
.delMod(prev, 'state')
.emit('current', {
prev : prev,
current : elem
});
This event fires on the JavaScript object corresponding to the menu block instance.
With that, any other block subscribed to the current BEM event of the menu can
learn when it changes its current item and react on it.
Live initialization on BEM event of an inner block
components.bundles/
014-live-init-bem-event/
blocks/
map-marks/
map-marks.bemhtml.js
map-marks.css
map-marks.js
map/
map.bemhtml.js
map.deps.js
map.js
menu/
menu.css
page/
014-live-init-bem-event.bemjson.js
014-live-init-bem-event.html
The example shows map-marks
block
which binds a menu and a map so that a user can select the menu item and see a
related mark in the map.
The map-marks block contains the blocks menu and map. This can be seen from
bemjson description of the
page
or inside the page html
014-live-init-bem-event.html.
This block is only needed when a user has been started to interact with the
menu. So the block uses live initialization and it is declared to initialize the
block only when the current BEM event fires on the included menu block.
The JavaScript implementation of the block map-marks.js uses live initialization depending on the inner block.
modules.define('map-marks', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) {
provide(BEMDOM.decl(this.name, {
...
}, {
live: function() {
this.liveInitOnBlockInsideEvent('current', 'menu', function(e, data) {
this._showMap(e, data.current);
});
}
}));
});
The liveInitOnBlockInsideEvent methods requests the names of an event and the
included block s well as a callback.
Once a user clicks a menu item, it becomes current and the menu block emits
current event. Being catched, it initializes the map-marks block, which means
it gets js_inited modifier ans the related method runs:
modules.define('map-marks', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
'js' : {
'inited' : function () {
this._menu = this.findBlockInside('menu');
this._map = this.findBlockInside('map');
}
}
},
...
}, {
live: function() {
...
}
}));
});
Then the callback runs the _showMap method of the block instance. This shows a
mark on a map using the map block.
modules.define('map-marks', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) {
provide(BEMDOM.decl(this.name, {
...
_showMap: function(e, elem) {
var params = this._menu.elemParams(elem);
this._map.showAddress(params['address']);
}
...
}, {
live: function() {
...
}
}));
});