Skip to content

Latest commit

 

History

26 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jQuery BEM

Это инструмент, предназначенный для описания поведения DOM элемента в BEM терминологии. demo

Зачем это нужно?

Каждый из нас, хоть раз, сталкиваться с задачей, в рамках которой нужно стилизовать элемент формы или сделать какой-то свой контрол. И тут приходится сделать выбор, отправиться в гугл или написать самому. Знакомая история, не правда ли?

Мне попадались разные библиотеки, но у них было одно общее - их нужно иницализировать. Есть конечно такие, которые делают инициализацию по DOMReady. Но что делать с теми элементами, которые мы создали позже, например, получив контен через ajax?

Кроме того, инициализация по DOMReady замедляет загрузку страницы. В тоже время, велика вероятность, что пользователь не использует стилизованный элемент, а он может быть не один. Тогда зачем нужно тратить рессурсы на их инициализацию?

Все эти проблемы меня очень печалили, а ведь хотелось простого - чтобы все работало само и по необходимости, как бы смешно это не звучало :]

Идея сформировалась давно и не нова. Суть её проста - в верстке мы используем CSS для определения стиля элемента, описываем селектор и дальше применяем его к нужным элементам. Еще есть BEM c блок, элемент и модификатор, осталось только описать поведение для этого нужного блока, и дело в шляпе

Для себя я сформировал следующие требования:

  • Все необходимые элементы находятся в верстке изначально. Ничего не генирируется скриптом во время исполнения страницы.
  • Инициализация происходит по необходимости, т.е. перед непосредственным использованием
  • Поведение максимально приближено к нативному
  • Код прозрачен как для верстальшика, так и разработчика
  • Расширямость (возможность написания своих компонентов)

В итоге, я получил иструмент, который позволил мне описывать поведение элемента для нужного селектора. Если применить класс b-buttonк некоторому элементу, то этот элемент будет вести себя, как кнопка, со всеми состояниями и событиями присущими кнопке. И самое замечательное, что не нужно беспокоиться о его инициализиции.

Пример 1

Рассмотрим простой пример, вам нужно, чтобы при наведении на элемент, к нему добавлялся модификатор _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>

Пример 2

Пример посложней, нам нужно сделать счетчик подсчета введенных символов:

<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');
		}
	}
});

b-button

Вот и настало время написать полноценный элемент-контрол-кнопка

<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

  • b-control_hover
  • b-control_focus
  • b-control_press
  • b-control_disabled

b-button <- b-control

<a href="#" class="b-button">Кнопка</a>

b-checkbox <- b-control

  • 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>

b-radio <- b-checkbox

<span class="b-radio">
	<span class="b-radio__bubble">
		<input name="radio" type="radio"/>
	</span>
</span>

b-list

  • 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>

b-dropdown <- b-control

  • 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

  • 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>

API

Создание описания

$.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

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
	}
}

onElemMod

{
	'element': {
		'modName': function ($el, state, mod, elemName) {
			// ..
		},

		'*': function ($el, mod, state, elemName) {
			// установка любого модификатор
		}
	},


	// установка любого модификатор, на любой элемент
	'*': function ($el, elemName, mod, state) {
		if (this.isDisabled()) { // блок задизаблен
			// Запрещаем смену модификаторов у элементов
			return false;
		}
	}
}

About

jQuery BEM (Development of the project is closed)

Resources

Stars

20 stars

Watchers

2 watching

Forks

Used by

Contributors

Languages