Статья подготовлена по материалам выступления Владимира Варанкина БЭМ и JavaScript: Зачем мы написали JS-фреймворк? на Я.Субботнике в Москве 8 сентября 2012.
В стеке БЭМ-технологий есть блок i-bem библиотеки bem-core, который также называют блоком-хелпером. Его JavaScript-реализация использует предметную область БЭМ и позволяет работать по всем БЭМ-принципам не только с внешним видом компонент, но и с их поведением.
Разработчики старой школы ещё помнят времена, когда не было даже jQuery и все приходилось делать самостоятельно. В каждом проекте был свой common.js
-файл, который включал в себя набор вспомогательных функций. Его копировали из проекта в проект, а потом выносили в свою маленькую JavaScript библиотеку.
Так, эволюционно появлялись JavaScript-фреймворки.
Похожие этапы преодолел и БЭМ в создании своего фреймворка. Сначала понятие блоков (интерфейсных модулей), их элементов и модификаторов существовало только для CSS. Затем разработчики захотели работать с такой же структурой в JavaScript, и использовать ее ключевое понятие — уровни переопределения
— чтобы дополнять и расширять поведение блоков от проекта к проекту.
Так появилась JavaScript реализация блока-хелпера i-bem.js. Он и стал фреймворком (или блоком-ядром) для того, чтобы писать JavaScript в терминах БЭМ.
Как и любой JavaScript-компонент, код, написанный под i-bem.js
должен быть проассоциирован с HTML-фрагментом, который он намерен превратить в функционирующую часть интерфейса. В БЭМ для этого достаточно добавить блоку CSS-класс i-bem
и указать в атрибуте data-bem
параметры блока.
<div lass="myblock i-bem" data-bem="{ myblock: { }}">
<span class="myblock__item"></span>
</div>
В JavaScript-файле блока (myblock.js
) описывается его поведение.
С точки зрения объектной модели все одинаковые блоки образуют класс. При этом каждое появление блока на странице рождает экземпляр этого класса.
Чтобы декларировать новый JS-блок с DOM-представлением (привязанный к HTML-элементу), необходимо доопределить ymaps-модуль i-bem__dom
.
Для описания поведения используется метод decl
, принимающий следующие параметры:
- Блок, о котором пойдёт речь.
- Собственные свойства экземпляра блока.
- Статические свойства класса, к которому принадлежит блок.
modules.define('myblock', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
/\* собственные свойства экземпляра \*/
}, {
/\* статические свойства \*/
}));
});
В JavaScript ссылку на экзепляр всегда можно получить по ключевому слову this
и использовать его зарезервированные поля __self
и __base
.
this.__self
Ссылается на статические методы класса, к которому принадлежит экземпляр.this.__base
Делает super call, то есть вызывает базовую реализацию метода.
Последнее позволяет использовать уровни переопределения. При расширении функциональности уже существующего блока, разработчик всегда имеет доступ к поведению, определённому предыдущим уровнем. То есть методы можно не только полностью перезаписывать, но и «обрамлять» дополнительным поведением.
modules.define('myblock', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl('myblock', {
method: function() {
this.__base();
this.doMore();
}
}));
});
Кроме наследования по уровням переопределения есть ещё возможность явно отнаследовать один блок от другого. Поэтому возможность наследования от уровня к уровню лучше воспринимать как соединение (merge) реализаций.
Для поиска других блоков можно воспользоваться одним из find*
методов. Выбор метода зависит от того, где находится желаемый блок относительно текущего:
// поиск внутри контекста
this.findBlockInside([elem], block)
// поиск снаружи контекста
this.findBlockOutside([elem], block)
// поиск на DOM-узле текущего блока
this.findBlockOn([elem], block)
Все эти методы возвращают JavaScript-объект, экземпляр найденного блока.
Похожим образом можно найти и коллекции блоков:
// поиск внутри контекста
this.findBlocksInside([elem], block)
// поиск снаружи контекста
this.findBlocksOutside([elem], block)
// поиск на BEM-узле текущего блока
this.findBlocksOn([elem], block)
Аналогично, есть методы для доступа к элементам блока: elem
и findElem
. Метод elem
кэширует свой результат при первом обращении. Это его основное отличие от метода findElem
. То есть можно не сохранять вызов в переменную, чтобы сэкономить на поиске — всё уже сделано в реализации этого метода.
//кэширующий селектор
this.elem(name,
[modName], [modVal])
//некэширующий
this.findElem([ctx], name,
[modName], [modVal])
Модификаторы в JavaScript служат для выражения состояния блока или элемента.
Методы работы с модификаторами одинаковы и для блоков, и для элементов. Но первый (опциональный) параметр показывает, о чём идёт речь.
// значение модификатора блока
this.getMod(modName)
// значение модификатора элемента
this.getMod(elem, modName)
// проверка модификатора
this.hasMod([elem], modName, modVal)
// установка модификатора
this.setMod([elem], modName, modVal)
// удаление модификатора
this.delMod([elem], modName)
// переключение значений модификатора
this.toggleMod([elem], modName,
modVal1, modVal2, [condition])
Сами модификаторы описываются как состояния блока. То есть, экземпляр блока знает о том, как ему нужно реагировать на установку модификатора.
Для такого описания используется поле onSetMod
из собственных свойств блока.
modules.define('myblock', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl('myblock', {
onSetMod : {
'mod1' : {
// установка модификатора mod1 в val1
'val1' : function(mod, val, oldVal) {
},
// установка модификатора`mod2` в любое значение
'mod2' : function(mod, val, oldVal) {
}
}
}));
});
Похожая декларация есть и для модификаторов элементов:
modules.define('myblock', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl('myblock', {
// …
onElemSetMod : {
// структура, аналогичная блоку
'elem' : {
'mod1' : {
// дополнительный параметр `elem`
'val1' : function(elem, mod, val, oldVal) {
}
}
}
}
}));
});
События играют ключевую роль в JavaScript. Поэтому и при реализации блока i-bem
их не обошли вниманием. Специальные методы позволяют работать с событиями как на DOM-узлах, соответствующих блокам, так и на BEM-объектах (JavaScript-объектах, представляющих экземпляры блоков).
// DOM-события
this
.bindTo([elem], event, fn)
.unbindFrom([elem], event)
// BEM-события
this
.on(event, [data], fn, [ctx])
.un(event, [data], fn, [ctx])
.trigger(event, [data])
DOM-события не нуждаются в пояснении — это то, что происходит в результате действий пользователя: клик мышкой, работа с клавиатурой, прокрутка и т.д.
BEM-события — это кастомные события, необходимые для возможности организовать API блоков.
Работа блока начинается с его инициализации. В этот момент у блока появляется модификатор js_inited
.
Аналогично другим модификаторам, на его установку можно реагировать исполнением задекларированного кода. То есть, существует возможность написать «конструктор».
onSetMod : {
'js' : {
'inited' : function(){
// «конструктор» блока
}
}
}
Кроме описанного, блок i-bem
предоставляет для других блоков возможности отложенной (ленивой) инициализации, позволяет писать блоки без DOM-представления, использует идею делегирования событий и многое другое.
Об этом можно прочесть на странице блока i-bem.