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() {
...
}
}));
});