Телезритель из Воронежа Ваня @voischev задает вопрос на давнюю тему «БЭМ — это не только про CSS», а мы с радостью и отвечаем:
Да, действительно, БЭМ — это не только про CSS.
БЭМ — это про компонентный подход к разработке в целом.
Он предполагает, что каждая полезная сущность (здесь мы называем ее блоком) может быть представлена в одной или сразу нескольких технологиях.
Например, если перед нами логотип, то скорее всего он будет реализован в двух технологиях: шаблоне и стилях.
<a class="logo" href="/">Ваша крутая компания</a>
и
.logo {
width: 150px;
height: 100px;
background: url(logo.png) no-repeat;
}
И шаблон и стили будет удобно положить в одну папку. Так при желании переиспользовать блок мы легко найдем все его части и спокойно перенесем на новое место в отличие от ситуации, когда CSS — это одна «портянка» в папке css/
, а JavaScript — в js/
, и чтобы перенести какую-то часть куда-то, нужно еще долго копаться в контексте.
Разумеется, встречаются блоки, которым требуется еще и JS для работы. Их реализацию по той же логике, что и выше, мы также кладем в папку с блоком. Выглядеть это может, например, так:
$('.logo').on('click', doSomething);
Конечно, бывают ситуации, когда блок состоит только из CSS (например, clearfix или только JS, как, скажем, блок для работы с куками.
Логика при этом не меняется: это по-прежнему блоки, которые по-прежнему имеют свою папку в файловой системе и аналогично другим блокам попадают в сборку.
Следуя все той же логике, мы реализуем разные технологии блока. Ими могут быть не только традиционные CSS и JS, но и картинки, тесты, документация, примеры использования, исходники и так далее. Со временем блок обрастает нужным «мясом», а мы все также легко можем его декомпозировать и масштабировать без урона для проекта.
В качестве примера можно взглянуть на блок button из библиотеки bem-components.
Когда-то давно БЭМ со своим «компонентным» подходом (тогда он еще так не назывался) нес в массы новые, не всегда понятые идеи. Сегодня ситуация изменилась. Этот же компонентный подход уже не нов и реализован не в одном, а многих продуктах, например, в Polymer или даже в стандарте Web Components.
Рассмотрим на примерах.
«Вы говорите, что блоки должны быть независимыми, но на уровне JavaScript они обязаны общаться друг с другом. Как?»
Давайте рассмотрим пример: у нас есть форма, перед отправкой которой необходимо проверить, что введено корректное значение, а в случае ошибки показать попап с предупреждением.
<form class="form" action="/">
<input class="input" name="email">
<input class="button" type="submit">
<div class="popup">Пожалуйста, введите корректный email</div>
</form>
.popup {
display: none;
}
.popup_visible {
display: block;
}
Как это могло быть реализовано в стиле old school?
$('.button').on('click', function(e) {
if (!/\S+@\S+\.\S+/.test($('.input').val())) {
$('.popup').addClass('popup_visible');
return false;
}
});
Всего 6 простых строчек, все работает. Однако, так делать плохо. Почему?
Эти 6 строк кода — отличный пример того, что называют сильной связанностью кода: кнопка «знает» про поле ввода и попап, кроме того, она явно подозревает, что находится внутри формы.
Если в процессе рефакторинга какой-либо из компонентов исчезнет, код сломается. Если появится еще одно поле, код снова сломается. Если вы захотите переиспользовать такую кнопку, вам обязательно придется тянуть за собой все компоненты с такими же классами и гарантировать, что больше нигде на странице они не встречаются.
В результате, несмотря на простоту кода, он доставит вам намного больше серьезных проблем с поддержкой, ведь код сложно поддерживать и практически бесполезно реиспользовать, если только вы не делаете точную копию проекта.
Что можно улучшить?
$('.form').on('submit', function(e) {
if (/\S+@\S+\.\S+/.test($('.input', this).val())) return true;
e.preventDefault();
$('.popup', this).addClass('popup_visible');
});
Что изменилось?
Мы переписали форму так, чтобы за все, что происходит с ней, отвечала она сама. Теперь компоненты внутри ничего не знают о существовании друг друга. А мы можем смело взять кнопку и перенести ее на другой проект, ведь она стала независимой: теперь за ней не потянется знание о какой-то форме, поле ввода и попапе.
Кроме того, мы вынесли все селекторы за рамки контекста формы и теперь можем добавлять сколько угодно новых полей ввода, попапов и кнопок за пределами формы — ничего не сломается.
Есть ли что-то еще, что можно улучшить?
Да. Если мы добавим еще одно поле, придется рефакторить код. Кроме того, чтобы гарантировать перекрытие попапом любых других элементов на странице, нам необходимо положить его в самом конце DOM-дерева, перед закрывающим тегом </body>
.
Продолжаем улучшать
Вынесем попап из формы и добавим еще одно поле. Сами поля смиксуем с элементами формы.
Микс — это объединение нескольких блоков на одном DOM-узле.
<form class="form" action="/">
<input class="input form__login" name="login">
<input class="input form__email" name="email">
<input class="button" type="submit">
</form>
<div class="popup form__hint">Пожалуйста, введите корректный email</div>
Теперь наш код выглядит так:
$('.form').on('submit', function(e) {
if (/\S+@\S+\.\S+/.test($('.form__email', this).val())) return true;
e.preventDefault();
$('.form__hint').addClass('popup_visible');
});
Мы исправили предыдущие проблемы, но появилась новая: если на странице окажется несколько форм, как каждая из них найдет свой попап?
Решение, да не одно
В качестве одного из решений мы можем реализовать механизм, который позволит выражать один блок на нескольких DOM-нодах. Схематично он может выглядеть так:
<form class="form" action="/" data-id="1">
<input class="input form__login" name="login">
<input class="input form__email" name="email">
<input class="button" type="submit">
</form>
<div class="popup form form__hint" data-id="1">Пожалуйста, введите корректный email</div>
Мы добавили форме data-атрибут с идентификатором и помимо элемента примиксовали к попапу саму форму с таким же идентификатором.
Теперь мы можем в коде сказать, что нам нужен элемент hint
именно этого блока form
, а не какого-то другого:
$('.form').on('submit', function(e) {
if (/\S+@\S+\.\S+/.test($('.form__email', this).val())) return true;
e.preventDefault();
$('.form__hint').filter('.form[data-id=' + $(this).data('id') + ']').addClass('popup_visible');
});
Следующее решение поможет нам сохранить независимость блоков, но избавиться от необходимости вносить изменения в DOM. Воспользуемся паттерном проектирования Посредник в очень упрощенном виде.
С ним наши компоненты будут знать о существовании посредника, но не будут ничего знать друг о друге. Вся коммуникация будет происходить на основе сообщений, которые компоненты будут публиковать и слушать на посреднике.
Чтобы максимально упростить пример, сделаем таким посредником body
. Он всегда присутствует в коде и определенно знает о всех компонентах, которые находятся внутри, + может обеспечить обмен сообщениями.
<body class="page">
<form class="form" action="/">
<input class="input form__login" name="login">
<input class="input form__email" name="email">
<input class="button" type="submit">
</form>
<div class="popup"></div>
</body>
var page = $('.page');
page.on('error', function(e, data) {
$('.popup')
.text(data)
.addClass('popup_visible');
});
$('.form').on('submit', function(e) {
if (/\S+@\S+\.\S+/.test($('.form__email', this).val())) return true;
e.preventDefault();
page.trigger('error', 'Ошибка валидации');
});
Теперь в случае ошибки валидации форма сообщит об этом посреднику — page
. Все компоненты, которые должны реагировать на это событие, могут «подписаться» на него через page.on()
.
В качестве еще одного решения можно воспользоваться паттерном MVC и обеспечить валидацию формы на уровне модели.
Подытожим: методология БЭМ — не только про CSS. Она затрагивает все технологии реализации блока, включая JS, и и помогает писать JavaScript-код, который сохранит независимость ваших блоков, упростит рефакторинг и продлит жизнь проекту.
«Зачем нужен i-bem.js
, если можно писать JS для независимых блоков на привычном jQuery
?»
Такой вариант возможен. И более того, i-bem.js
написан с использованием jQuery
.
Зачем же нам понадобился отдельный блок?
Решая одни и те же задачи на JavaScript в терминах блоков, элементов и модификаторов, мы регулярно делали одни и те же действия. И чтобы автоматизировать процесс и избавиться от копипаста, а также предоставить удобные хелперы пользователям, мы написали i-bem.js.
Если у вас остались вопросы, смело задавайте их в комментариях. Мы обязательно ответим!
Спасибо! Кажется это должны быть серия статей :) теперь хочется узнать про модульную систему с использованием i-bem.js и шаблонизаторы
@voischev Давай какие-нибудь наводящие вопросы. Иначе получится пересказ существующей документации.
@tadatuta Можно начать как форму описанную выше представить в i-bem.js плюс показать как круто можно доопределять код в зависимости от уровня. Показать на простом примере и тоже самое как бы это выглядело с использованием инструментов БЭМ. И что таким образом можно положить код в библиотеку блоков и их потом реиспользовать. Зачем вообще можно использовать модульную систему и i-bem.js в простых проектах без сборки bem-tools
Про шаблонизаторы хотелось бы описать проблему реиспользуемости кода и доопределения. Организовать так что бы можно было использовать одну верстку с разными темами оформления. Сравнить какой нибудь шаблонизатор с тем что предлагает БЭМ стек. Например нам нужно делать много лендинг-пейдж и тогда в случае с простым шаблонизатором у нас скорее всего был бы постоянный копи паст. А в случае с bemhtml есть возможность организовать какой то common код и в проекте уже точечно доопределять что-то. Описать например подобную ситуацию — "к нам пришел старый клиент и попросил заменить на всех десяти его лендинг пейджах все списки с
ul>li>a
наnav>a
потому например так СЕОшники сказали, а еще добавить микроразметку к этим спискам"Приведу пример реализации для предпоследнего варианта разметки с небольшими изменениями под
i-bem.js
:Видно, что к форме добавился класс
i-bem
, который позволяет определить, что на данном DOM-узле необходимо инициализировать JS-блок. Кроме того, в атрибутеdata-bem
мы явно указываем какому блоку принадлежитid
по которому связываем форму и попап. Это необходимо для того, чтобы иметь возможность миксовать на одном DOM-узле несколько разных блоков с JS-реализацией.Разумеется, профит от использования фреймворка проявляется тем сильнее, чем больше становится проект.
@tadatuta Сложновато написал реализацию для
popup
)@voischev сходу не вижу, а что именно тебя смущает?
@tadatuta да это я по началу не понял что ты через микс связал блоки.
@tadatuta Но тут вопрос. А что если у формы будут какие то стили? Они обязательно повлияют на попап? Нам бы этого не хотелось.
И я бы спросил у читателей. Все понимают как подключить модульную систему (ymodules) и i-bem.js к своему проекту. Кто как это делает?
@voischev ну по поводу подключения i-bem.js я как-то спрашивал и я подключаю просто файлик i-bem.js к странице. Но там нет использования ymodules. Там что-то типа такого кода:
А вот как подключить ymodules? P.S. Я не использую полный БЭМ стек. P.P.S: Спасибо за добавление функции предпросмотра сообщений!
@kuflash Тоже нужно подключить файлик к странице. Скачивать можно из NPM https://www.npmjs.com/package/ym или https://github.com/ymaps/modules Вот это описание, зачем оно нужно, мне больше всего нравится https://github.com/ymaps/modules/blob/master/what-is-this.md Там как раз пример с формой и кнопкой.
Еще есть тестовый проект на котором я учился работать с модулями https://github.com/voischev/YModules-demo возможно будет полезен.
@kuflash судя по коду, используется
i-bem
изbem-bl
. У меня в примере изbem-core
— можно сказать, следующем поколенииbem-bl
. Про отличия можно почитать тут — изменения в версии1.0.0
даны по отношению кbem-bl
.@voischev при текущей реализации — да.
Как один из вариантов решения можно завернуть содержимое формы в
form__inner
и писать стили уже на него. Еще вариант — описывать логику не в универсальнойform
, а в неком конкретном блоке про получение логина и ящика (скажем,credentials
). Тогда никаких пересечений не будет. Еще — описывать внешний вид не прямо в блоке, а в модификаторе про тему, тогда у попапа будет<div class="form popup popup_theme_cool">
, так что стили из.form_theme_cool
не него не повлияют. Ну и так далее ;)В целом про подключение
i-bem.js
отдельно мы давно обещаем, что будет возможность подключить предсобранный файл с нашего CDN. К сожалению, все никак не доходят руки :( Но обязательно сделаем и расскажем.Самая полезная серия постов. Спасибо.
А разве можно, чтобы элемент был вне ноды родителя? Мой мир рухнул. А если хотим убрать блок со страницы, нужно по всему коду бегать убирать элементы, вне ноды блока?
@hudochenkov В общем случае BEM-дерево не обязано соответствовать DOM-дереву. Вы цитируете "промежуточный" вариант решения проблемы, по сути отражающего ход мысли. Уже в следующем сниппете на вынесенный элемент добавляются 2 существенные детали: класс блока
form
и js параметрdata-id="1"
связывающий 2 dom ноды одного js блока.@tadatuta поправит меня, если я ошибся.
@Guria да, все так.
@Guria @tadatuta спасибо. Я думал, что каждый из представленных сниппетов один из вариантов BEM блока, пусть и не всегда самый лучший.
Ну вообще примеры довольно абстрактны и построены на обычном jquery без использования i-bem.js просто для демонстрации подхода применения методологии к javascript.
Более того ни один из примеров не является реализацией "bem-core совместимого БЭМ-блока"
@tadatuta Скажите, пожалуйста, зачем нужно this в .test($('.input', this) ?