Это инструмент, предназначенный для описания поведения DOM элемента в BEM терминологии. demo
Каждый из нас, хоть раз, сталкиваться с задачей, в рамках которой нужно стилизовать элемент формы или сделать какой-то свой контрол. И тут приходится сделать выбор, отправиться в гугл или написать самому. Знакомая история, не правда ли?
Мне попадались разные библиотеки, но у них было одно общее - их нужно иницализировать. Есть конечно такие, которые делают инициализацию по DOMReady. Но что делать с теми элементами, которые мы создали позже, например, получив контен через ajax?
Кроме того, инициализация по DOMReady замедляет загрузку страницы. В тоже время, велика вероятность, что пользователь не использует стилизованный элемент, а он может быть не один. Тогда зачем нужно тратить рессурсы на их инициализацию?
Все эти проблемы меня очень печалили, а ведь хотелось простого - чтобы все работало само и по необходимости, как бы смешно это не звучало :]
Идея сформировалась давно и не нова. Суть её проста - в верстке мы используем CSS для определения стиля элемента, описываем селектор и дальше применяем его к нужным элементам. Еще есть BEM c блок, элемент и модификатор, осталось только описать поведение для этого нужного блока, и дело в шляпе
Для себя я сформировал следующие требования:
- Все необходимые элементы находятся в верстке изначально. Ничего не генирируется скриптом во время исполнения страницы.
- Инициализация происходит по необходимости, т.е. перед непосредственным использованием
- Поведение максимально приближено к нативному
- Код прозрачен как для верстальшика, так и разработчика
- Расширямость (возможность написания своих компонентов)
В итоге, я получил иструмент, который позволил мне описывать поведение
элемента для нужного селектора. Если применить класс b-buttonк некоторому элементу,
то этот элемент будет вести себя, как кнопка, со всеми состояниями
и событиями присущими кнопке. И самое замечательное, что не нужно беспокоиться о его инициализиции.
Рассмотрим простой пример, вам нужно, чтобы при наведении на элемент, к нему добавлялся
модификатор _hover, а при фокусе (например при помощи tab) _focus:
<a href="#" class="link" tabindex="1">link</a>
или
<span class="link" tabindex="2">...</span>$.bem('link', null, {
// static methods and properties
mods: 'focus hover'
});И все, больше никаких телодвижений, теперь если вы наведетесь на элемент с классом "link", то получите:
<a href="#" class="link link_hover" tabindex="1">link</a>Пример посложней, нам нужно сделать счетчик подсчета введенных символов:
<div class="b-input">
<input value="" type="text" class="b-input__input" />
<span class="b-input__length"></span>
</div>$.bem('b-input', {
_onKeyUpCalc: function (){
// Работа с элементами, внутри блока
this.$('__length').text( this.$('__input').val().length );
// можно и так
// this.$('.b-input__length').text( this.$(':input').val().length );
}
}, {
cache: true, // кешировать выборки
live: {
'focusin focusout': function (evt){
// обработчик события "keyup" назначаем в зависимости от фокуса
this[evt.type == 'focusin' ? 'on' : 'off']('keyup.calc', '_onKeyUpCalc');
}
}
});Вот и настало время написать полноценный элемент-контрол-кнопка
<a href="..." class="b-button" tabindex="1">
<span class="b-button__label">button</span>
<input type="submit" class="b-button__input" />
</a>$.bem('b-button', {
role: 'button',
onMod: {
disabled: function (state){
var attrs = { disabled: state };
// Сохраним url ссылки
this._href = state || !this._href ? this.$attr('href') : this._href;
this
.$aria(attrs)
.$attr(attrs)
.$attr('href', state ? null : this._href) // установить или удалить href, в зависимости от состояния
;
if( state ){
// кнопка disabled, так что удалим все модификаторы
this.delMod('hover focus press');
}
},
press: function (state){
this.trigger(state ? 'press' : 'release');
},
hover_no: function (){
this.delMod('press');
},
focus: function (state){
if( state ){
// вешаем событие за пределами элемента
this.onOutside('keydown.focus', 'onFocusKeyDown');
} else {
this.offOutside('keyup.focus keydown.focus');
}
},
'*': function (mod, state){
if( state && this.isDisabled() ){
return !~'press hover focus'.indexOf(mod);
}
}
},
onClick: function (evt){
if( !this.isDisabled ){
this.$('__input').click();
}
},
onFocusKeyDown: function (evt){
if( !this.hasOn('keyup.focus') ){
this.on('keyup.focus', function (evt){
if( this.hasMod('press') ){
this.onClick(evt);
}
this.delMod('press').off('keyup.focus');
});
}
var key = evt.keyCode;
if( key == 13 || key == 32 ){
this.addMod('press');
evt.preventDefault();
}
}
}, {
mods: 'hover press focus',
live: {
leftclick: 'onClick'
}
});$.bem(['b-button', 'b-submit'], {
onMod: {
'*': function (mod, state){
var ret = this.parent(mod, state);
// you logic ...
}
},
onFocusKeyDown: function (evt){
// you logic ...
this.parent(evt); // call parent method
}
});- b-control_hover
- b-control_focus
- b-control_press
- b-control_disabled
<a href="#" class="b-button">Кнопка</a>- Space OR Enter — for toggle "checked"
- b-checkbox_checked
- b-checkbox_checked_mixed
<span class="b-checkbox">
<span class="b-checkbox__checkmark">
<input name="cbx" type="checkbox"/>
</span>
</span><span class="b-radio">
<span class="b-radio__bubble">
<input name="radio" type="radio"/>
</span>
</span>- UP/DOWN arrows — move "b-list__item_hover" between "b-list__item"
- b-list_focus
- b-list_active
- b-list__item_hover
<ul class="b-list">
<li class="b-list__item">item 1</li>
<li class="b-list__item">item 2</li>
</ul>- ESC — remove "expanded"
- Space OR Enter — toggle "expanded"
- UP/DOWN arrows — move "b-dropdown__list__item" between "b-dropdown__list__item_hover", if before include b-list
- b-dropdown_expanded
- b-dropdown__ctrl_focus
- b-dropdown__ctrl_hover
- b-dropdown__ctrl_expanded
- b-dropdown__list_expanded
- b-dropdown__list__item_hover — if before define b-list
<div class="b-dropdown">
<div class="b-dropdown__ctrl">text</div>
<div class="b-dropdown__list">
<div class="b-dropdown__list__item">item 1</div>
<div class="b-dropdown__list__item">item 2</div>
</div>
</div>- b-filter__list__item_filtered — hidden element
<div class="b-filter">
<input type="text" class="b-filter__input" />
<div class="js-filter-item">item 1</div>
<div class="js-filter-item">item 2</div>
</div>$.bem(className:String, methods:Object, statics:Object);
- className — название css-класса, для которого описываем поведение
- methods — методы экземпляра класса
- statics — статические методы класса
$.bem(className:String, extend:String, methods:Object, statics:Object);
- extend — название того, кого наследуем
- .$win
:jQuery— ссылка на $(window) - .$doc
:jQuery— ссылка на $(document) - .lazy
:Boolean = false— ленивая инициализация - .cache
:Boolean = false— кешировать все выборки - .forced
:Boolean = false— принудительно инициализировать все элементы - .live
:Object— делигируемые события - .mods
:Set(hover,focus,press)— авто-модификаторы (перечисление через пробел) - .attrs
:Object— аттрибуты, которые необходимо выставить DOM-элементу
- .self
:BEM— ссылка на класс, для доступа к статическим методам и свойствам - .boundAll
:String— название методов, через пробел которые нужно привязать к контексту инстанса - .debouceAll
:String— формат записи "methodName:mSec" - .cache
:Boolean— кешировать выборки - .forced
:Boolean— инициализировать объект сразу, после его появления - .uniqId
:Number— уникальный модификатор в рамках BEM-элементов - .role
:String— role-атрибут - .$el
:jQuery— ссылка на jQuery-элемент - .el
:HTMLElement— ссылка на DOM-элемент - onMod
:Object— список слушателей, на установку того или иного модификатора
- .init() — вызывается при инициализация объекта (@protected)
- .getId()
:Number— получить уникальный идентификатор - .bound(fn
:Function|String[, arg1[, argsN]]):Function— связать функцию с контекстом класса - .debounce(fn
:Function|String[, delay:Number]):Function— вызов функции будет произведен только один раз, через N ms - .throttle(fn
:Function|String[, delay:Number]):Function— вызов функции будет произведен только один раз в N ms - .hasMod(mod
:String[, state:Mixed]):Boolean— проверить наличие модификатора - .addMod(mods
:String[, state:Mixed]):this— добавить список модификатор, разделитель пробел - .delMod(mods
:String[, state:Mixed]):this— убрать модификаторы - .toggleMod(mod
:String[, state:Mixed])):this— addMod/delMod - .$()
:this.$el— вернет ссылку на элемент - .$(selector
:String):jQuery— найти все элементы соответствующие css-селектору в this.$el - .$(__name
:String):jQuery— найти элементы в соответствии c BEM именованием - .$attr(name
:String):Mixed— получить значение атрибута - .$attr(name
:String, value:Mixed):this— изменить атрибут - .$attr(attrs
:Object):this- изменить атрибуты - .$attr(name
:String, null):this— установка атрибута в null равносильно его удалению - .$attr(selector
:String, attrs:Object):this— изменить атрибуты для элементов, соответствующих css-селектору - .$aria() - тоже самый $attr, только ко всем атрибутам добавляем префикс "aria-"
- .$css(), $prop() — аналогично $attr
- .on(name
:String, fn:String|Function):this— подписаться на событие, this.$el.bind(name, fn) - .on(name
:String, selector:String, fn:String|Function):this- слушать событие с конкретных элементов, this.$el.delegate(selecotr, name, fn) - .off(name
:String):this— убрать всех слушателей, this.$el.unbind(name) - .off(name
:String, selector:String):this— снять слушателя, с конкретных элементов, this.$el.undelegate(selecotr, name) - .hasOn(name
:String):Boolean— проверить наличие подписки на конкретное событие - .onOutside(name
:String, fn:String|Function):this— подписаться на событие, за пределами элемента - .offOutside(name
:String):this— убрать слушателя - .trigger(name
:String[, args:Array]):this— испустить событие - .isDisabled()
:Boolean— проверить элемент на наличие модификатора disabled - .destroy() — уничтожить экземпляр класса
- .destroy(true) — уничтожить + удалить связанные элемент
onMod: {
size: {
S: function (){ /* set: _size_S */ },
M: function (){ /* set: _size_M */ },
L: function (){ /* set: _size_S */ },
'': function (){ /* remove mod */ }
},
size_XL: function (){}
focus: function (state/**Mixed*/){
// (2) after "*"
},
focus_yes: function (){
// (3) call after "focus"
},
'*': function (mod/**String*/, state/**Mixed*/){
// (1) Call before set modifier
// return false, to break
}
}{
'element': {
'modName': function ($el, state, mod, elemName) {
// ..
},
'*': function ($el, mod, state, elemName) {
// установка любого модификатор
}
},
// установка любого модификатор, на любой элемент
'*': function ($el, elemName, mod, state) {
if (this.isDisabled()) { // блок задизаблен
// Запрещаем смену модификаторов у элементов
return false;
}
}
}