Живая (ленивая) инициализация
Перед началом работы блок инициализируется ядром. В конце этого процесса блок
получает модификатор js_inited, о котором вы уже знаете.
В процессе инициализации возникает JavaScript-объект, соответствующий экземпляру
блока. Затем запускается callback модификатора js_inited со всеми инициирующими
действиями.
В предыдущих примерах все блоки инициализировались по domReady. Хотя на
страницах со множеством блоков нет никакой нужды инициализировать всё сразу.
Иногда пользователь загружает страницу только чтобы нажать на ней одну-единственную кнопку. Так что лучше сэкономить на вычислениях и памяти браузера, проинициализировав компонент только когда пользователь начал с ним работать.
Это называется «живая» или «ленивая» инициализация.
Статические свойства lazyInit и onInit
Инструкции по ленивой инициализации блока даются в статических свойствах
lazyInit и onInit.
modules.define('my-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {
onSetMod: {
...
},
...
}, {
lazyInit: true,
onInit: function() {
// Здесь можно сказать, когда инициализировать экземпляр блока
}
}));
});
В предыдущих примерах вообще не было статических методов. Это рассматривалось
как то, что свойство lazyInit имеет значение false.
А в данном примере lazyInit: true говорит ядру, что блок не нужно сразу
инициализировать. В onInit можно подписаться на условия, которые создадут
экземпляр блока: например, на 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.declBlock(this.name, {
...
},{
lazyInit: true,
onInit: function() {
this._domEvents().on('click', function() {});
}
}));
});
По клику ядро устанавливает для блока модификатор js_inited и запускает
конструктор — функцию, проассоциированную с этим модификатором.
modules.define('translate', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {
onSetMod: {
'js' : {
'inited' : function() {
var prompt = this._elem('prompt');
prompt.domElem.text(this.params.prompt);
prompt.setMod('visible', true);
}
}
}
},{
lazyInit: true,
onInit: function() {
this._domEvents().on('click', function() {});
}
}));
});
Вложенному элементу prompt назначается модификатор visible со значением
true, что делает его видимым на странице. Также это означает, что из
параметров блока берётся перевод — свойство this.params.prompt — и
вставляется внутрь элемента.
На самом деле текст перевода можно было и раньше вставить в элемент, ведь он не
был виден пользователю. Но для демонстрации того, как получать параметры из
data-bem, выбран этот вариант.
Возвращаясь к живой инициализации вы можете увидеть, что на странице со множеством блоков ядро инициализирует только те, на которых произошло слушаемое событие. Это экономит память и страница работает быстрее.
В основе лежит
делегирование событий. То есть, несмотря
на то, что блоков много, есть всего один обработчик события click на объекте
document.
Это не только выгодно с точки зрения производительности, но и добавляет гибкости для программирования динамически изменяемых страниц. В этом вы можете убедиться на следующем примере.
Делегированная инициализация
В этом примере используется блок translate — тот же самый, что и в предыдущем.
Но кроме этого там есть страшный JavaScript. Он срабатывает, если пользователь
нажал на розовую кнопку и динамически вставляет в страницу несколько новых
блоков translate. Если после этого вы кликните на некоторые фразы свежей
шутки, то увидите, что эти блоки работают абсолютно так же, как и те, что
присутствовали на странице с самого начала.
Ядро фреймворка i-bem слушает события на объекте document. Когда пользователь
кликает по блокам, событие поднимается наверх до document, и ядро инициализирует
блок, следуя инструкциям из onInit.
Обработчики событий ленивой инициализации
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) — показывает, что реагировать на события ленивой инициализации можно всегда, а не только при инициализации.
У представленного на странице блока button есть ленивая инициализация. Было бы
неразумно инициализировать их все сразу, и на каждом слушать событие click.
modules.define('button', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {
onSetMod: {
'js' : {
'inited' : function() {
var button = this.domElem[0].innerHTML;
console.log('Here an object of ' + button + ' comes. Just once.');
}
}
},
_onClick: function() {
console.log('Here I can track clicks');
}
},{
lazyInit: true,
onInit: function() {
this._domEvents().on('click', this.prototype._onClick);
}
}));
});
Так же как и в предыдущем примере, этот код
инициализирует блок и запускает callback модификатора js_inited.
Разница состоит в том, что _domEvents().on() запускает callback не только при
инициализации, а каждый раз когда пользователь кликает по кнопке.
modules.define('button', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {
onSetMod: {
...
},
_onClick: function() {
console.log('Here I can track clicks');
}
},{
lazyInit: true,
onInit: function() {
this._domEvents().on('click', this.prototype._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.declBlock(this.name, {
...
_onClick : function() {
this.setMod('focused', true);
},
...
},{
lazyInit: true,
onInit: function() {
this._domEvents('label').on('click', this.prototype._onClick);
}
}));
});
Здесь тоже используется метод _domEvents().on(), чтобы не только проинициализировать
блок, но и запускать callback на следующих кликах. Заметьте, что здесь
использован опциональный параметр с именем элемента label, т.к. нас интересуют
клики именно на нём.
Кроме того, контрол может быть изменен с клавиатуры (или другим JavaScript), и это тоже нужно учесть.
Внутри метода onInit можно поместить столько инструкций по инициализации,
сколько нужно для данного блока. Здесь она случается по клику на элементе
label и по событию change элемента control (это узел input).
modules.define('checkbox', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {
...
_onClick : function() {
this.setMod('focused', true);
},
_onChange : function(e) {
this.setMod('checked', e.target.checked);
}
},{
lazyInit: true,
onInit: function() {
this._domEvents('label').on('click', this.prototype._onClick);
this._domEvents('control').on('change', this.prototype._onChange);
}
}));
});
Также блок должен быть проинициализирован, если он оказывается в фокусе и если фокус уходит из него.
modules.define('checkbox', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {
...
},{
lazyInit: true,
onInit: function() {
this._domEvents('label').on('click', this.prototype._onClick);
this._domEvents('control').on('change', this.prototype._onChange);
this._domEvents('control').on('focusin focusout', this.prototype._onFocusChange);
}
}));
});
Как видно из кода, один и тот же callback можно привязать к нескольким событиям. Для этого имена событий пишутся через пробел.
После добавления функций callback на модификаторы, программирование блока закончено.
modules.define('checkbox', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {
onSetMod: {
'focused' : {
'true' : function() {
this._elem('control').domElem.focus();
},
'' : function() {
this._elem('control').domElem.blur();
}
},
'checked' : function(modName, modVal) {
this._elem('control').domElem.attr('checked', modVal ? 'checked' : false);
}
},
...
},{
lazyInit: true,
onInit: 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, сигнализирующее о смене выделенного пункта меню и дающее
данные о текущем пункте.
prev && prev !== this && prev.delMod('state');
menu._emit('current', {
prev : prev,
current : this
});
Это событие возникает на JavaScript-объекте, соответствующем экземпляру блока.
Используя это, другой блок может подписаться на БЭМ-событие current блока
menu, узнавать об изменении текущего пункта меню и реагировать на это.
Ленивая инициализация по БЭМ-событию вложенного блока
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.
Этот блок нужен только при взаимодействии пользователя со страницей. Поэтому
блок использует ленивую инициализацию: экземпляр создаётся только тогда, когда
на вложенном в него блоке menu возникает событие current.
JavaScript реализация блока
map-marks.js
использует подписку на БЭМ-событие вложенного блока из статического метода
onInit.
modules.define('map-marks', ['i-bem-dom', 'map', 'menu'], function(provide, bemDom, Map, Menu) {
provide(bemDom.declBlock(this.name, {
_onMenuCurrent: function(e, data) {
this._showMap(data.current);
}
}, {
lazyInit: true,
onInit: function() {
this._events(Menu).on('current', this.prototype._onMenuCurrent);
}
}));
});
Методу _events(Menu).on() передаются имя события и callback.
Так как подписка выполнена из класса блока, обработчик сначала инициализирует
ближайший экземпляр map-marks, внутри которого произошло событие, а затем
вызывает callback в контексте этого экземпляра.
Как только пользователь кликает по какому-нибудь пункту меню, он становится
активным, и на блоке menu возникает событие current. Событие ловится блоком
map-marks, и блок инициализируется. То есть блок приобретает модификатор
js_inited, и запускается соответствующий callback:
modules.define('map-marks', ['i-bem-dom', 'map', 'menu'], function(provide, bemDom, Map, Menu) {
provide(bemDom.declBlock(this.name, {
onSetMod: {
'js' : {
'inited' : function () {
this._menu = this.findChildBlock(Menu);
this._map = this.findChildBlock(Map);
}
}
},
...
}, {
lazyInit: true,
onInit: function() {
this._events(Menu).on('current', this.prototype._onMenuCurrent);
}
}));
});
Затем вызывается метод _showMap экземпляра блока. Он показывает точку на карте,
обращаясь к блоку map.
modules.define('map-marks', ['i-bem-dom', 'map', 'menu'], function(provide, bemDom, Map, Menu) {
provide(bemDom.declBlock(this.name, {
...
_showMap: function(elem) {
var params = elem.params;
this._map.showAddress(params['address']);
}
...
}, {
lazyInit: true,
onInit: function() {
this._events(Menu).on('current', this.prototype._onMenuCurrent);
}
}));
});