К хорошему привыкаешь быстро, особенно, когда верстаешь страницу в стеке Vue + Pug + Bem!
На мой взгляд это наиболее удобный стек для верстки.
Вот как выглядит код на этом стеке:
+b.product
+e.inner
+e.image
+e.info
+e.H2.title {{ product.title }}
+e.price._new {{ product.price }}
А так этот же код выглядит на чистом HTML:
<div class="product">
<div class="product__inner">
<div class="product__image"></div>
<div class="product__info">
<h2 class="product__title">{{ product.title }}</h2>
<div class="product__price product__price_new">{{ product.price }}</div>
</div>
</div>
</div>
Как видно из примера — вместо каждого +e дописывается название, взятое из предыдущего +b.
Синтаксис таков:
- Часть названия класса, которая идет после точки компилируется в имя дочернего элемента:
+e.inner
>product__inner
; - Часть названия класса, которая идет после точки и начинается с символа нижнего подчеркивания компилируется в имя миксина:
+e.price._new
>product__price product__price_new
;
Работая с WordPress реализовать такую роскошь для создания шаблонов сложно, хотя можно… но кому это вообще надо? )
Делая очередной шаблон страницы на WordPress я очень скучал по простому созданию классов в стиле БЭМ и решил хоть как-тот облегчить себе задачу.
Техническое задание
Без использования шаблонизатора не удастся легко и просто реализовать работу через псевдо-переменные +b
и +e
, поэтому придется придумать что-то попроще.
Должен быть реализован синтаксис создания классов дочерних элементов и миксинов.
- Если указано название класса, не начинающееся с точки, это должен быть родительский класс, например
product
; - Если за родительским классом идет один или более классов, начинающихся с точки, после которой нет символа нижнего подчеркивания, то должно быть создано столько экземпляров дочерних классов, сколько объявлено, например:
product.one.two.three
>product__one product__two product__three
; - Если после точки идет символ нижнего подчеркивания — это миксин и он должен быть дописан к каждому классу, например:
product.one.two.three._first._second
>product__one product__two product__three product__one_first product__one_second product__two_first product__two_second product__three_first product__three_second
.
Реализация
Я хочу, чтоб это было максимально просто, на сколько это возможно в условиях создания шаблонов в WordPress, то есть в условия написания кода на чистом PHP.
Пусть функция будет называться bem
и для начала пусть она принимает один параметр — строку, которая является цепочкой частей классов:
bem('product.one.two.three._first._second');
Объявим функцию:
function bem( $classTrail = '' ) {
if ( empty( $classTrail ) ) {
return '';
}
}
При этом я сразу говорю, что если на вход ничего не пришло, следует вернуть пустую строку.
Далее следует разбить строку на массив:
$classTrail = array_values( array_filter( explode( '.', $classTrail ) ) );
Следом необходимо определить переменные, которые будут хранить промежуточные данные для миксинов и классов, а так же определить начальное значение для составного класса и посчитать число элементов цепочки:
$mixins = [];
$classes = [];
$block = '';
$count = sizeof( $classTrail );
Далее делаем перебор нашего массива $classTrail
:
foreach ( $classTrail as $i => $item ) {
// код
}
Первым делом следует к составному элементу $block
приписать название родительского класса, в нашем случае это product
. Это следует сделать только в том случае, если мы на первой итерации:
if ( 0 === $i ) {
$block = $classTrail[ $i ];
}
Вполне может быть, что в нашей цепочке частей классов указан только один класс, поэтому в таком случае нам следует сделать проверку и если так и окажется, добавить этот класс в список:
if ( 0 === $i ) {
$block = $classTrail[ $i ];
if ( 1 == $count ) {
$classes[] = $block;
}
}
Далее, если итерация не первая приступим к обработке остальных частей цепочки. Начнем с миксинов, проверим наличае символа нижнего подчеркивания:
if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
// код
}
И если это миксин, сразу добавим его в список миксинов:
if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
$mixins[] = $classTrail[ $i ];
}
Если при этом в нашей цепочке всего 2 элемента, это означает, что указан родительский класс и миксин: product._new
, в этом случае следует часть составного класса добавить в список классов:
if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
$mixins[] = $classTrail[ $i ];
if ( 2 == $count ) {
$classes[] = $block;
}
}
В ином случае, если это не миксин, мы понимаем, что это дочерний класс, который сразу добавим в список классов:
if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
$mixins[] = $classTrail[ $i ];
if ( 2 == $count ) {
$classes[] = $block;
}
}
else {
$classes[] = $block . '__' . $classTrail[ $i ];
}
Таким образом мы составили список классов и миксинов:
$mixins = [];
$classes = [];
$block = '';
$count = sizeof( $classTrail );
foreach ( $classTrail as $i => $item ) {
if ( 0 === $i ) {
$block = $classTrail[ $i ];
if ( 1 == $count ) {
$classes[] = $block;
}
}
else {
if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
$mixins[] = $classTrail[ $i ];
if ( 2 == $count ) {
$classes[] = $block;
}
}
else {
$classes[] = $block . '__' . $classTrail[ $i ];
}
}
}
Теперь необходимо перебирая список получившихся классов дописать к каждому миксин из списка миксинов, для этого составим вложенный цикл:
foreach ( $classes as $i => $class ) {
foreach ( $mixins as $j => $mixin ) {
$classes[] = $classes[ $i ] . $mixin;
}
}
Работа практически завершена, но лично у меня есть потребность создавать различные наборы классов. Скажем одна цепочка служит для создания набора классов для описания внешнего вида, вторая для указания селекторов для использования в JS, например:
bem('product._new js.product');
// в результатае будет: product product_new js__product
Для этого нам необходимо строку разбить на массив по пробелам и весь код, написанный выше, поместить в еще один цикл:
$trails = [];
$classTrails = array_values( array_filter( explode( ' ', $classTrail ) ) );
foreach ( $classTrails as $classTrail ) {
$classTrail = array_values( array_filter( explode( '.', $classTrail ) ) );
$mixins = [];
$classes = [];
$block = '';
$count = sizeof( $classTrail );
foreach ( $classTrail as $i => $item ) {
if ( 0 === $i ) {
$block = $classTrail[ $i ];
if ( 1 == $count ) {
$classes[] = $block;
}
}
else {
if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
$mixins[] = $classTrail[ $i ];
if ( 2 == $count ) {
$classes[] = $block;
}
}
else {
$classes[] = $block . '__' . $classTrail[ $i ];
}
}
}
foreach ( $classes as $i => $class ) {
foreach ( $mixins as $j => $mixin ) {
$classes[] = $classes[ $i ] . $mixin;
}
}
$trails = array_merge( $trails, $classes );
}
Теперь осталось вернуть результат. Его можно вернуть в виде:
- массива;
- строки;
- строки вида
class="product product_new"
.
Для этого я ввел еще два входных параметра в функции, мне удобно было именно так:
$toAttribute = false
— венуть классы внутри атрибута class;$isArray = true
— вернуть массив.
bem('product._new js.product', true);
// вернет class="product product_new"
bem('product._new js.product', false, false);
// вернет 'product product_new'
bem('product._new js.product');
// вернет массив
Полный код функции
/**
* Function that creates the classes chain in BEM style
*
* @param string $classTrail
* @param bool $toAttribute
* @param bool $isArray
*
* @return array|string
*/
function bem( $classTrail = '', $toAttribute = false, $isArray = true ) {
if ( empty( $classTrail ) ) {
return '';
}
$trails = [];
$classTrails = array_values( array_filter( explode( ' ', $classTrail ) ) );
foreach ( $classTrails as $classTrail ) {
$classTrail = array_values( array_filter( explode( '.', $classTrail ) ) );
$mixins = [];
$classes = [];
$block = '';
$count = sizeof( $classTrail );
foreach ( $classTrail as $i => $item ) {
if ( 0 === $i ) {
$block = $classTrail[ $i ];
if ( 1 == $count ) {
$classes[] = $block;
}
}
else {
if ( 0 === strpos( $classTrail[ $i ], '_' ) ) {
$mixins[] = $classTrail[ $i ];
if ( 2 == $count ) {
$classes[] = $block;
}
}
else {
$classes[] = $block . '__' . $classTrail[ $i ];
}
}
}
foreach ( $classes as $i => $class ) {
foreach ( $mixins as $j => $mixin ) {
$classes[] = $classes[ $i ] . $mixin;
}
}
$trails = array_merge( $trails, $classes );
}
if ( ! empty( $toAttribute ) ) {
return ' class="' . join( ' ', $trails ) . '" ';
}
// if $isArray is false
if ( empty( $isArray ) ) {
$trails = join( ' ', $trails );
}
// return classes as array
return $trails;
}
Данный код вы сможете увидеть в моем новом плагине для создания и обработки форм, который уже готов и скоро будет доступен для скачивания, ссылка появится в данной статье.
Благодаря данной функции наш шаблон станет выглядеть так:
<div <?php echo bem( 'product', true ); ?>>
<div <?php echo bem( 'product.inner', true ); ?>>
<div <?php echo bem( 'product.image', true ); ?>></div>
<div <?php echo bem( 'product.info', true ); ?>>
<h2 <?php echo bem( 'product.title', true ); ?>>{{ product.title }}</h2>
<div <?php echo bem( 'product.price', true ); ?>>{{ product.price }}</div>
</div>
</div>
</div>
Кому-то может показаться, что стало хуже… но этот только в том случае, если вы не работаете с БЭМ, да еще и в WordPress ))
Могу сказать, что на практике это упростит читабельность классов и сделает их более структурированными.