Modifiers
In BEM, modifiers express block states. To put a block into a special state we set a modifier on it. Then a block runs a callback associated with this modifier.
Setting a modifier on a block and reacting to it
pure.bundles/ 002-change-modifier/ blocks/ call-button/ call-button.bemhtml.js call-button.css call-button.js call-button.png 002-change-modifier.bemjson.js 002-change-modifier.html
In the 002-change-modifier (BEMJSON) example you can see a button changing its state after a user clicks on it.
The button is a block named call-button
and is represented by
CSS,
JavaScript
and templates
placed into the block folder.
In JavaScript blocks/call-button/call-button.js there is a common BEM DOM block declaration.
The callback associated with js_inited
modifier runs when a block is
initialized by the core. In this example it starts with binding to a click
event on the DOM node corresponding to the block. This is done with the bindTo
helper.
In the callback it is said to set a calling
modifier to the block and the setMod
method serves for it.
NOTE: In many cases using bindTo
for events listening is not the best solution
as it needs to watch every block of the kind. It becomes even worse with elements of
the blocks since they are many. You will see below much better way in the live
section.
modules.define('call-button', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
'js' : {
'inited' : function() {
this.bindTo('click', function() {
this.setMod('calling');
});
}
}
...
Take into account that here we use a boolean modifier, which has no value. But
as you will see below, modifiers are very often used as key-value pairs. In that
case, both modifier name and its value have to be passed to the setMod
helper:
this.setMod('status', 'on');
...
this.setMod('status', 'off');
The setMod
method applies a modifier CSS class to the blocks which makes the
block change its appearance. If you need additional changes on a block,
place them into a function corresponding to the modifier. Like the following:
modules.define('call-button', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
'js' : { ... },
'calling' : function() {
this.elem('link').text('Calling...');
}
}
}));
});
Here you can run your calculations, or code any functionality of the block. As
there is access to the block DOM node and its children, the DOM structure can
also be changed. With the elem
helper you can select the elements of the block
by their names.
The concept of pre-defined block states expressed with modifiers is a very powerful and efficient way to describe an interface component.
Everything related to a particular block state is encapsulated in a relevant modifier. From wherever you change a block modifier, it knows what to do.
Modifiers are described in a declarative manner, which empowers a programmer to extend the code with further implementations or to redefine it completely, as is shown in the tutorial below.
Setting a modifier on an element
pure.bundles/ 003-element-modifier/ blocks page/ sign/ text/ traffic-light/ __go/ traffic-light__go.mp3 traffic-light.bemhtml.js traffic-light.css traffic-light.js 003-element-modifier.bemjson.js 003-element-modifier.html
According to BEM, elements can be modified in the same way as blocks. JavaScript methods are similar in both. The 003-element-modifier example illustrates this.
Similar to the previous example, the traffic-light
has
CSS,
JavaScript
and BEMHTML implementations and is introduced to the i-bem
core as a
DOM-equipped block.
It contains three light elements stop
, slow
and go
each of which can have a
status
modifier with its on
and off
value.
modules.define('traffic-light', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
'js' : {
'inited' : function() {
...
this.setMod('status', 'stop');
}
},
...
}));
});
The traffic light works by switching its status
modifier from the stop
to the
slow
and then to the go
values. In its initializing method it is said to set
a modifier status_stop
to the block, so that the cycle begins.
The status
modifier is declared with its callback, once for all its values. This
is a good way to get rid of copy&paste if the corresponding states work similarly.
modules.define('traffic-light', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl('traffic-light', {
onSetMod: {
'js' : { ... },
'status' : function(modName, modVal, oldModVal) {
clearTimeout(this.timer);
var nextStatus = {
'stop' : 'slow',
'slow' : 'go',
'go' : 'stop'
},
_this = this;
oldModVal && this.setMod(this.elem(oldModVal), 'status', 'off');
this.setMod(this.elem(modVal), 'status', 'on');
this.timer = window.setTimeout(function() {
_this.setMod('status', nextStatus[modVal]);
}, 2000);
}
},
...
}));
});
The arguments passed into the modifier callback are:
Modifier name,
Modifier value to be set,
Previous modifier value.
With these, the actions can be a bit different depending on the modifier value.
Here a corresponding element is given the status_on
modifier so that its light
turns on and the previously active projector is set status_off
.
Modifiers are set on elements with the already familiar setMod
helper with an
optional first parameter which means an element name.
So, by providing different parameters to the same setMod
function you can:
// apply a modifier to a current block
this.setMod('modName', 'modValue');
// apply a modifier to an element of a current block
this.setMod(this.elem('elemName'), 'modName', 'modValue');
Describing the actions related to element modifiers is similar to block modifier
actions. By analogy to onSetMod
property you can user onElemSetMod
with the
following syntax:
DOM.decl('my-block', {
onElemSetMod: {
'elemName' : {
'foo' : function() {
// Runs when an element gets any value of `foo` modifier
},
'bar' : {
'qux' : function() {
// Runs when an element gets 'qux' value of 'bar' modifier
},
'' : function() {
// Runs when `bar` modifier is removed from an element
}
}
}
}
});
In this example, only the go
element is provided with a special functionality.
modules.define('traffic-light', ['i-bem__dom'], function(provide, BEMDOM) {
var goSound = new Audio('blocks/traffic-light/__go/traffic-light__go.mp3');
provide(BEMDOM.decl(this.name, {
onSetMod: { ... },
onElemSetMod: {
'go' : {
'status' : {
'on' : function() {
goSound.play();
},
'off' : function() {
goSound.pause();
}
}
}
}
}));
});```
This makes a browser play a traffic light sound when an element is switched into
`status_on` and to keep silent when the modifier goes off.
## Toggling a modifier
```files
pure.bundles/
004-toggle-mod/
blocks/
page/
switch/
switch.bemhtml.js
switch.css
switch.js
004-toggle-mod.bemjson.js
004-toggle-mod.html
It is useful to toggle a modifier if there are 2 values of it to be changed one by one. This is what the 004-toggle-mod (BEMJSON) example demonstrates.
It shows a switch
block, which is a nice button, with its switched_off
modifier meaning that the button is inactive at the moment.
The
switch.js
file of the block instructs the button to react to user clicks and toggle the
modifier from switched_off
to switched_on
and backwards by using the
toggleMod
helper.
modules.define('switch', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
'js' : {
'inited' : function() {
this.bindTo('click', function() {
this.toggleMod('switched', 'on', 'off');
});
}
}
}
}));
});
Indeed, the same goes for elements which an additional first parameter for the helper method.
Deleting a modifier
pure.bundles/ 005-modifier-removing/ blocks/ page/ todo/ todo.bemhtml.js todo.css todo.js 005-modifier-removing.bemjson.js 005-modifier-removing.html
Removing a modifier from an element (or a block) explained with 005-modifier-removing (BEMJSON) example. This is a kind of To-Do list, where each task is a sticky note and can be hidden (which means to be marked done) with a click.
The list is represented as a todo
block where every item is name a task
block. As all the tasks are visible by default, it is emphasized by a
visible_yes
modifier.
<ul class="todo ..." data-bem="{ 'todo': {} }">
<li class="todo__task todo__task_visible_yes" title="Click to remove">
<a class="todo__task-inner">
<h2>Lean more about BEM</h2>
Visit bem.info to learn more.
</a>
</li>
...
How the block behaves is described in its todo.js file.
modules.define('todo', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
'js' : {
'inited' : function() {
this.bindTo(this.elem('task'), 'click', function(e) {
this.delMod(e.domElem, 'visible');
});
}
}
}
}));
});
Whenever a user clicks on a task
element the visible
modifier is removed
from it by delMod
modifier.
The delMod
helper can also be used for blocks as the first parameter (an
element object) is optional.
Notice that the bindTo
helper works not with a block but with its elements
here.
NOTE: As it was mentioned above, bindTo
helper listens for every element of
the kind. If this block had 100 task elements, that would mean 100 event watchers.
Moreover, a dynamically added new task should have been provided with an event
listener as well. There is another way to work with the events fully explained in
the live
section. Make sure you have learnt it before starting with a real
powerful application.
Before a modifier is set
pure.bundles/ 006-before-set-mod/ blocks/ page/ accordion-menu/ accordion-menu.bemhtml.js accordion-menu.css accordion-menu.js 006-before-set-mod.bemjson.js 006-before-set-mod.html
Besides the possibility to react on a modifier setting, you can do something before that happens. It is widely adopted for the cases when you need to prevent setting a modifier.
The 006-before-set-mod (BEMJSON) example illustrates such a case with an accordion-menu block.
You can see a menu with a few items on a page. Each of them can reveal
its subitems when being clicked. To do that you need bind to a click
event on
the menu items, set current
modifier into true
for the related item and
ensure that previously selected item is closed (which means its current
modifier is set into false
).
modules.define('accordion-menu',
['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
'js' : {
'inited' : function() {
this._current = this.findElem('item', 'current', true);
this.bindTo('item', 'click', function(e) {
this.setMod($(e.currentTarget), 'current', true);
});
}
}
},
onElemSetMod: {
'item' : {
'current' : {
'true' : function(elem) {
this.delMod(this._current, 'current');
this._current = elem;
}
}
}
}
}));
});
NOTE: You may also take notice that jQuery is used here to wrap the elements and this provides some changes into the code. The bem-core library is based on a ymaps/modules module system. With it each module should be declared before using.
The example becomes more interesting when a disabled item appears. Such an item
has to prevent its being in the current
state. That is always possible to put
an additional condition in the modifier callback but the core provides more
elegant solution. Similar to onSetMod
and onElemSetMod
properties you can
use beforeSetMod
and beforeElemSetMod
to instruct the block component what
to do previously. It is also prevents setting a modifier when a callback related
to the 'before' part returns false
.
modules.define('accordion-menu',
['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) {
provide(BEMDOM.decl(this.name, {
beforeElemSetMod: {
'item' : {
'current' : {
'true' : function(elem) {
return !this.hasMod(elem, 'disabled');
}
}
}
},
...
}));
});
Here it checks if the clicked item is disabled and prevents such an item to be
current
.