bem-site / bem-forum-content-ru

Content BEM forum for Russian speak users
MIT License
56 stars 6 forks source link

БЭМ — это не только про CSS #163

Open tadatuta opened 9 years ago

tadatuta commented 9 years ago

Телезритель из Воронежа Ваня @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.

Если у вас остались вопросы, смело задавайте их в комментариях. Мы обязательно ответим!

voischev commented 9 years ago

Спасибо! Кажется это должны быть серия статей :) теперь хочется узнать про модульную систему с использованием i-bem.js и шаблонизаторы

tadatuta commented 9 years ago

@voischev Давай какие-нибудь наводящие вопросы. Иначе получится пересказ существующей документации.

voischev commented 9 years ago

@tadatuta Можно начать как форму описанную выше представить в i-bem.js плюс показать как круто можно доопределять код в зависимости от уровня. Показать на простом примере и тоже самое как бы это выглядело с использованием инструментов БЭМ. И что таким образом можно положить код в библиотеку блоков и их потом реиспользовать. Зачем вообще можно использовать модульную систему и i-bem.js в простых проектах без сборки bem-tools

Про шаблонизаторы хотелось бы описать проблему реиспользуемости кода и доопределения. Организовать так что бы можно было использовать одну верстку с разными темами оформления. Сравнить какой нибудь шаблонизатор с тем что предлагает БЭМ стек. Например нам нужно делать много лендинг-пейдж и тогда в случае с простым шаблонизатором у нас скорее всего был бы постоянный копи паст. А в случае с bemhtml есть возможность организовать какой то common код и в проекте уже точечно доопределять что-то. Описать например подобную ситуацию — "к нам пришел старый клиент и попросил заменить на всех десяти его лендинг пейджах все списки с ul>li>a на nav>a потому например так СЕОшники сказали, а еще добавить микроразметку к этим спискам"

tadatuta commented 9 years ago

Приведу пример реализации для предпоследнего варианта разметки с небольшими изменениями под i-bem.js:

<form class="form i-bem" data-bem='{"form":{"id":1}}'>
    <input class="form__login input" name="login">
    <input class="form__email input" name="email">
    <button type="submit">Отправить</button>
</form>
<div class="form popup i-bem" data-bem='{"form":{"id":1}}'>Пожалуйста, введите корректный email</div>

Видно, что к форме добавился класс i-bem, который позволяет определить, что на данном DOM-узле необходимо инициализировать JS-блок. Кроме того, в атрибуте data-bem мы явно указываем какому блоку принадлежит id по которому связываем форму и попап. Это необходимо для того, чтобы иметь возможность миксовать на одном DOM-узле несколько разных блоков с JS-реализацией.

// декларируем модуль form, который зависит от i-bem__dom
modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) {

// т.к. модульная система работает асинхронно, передаем модуль в callback
provide(BEMDOM.decl(this.name, { // декларируем блок
    onSetMod: { // когда установится модификатор
        js: { // js
            inited: function() { // в значение inited (это произойдет в момент инициализации блока)
                this.bindTo('submit', this._onSubmit); // начинаем слушать событие submit
            }
        }
    },
    _onSubmit: function(e) {
        // чтобы получить элемент текущей формы, мы просто используем хелпер i-bem.js,
        // нам не нужно заботиться о соблюдении контекста или следить за именами при рефакторинге
        if (/\S+@\S+\.\S+/.test(this.elem('email').val())) return true;
        e.preventDefault();
        // findBlockOn позволяет найти блок на DOM-узле текущего блока. При этом i-bem.js автоматически учитывает, что блок form представлен двумя разными DOM-узлами и ищет по обоим
        // а setMod — работать с модификаторами гораздо удобнее, чем простой конкатенацией классов
        this.findBlockOn('popup').setMod('visible', true);
    }
}));

});

Разумеется, профит от использования фреймворка проявляется тем сильнее, чем больше становится проект.

voischev commented 9 years ago

@tadatuta Сложновато написал реализацию для popup )

tadatuta commented 9 years ago

@voischev сходу не вижу, а что именно тебя смущает?

voischev commented 9 years ago

@tadatuta да это я по началу не понял что ты через микс связал блоки.

voischev commented 9 years ago

@tadatuta Но тут вопрос. А что если у формы будут какие то стили? Они обязательно повлияют на попап? Нам бы этого не хотелось.

voischev commented 9 years ago

И я бы спросил у читателей. Все понимают как подключить модульную систему (ymodules) и i-bem.js к своему проекту. Кто как это делает?

kuflash commented 9 years ago

@voischev ну по поводу подключения i-bem.js я как-то спрашивал и я подключаю просто файлик i-bem.js к странице. Но там нет использования ymodules. Там что-то типа такого кода:

"use strict"

BEM.DOM.decl('b-button', {
    onSetMod : {
        'js' : {
            'inited': function() {
                this.bindTo('click', function(e) {
                    var domElem = $(e.currentTarget); // DOM-элемент, на котором слушается событие
                    // в данном случае то же, что this.domElem
                    this.setMod('size', 'big');
                });
            }
        }
    }
});

BEM.DOM.init($('body'));

А вот как подключить ymodules? P.S. Я не использую полный БЭМ стек. P.P.S: Спасибо за добавление функции предпросмотра сообщений!

voischev commented 9 years ago

@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 возможно будет полезен.

tadatuta commented 9 years ago

@kuflash судя по коду, используется i-bem из bem-bl. У меня в примере из bem-core — можно сказать, следующем поколении bem-bl. Про отличия можно почитать тут — изменения в версии 1.0.0 даны по отношению к bem-bl.

tadatuta commented 9 years ago

@voischev при текущей реализации — да.

Как один из вариантов решения можно завернуть содержимое формы в form__inner и писать стили уже на него. Еще вариант — описывать логику не в универсальной form, а в неком конкретном блоке про получение логина и ящика (скажем, credentials). Тогда никаких пересечений не будет. Еще — описывать внешний вид не прямо в блоке, а в модификаторе про тему, тогда у попапа будет <div class="form popup popup_theme_cool">, так что стили из .form_theme_cool не него не повлияют. Ну и так далее ;)

tadatuta commented 9 years ago

В целом про подключение i-bem.js отдельно мы давно обещаем, что будет возможность подключить предсобранный файл с нашего CDN. К сожалению, все никак не доходят руки :( Но обязательно сделаем и расскажем.

Guria commented 9 years ago

Самая полезная серия постов. Спасибо.

hudochenkov commented 9 years ago

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

<form class="form" action="/">...</form>
<div class="popup form__hint">Пожалуйста, введите корректный email</div>
Guria commented 9 years ago

@hudochenkov В общем случае BEM-дерево не обязано соответствовать DOM-дереву. Вы цитируете "промежуточный" вариант решения проблемы, по сути отражающего ход мысли. Уже в следующем сниппете на вынесенный элемент добавляются 2 существенные детали: класс блока form и js параметр data-id="1" связывающий 2 dom ноды одного js блока.

@tadatuta поправит меня, если я ошибся.

tadatuta commented 9 years ago

@Guria да, все так.

hudochenkov commented 9 years ago

@Guria @tadatuta спасибо. Я думал, что каждый из представленных сниппетов один из вариантов BEM блока, пусть и не всегда самый лучший.

Guria commented 9 years ago

Ну вообще примеры довольно абстрактны и построены на обычном jquery без использования i-bem.js просто для демонстрации подхода применения методологии к javascript.

Guria commented 9 years ago

Более того ни один из примеров не является реализацией "bem-core совместимого БЭМ-блока"

denisnow commented 5 years ago

@tadatuta Скажите, пожалуйста, зачем нужно this в .test($('.input', this) ?