Режимы и состояния в SCSS
Соображения на тему использования режимов и состояний компонентов пользовательского интерфейса. Рассмотрим применение подхода при работе с препроцессорами стилей. Осторожно, в статье слишком много примеров кода.
Компонент интерфейса может иметь несколько состояний: зелёная кнопка может быть нажата и отжата. Компонент интерфейса может иметь несколько наборов состояний: кнопка может быть не только зелёная, но и голубая, и обе они могут быть как нажаты, так и отжаты.
Каждому состоянию кнопки соответствует некоторое оформление. Оформление компонента интерфейса может повторяться в различных состояниях одного режима и в разных режимах. Количество состояний режима зависит от конкретного режима.
![Соотношение Режим-Состояние](https://habrastorage.org/files/084/b52/82a/084b5282a6e7411a863c3e7ddf4c133c.png)
Таким образом, можно условиться, что
- View / Представление
— набор стилей характеризующих сущность пользовательского интерфейса (в определённом Состоянии); - State / Состояние
— условия влияющие на Представление; - Mode / Режим
— множество Состояний одной сущности.
В роле объекта Режима выступает компонент интерфейса не ниже уровня Блок в терминах БЭМ.
Два режима не могут иметь Представления с одинаковыми свойствами. Если Режимы влияют на одни и те же свойства, объедините их.
В интерфейсе кнопка имеет несколько состояний: link — в ожидании действия пользователя, hover — пользователь навёл указатель мыши на кнопку, active — пользователь зажал левую клавишу мыши над кнопкой. Кнопка может быть синей и зелёной.
![Пример с кнопками](https://habrastorage.org/files/fe6/f71/74d/fe6f7174d7a74feebb873297040b4f17.png)
В данном примере обе кнопки (голубая и зелёная) представлены в трёх состояниях. Для каждой из кнопок набор состояний идентичен. Набор состояний образует некоторый режим.
![Режимы и состояния на примере кнопок](https://habrastorage.org/files/53a/fe9/f0b/53afe9f0bf864de9b0dc6790e5e826ad.png)
Я формирую стили для кнопок следующим образом:
// set modes
@each $skinName, $skinColor in (
( 'green', $green ),
( 'blue', $blue )
) {
.button_color_#{ $skinName } { // set state «link»
@include skinView($skinName, 'link', $skinColor); // call view
}
.button_color_#{ $skinName }:hover:not(.button_disabled) { // set state «hover»
@include skinView($skinName, 'hover', $skinColor); // call view
}
.button_color_#{ $skinName }:active:not(.button_disabled) { // set state «active»
@include skinView($skinName, 'active', $skinColor); // call view
}
}
Описание Представлений отделено от описания Состояний:
// set views
@mixin skinView($skin, $state, $color) {
& {
background-color:
if( $state == 'link', $color, null )
if( $state == 'hover', lighten($color, 10%), null )
if( $state == 'active', darken($color, 10%), null );
}
&:before {
border-bottom-color:
if( $state == 'link', darken($color, 10%), null )
if( $state == 'hover', $color, null )
if( $state == 'active', darken($color, 20%), null );
}
.button__text {
color:
if( $state == 'link', #fff, null )
if( $state == 'hover', lighten($color, 45%), null )
if( $state == 'active', lighten($color, 40%), null );
}
}
Таким образом мы получили расширяемый механизм генерации стилей для различных кнопок в различных состояниях.
На моём сайте ширина контентной области не фиксирована. Контентную часть наполняет множество блоков: параграфы, списки, изображения, примеры кода и т.д. Каждый из контентных блоков имеет несколько модификаций по различным параметрам. Например, списки могут быть ol
, ul
и dl
.
И каждый из них может быть как «широким», так и «узким» в зависимости от различных условий.
Одним из параметров отображения списков является ширина контентной части, которая влияет на ширину самого списка.
![Различное поведение ширины контентной области](https://habrastorage.org/files/5a6/1d5/723/5a61d57238f146828ae8b73dfe6680dc.png)
Если экран пользователя, относительно, большой, то список занимает ровно 640 px по ширине, если экран маленький, — всю ширину контентной области страницы.
Схожая зависимость присуща и другим контентным блокам. Контентная область сниппета кода на большом экране узкая, его ширина составляет 640 px, а на маленьком — узкая, но другая.
А, «широкое» изображение, широкое на всех экранах — оно занимает 100% ширины родителя. Таким образом комбинаций Представлений контентных блоков слишком большое.
Большое количество контентных блоков и наличие нескольких условий отображения заставляют создать одну универсальную абстрактную сущность, поведение / характеристики которой наследовали бы все контентные блоки. Всё как в ООП.
На моём сайте каждый контентный блок принимает один из трёх Режимов поведения:
- wide / широкий
широкий на большом экране, широкий на маленьком; - tricky / хитрый
узкий на большом экране, широкий на маленьком; - slim / узкий
узкий на большом экране, узкий на маленком.
Условиями отображения являются: тип устройства и размер экрана. Сложности добавляет Режим «Хитрый», который включает Представления от двух других режимов: на большом экране он «узкий», а на маленьком — «широкий».
![Контентные блоки различной ширины](https://habrastorage.org/files/dd6/165/8ac/dd61658ac6cb4e7a9298ced8696f0104.png)
Режимы и Cостояния описываются следующим образом:
// set modes
@mixin slimContent($mode) {
// set states for desktop and tablet
@include device(desktop, tablet) {
@include screen_m-- {
@include slimContent__view($mode, 'screenBig'); // call view
}
@include screen_--s {
@include slimContent__view($mode, 'screenSmall'); // call view
}
}
// set state for phone
@include device(phone) {
@include slimContent__view($mode, 'screenSmall'); // call view
}
}
Для описания Представлений используем хелпер:
// set views
@mixin slimContent__view($mode, $screenSize) {
@if $mode == 'wide' {
@if $screenSize == 'screenBig' {
@include slimContent__view-helper(padding, 0); // call view-helper
}
@if $screenSize == 'screenSmall' {
@include slimContent__view-helper(padding, 0); // call view-helper
}
}
@if $mode == 'tricky' {
@if $screenSize == 'screenBig' {
@include slimContent__view-helper(margin, auto); // call view-helper
}
@if $screenSize == 'screenSmall' {
@include slimContent__view-helper(margin, 0); // call view-helper
}
}
@if $mode == 'slim' {
@if $screenSize == 'screenBig' {
@include slimContent__view-helper(padding, auto); // call view-helper
}
@if $screenSize == 'screenSmall' {
@include slimContent__view-helper(padding, $d); // call view-helper
}
}
}
Содержание хелпера таково:
// view-helper
@mixin slimContent__view-helper($property, $value) {
@if $value == 'auto' {
#{$property}-left: calc((100% - #{$contentWidth})*(1/3));
#{$property}-right: calc((100% - #{$contentWidth})*(2/3));
}
@else {
#{$property}-left: $value;
#{$property}-right: $value;
}
}
Применение Режима элементарно:
.ui-snippet {
@include slimContent(slim);
@include ritm(1*$v);
overflow-x: auto;
}
.ui-image {
&.ui-image_wide {
@include slimContent(wide);
}
&.ui-image_narrow {
@include slimContent(tricky);
}
}
Такой подход позволяет структурировать код и избежать многократного дублирования, которое имело бы место, при описании данного поведения у каждого контентного блока в отдельности.
Оригинал статьи