Живая (ленивая) инициализация
Перед началом работы блок инициализируется ядром. В конце этого процесса блок
получает модификатор js_inited
, о котором вы уже знаете.
В процессе инициализации возникает JavaScript-объект, соответствующий экземпляру
блока. Затем запускается callback модификатора js_inited
со всеми инициирующими
действиями.
В предыдущих примерах все блоки инициализировались по domReady
. Хотя на
страницах со множеством блоков нет никакой нужды инициализировать всё сразу.
Иногда пользователь загружает страницу только чтобы нажать на ней одну-единственную кнопку. Так что лучше сэкономить на вычислениях и памяти браузера, проинициализировав компонент только когда пользователь начал с ним работать.
Это называется «живая» или «ленивая» инициализация.
Статический метод live
Инструкции по ленивой инициализации блока даются в статическом методе live
.
modules.define('my-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
...
},
...
}, {
live: function() {
// Здесь можно сказать, когда инициализировать экземпляр блока
}
}));
});
В предыдущих примерах вообще не было статических методов. Это рассматривалось
как то, что свойство live
имеет значение false
.
А в данном примере это функция, так что ядро понимает, что блок не нужно сразу инициализировать, а нужно подождать возникновения специальных условий. Это может быть, например, DOM-событие на блоке или элементе.
Инициализация блока по DOM-событию
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
На странице
010-live-init-on-event.html
(BEMJSON)
вы можете увидеть историю, написанную на голландском языке. На самом деле текст
разделен на множество маленьких кусочков по фразам. Каждая фраза завернута в
блок translate
.
Если пользователь, читающий текст, не понимает его значения, он может увидеть перевод, кликнув по нужной фразе.
<span
class="translate i-bem"
data-bem="{'translate':{'prompt':'один мужчина заходит на почту;'}}">
Een man gaat een postkantor binnen
<i class="translate__prompt"></i>
</span>
Как видно из HTML-структуры, блок translate
содержит фразу на голландском
языке, а ее русский перевод хранится как параметр блока в атрибуте data-bem
.
Также есть элемент prompt
, которого не видно, пока он не понадобится.
Обратите внимание, что у блока нет класса translate_js_inited
, он не
появляется даже при полной загрузке страницы. Это означает, что и
JavaScript-объект, соответствующий экземпляру блока, пока не создан.
В файле
translate.js
сказано, что блок нужно инициализировать тогда, когда на его DOM-узле случится
событие click
.
modules.define('translate', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
...
},{
live: function() {
this.liveInitOnEvent('click');
}
}));
});
По клику ядро устанавливает для блока модификатор js_inited
и запускает
конструктор — функцию, проассоциированную с этим модификатором.
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']);
}
}
}
},{
...
}));
});
Вложенному элементу prompt
назначается модификатор visible
со значением
true
, что делает его видимым на странице. Также это означает, что из
параметров блока берётся перевод — свойство this.params.prompt
— и
вставляется внутрь элемента.
На самом деле текст перевода можно было и раньше вставить в элемент, ведь он не
был виден пользователю. Но для демонстрации того, как получать параметры из
data-bem
, выбран этот вариант.
Возвращаясь к живой инициализации вы можете увидеть, что на странице со множеством блоков ядро инициализирует только те, на которых произошло слушаемое событие. Это экономит память и страница работает быстрее.
В основе лежит
делегирование событий. То есть, несмотря
на то, что блоков много, есть всего один обработчик события click
на объекте
document
.
Это не только выгодно с точки зрения производительности, но и добавляет гибкости для программирования динамически изменяемых страниц. В этом вы можете убедиться на следующем примере.
Делегированная инициализация
В этом примере используется блок translate
— тот же самый, что и в предыдущем.
Но кроме этого там есть страшный JavaScript. Он срабатывает, если пользователь
нажал на розовую кнопку и динамически вставляет в страницу несколько новых
блоков translate
. Если после этого вы кликните на некоторые фразы свежей
шутки, то увидите, что эти блоки работают абсолютно так же, как и те, что
присутствовали на странице с самого начала.
Ядро фреймворка i-bem слушает события на объекте document
. Когда пользователь
кликает по блокам, событие поднимается наверх до document
, и ядро инициализирует
блок, следуя инструкциям в секции live
.
Обработчики live событий
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
Следующий пример — страница со 100 BonBon кнопками (BEMJSON) — показывает, что реагировать на live события можно всегда, а не только при инициализации.
У представленного на странице блока button
есть live инициализация. Было бы
неразумно инициализировать их все сразу, и на каждом слушать событие click
.
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');
}
}));
});
Также как и в предыдущем примере (где использовался liveInitOnEvent
), этот код
инициализирует блок и запускает callback модификатора js_inited
.
Но разница состоит в том, что liveBindTo
говорит запускать callback не только при
инициализации, а каждый раз когда пользователь кликает по кнопке.
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();
});
}
}));
});
Инициализация по нескольким событиям
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
В предыдущих примерах инициализация блоков проходила по возникновению на них
события click
. Но иногда слушать одно-единственное событие недостаточно.
Пример
012-live-init-many-events
(BEMJSON)
демонстрирует такой случай на кастомизированном checkbox.
<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>
Очевидно, что экземпляр блока должен быть инициализирован, когда пользователь
кликает по элементу label
.
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();
});
}
}));
});
Здесь тоже используется метод liveBindTo
, чтобы не только проинициализировать
блок, но и запускать callback на следующих кликах. Заметьте, что здесь
использован опциональный параметр с именем элемента label
, т.к. нас интересуют
клики именно на нём.
Кроме того, контрол может быть изменен с клавиатуры (или другим JavaScript), и это тоже нужно учесть.
Внутри метода live
можно поместить столько инструкций по инициализации,
сколько нужно для данного блока. Здесь она случается по клику на элементе
label
и по событию change
элемента control
(это узел 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);
});
}
}));
});
Также блок должен быть проинициализирован, если он оказывается в фокусе и если фокус уходит из него.
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');
});
}
}));
});
Как видно из кода, один и тот же callback можно привязать к нескольким событиям. Для этого имена событий пишутся через пробел.
После добавления функций callback на модификаторы, программирование блока закончено.
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() {
...
}
}));
});
Такой подход позволяет описать компонент консистентно. Вне зависимости от того, как начнет использоваться блок — взаимодействуя с пользователем, или из другого JavaScript страницы, или реагируя на события в браузере — он будет работать как задумано.
Получив модификатор focused
, блок установит фокус на встроенном в него input
.
И наоборот — увидев, что встроенный input
по каким-то причинам получил фокус, блок
установит себе модификатор focused
и таким образом приобретет нужный внешний вид.
При изменении блока — и вручную, и автоматически — блок получит модификатор
checked
и задаст атрибут checked
встроенному input
или снимет их.
Почему не :checked?
Как вы могли заметить, в этом примере выделенность встроенного элемента control
(input) проверяется при помощи модификатора checked
соответствующего блока.
<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;
}
Конечно можно было бы воспользоваться псевдо-селектором :checked
как это
сделано здесь: control prototype.
.checkbox input[type=checkbox]:checked + label {
left: 54px;
}
.checkbox input[type=checkbox]:checked + label:after {
background: #00bf00;
}
Но использование модификаторов более гибкое, и позволяет всему блоку менять внешний вид при выделении.
.checkbox_checked
{
background-image: linear-gradient(0deg, #333, #333 4px, #555 4px, #555 6px);
background-size: 6px 6px;
}
И, конечно, позволяет экономить на парсинге селекторов, и делает код более консистентным.
БЭМ-события
Кроме DOM-событий i-bem.js умеет работать с кастомными JavaScript событиями на объектах, соответствующих блокам. Эти события назваются БЭМ-события и обычно составляют API блока.
Примером порождения таких событий может быть
блок link
библиотеки bem-components.
На JavaScript-объекте, соответствующем блоку, возникает кастомное событие click
в
том случае, если пользователь кликает по ссылке левой кнопкой, и ссылка не является
неактивной в данный момент.
Запускается это событие при помощи метода emit
.
_onClick : function(e) {
e.preventDefault();
this.hasMod('disabled') || this.emit('click');
}
Таким образом, блок link
получает API, и им может воспользоваться любой другой
блок страницы.
Другим примером может служить блок
menu
.
Он представляет список пунктов меню в HTML, один из которых может быть выделен.
<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>
Блок слушает DOM-события click
на своих элементах item-selector
и запускает
событие current
, сигнализирующее о смене выделенного пункта меню и дающее
данные о текущем пункте.
this
.delMod(prev, 'state')
.emit('current', {
prev : prev,
current : elem
});
Это событие возникает на JavaScript-объекте, соответствующем экземпляру блока.
Используя это, другой блок может подписаться на БЭМ-событие current
блока
menu
, узнавать об изменении текущего пункта меню и реагировать на это.
Live инициализация по БЭМ-событию вложенного блока
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
На этом примере вы можете видеть блок
map-marks
.
Он связывает вместе блоки меню и карты таким образом, что пользователь кликающий
по меню, может видеть соответствуюшую точку на карте.
Блок map-marks
содержит блок menu
и map
. Это можно увидеть из bemjson описания
страницы
или заглянув в HTML
014-live-init-bem-event.html.
Этот блок нужен только при взаимодействии пользователя со страницей. Поэтому
блок использует live-инициализацию, где сказано инициализировать блок только
тогда, когда на вложенном в него блоке menu
возникнет событие current
.
JavaScript реализация блока map-marks.js использует live-инициализацию, зависящую от вложенного блока.
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);
});
}
}));
});
Методу liveInitOnBlockInsideEvent
в качестве параметров передаются имя события,
имя блока и callback.
Как только пользователь кликает по какому-нибудь пункту меню, он становится
активным, и на блоке menu
возникает событие current
. Событие ловится блоком
map-marks
, и блок инициализируется. То есть блок приобретает модификатор
js_inited
, и запускается соответствующий callback:
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() {
...
}
}));
});
Затем вызывается метод _showMap
экземпляра блока. Он показывает точку на карте,
обращаясь к блоку map
.
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() {
...
}
}));
});