![Режимы и состояния в SCSS Режимы и состояния в SCSS](https://habrastorage.org/files/5de/a7f/42e/5dea7f42e0bf479d8c92225e110a822b.png)
Соображения на тему использования режимов и состояний компонентов пользовательского интерфейса. Рассмотрим применение подхода при работе с препроцессорами стилей. Осторожно, в статье слишком много примеров кода.
Концепция
Компонент интерфейса может иметь несколько состояний: зелёная кнопка может быть нажата и отжата. Компонент интерфейса может иметь несколько наборов состояний: кнопка может быть не только зелёная, но и голубая, и обе они могут быть как нажаты, так и отжаты.
Каждому состоянию кнопки соответствует некоторое оформление. Оформление компонента интерфейса может повторяться в различных состояниях одного режима и в разных режимах. Количество состояний режима зависит от конкретного режима.
![Соотношение Режим-Состояние Соотношение Режим-Состояние](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)
Если экран пользователя, относительно, большой, то список занимает ровно 640px по ширине, если экран маленький, — всю ширину контентной области страницы.
Схожая зависимость присуща и другим контентным блокам. Контентная область сниппета кода на большом экране узкая, его ширина составляет 640px, а на маленьком — узкая, но другая.
А, «широкое» изображение, широкое на всех экранах — оно занимает 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) {
@include device(desktop, tablet, phone) {
@if $value == 'auto' {
#{$property}-left: calc((100% - #{$contentWidth})*(1/3));
#{$property}-right: calc((100% - #{$contentWidth})*(2/3));
}
@else {
@if $value != 0 {
#{$property}-left: $value;
#{$property}-right: $value;
}
}
}
@include device(ie) {
@if $value == 'auto' {
#{$property}-left: 3*$d;
#{$property}-right: 6*$d;
}
@else {
@if $value != 0 {
#{$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);
}
}
Такой подход позволяет структурировать код и избежать многократного дублирования, которое имело бы место, при описании данного поведения у каждого контентного блока в отдельности.
По просьбе stardust_kid размещаю сгенерированный CSS, код для типа устройства
desktop
. По пути разметил полный код хелпера slimContent__view-helper
, с таргетингом по устройствам и проверкой на ноль..btn_color_blue {
background-color: #1aa2e6;
}
.btn_color_blue:before {
border-bottom-color: #1481b8;
}
.btn_color_blue .btn__text {
color: #fff;
}
.btn_color_blue:hover:not(.btn_disabled) {
background-color: #47b4eb;
}
.btn_color_blue:hover:not(.btn_disabled):before {
border-bottom-color: #1aa2e6;
}
.btn_color_blue:hover:not(.btn_disabled) .btn__text {
color: #e8f6fc;
}
.btn_color_blue:active:not(.btn_disabled) {
background-color: #1481b8;
}
.btn_color_blue:active:not(.btn_disabled):before {
border-bottom-color: #0f618a;
}
.btn_color_blue:active:not(.btn_disabled) .btn__text {
color: #d1ecfa;
}
@media screen and (min-width: 1240px) {
pre.ui-code {
padding-left: calc((100% - 640px)*(1/3));
padding-right: calc((100% - 640px)*(2/3));
}
}
@media screen and (max-width: 1239px) {
pre.ui-code {
padding-left: 24px;
padding-right: 24px;
}
}
// селектор по тегу используется, так как существует инлайновый span.ui-code
@media screen and (min-width: 1240px) {
.ui-img.ui-img_narrow {
margin-left: calc((100% - 640px)*(1/3));
margin-right: calc((100% - 640px)*(2/3));
}
}
Для понимания масштаба решаемого вопроса приведу некоторые данные:
- устройств 4
- контентных блоков около 20
- каждый контентный блок «внутри» конкретного устройства имеет 1–2 состояния
Оригинал статьи
Комментарии (15)
Eklykti
12.12.2015 03:04+4Зато многократное дублирование окажется в сгенеренном CSS.
k12th
13.12.2015 00:14Ну вообще хороший минифактор должен бы с этим справиться.
apian
13.12.2015 08:39Вот я и спрашивал товарища выше по ветке Eklykti, мол что он имеет в виду: дублирование селекторов в одном правиле, дублирование правил, дублирование содержимого правил. Товарищ поступил по-английски.
В случае с кнопкой код генерируется следующего содержания
.button__blue { style for button__blue(bg ,color, border) } .button__purple { style for button__purple(bg ,color, border) } // дублирования нет
В случае с контентным блоком происходит дублирование правил, т.е.
@media screen and (min-width: 1240px) { pre.ui-code { padding-left: calc((100% - 640px)*(1/3)); padding-right: calc((100% - 640px)*(2/3)); } } // содержимое правила для сниппета «дублирует» содержимое правила для, скажем, изображения @media screen and (min-width: 1240px) { .ui-img.ui-img_narrow { margin-left: calc((100% - 640px)*(1/3)); margin-right: calc((100% - 640px)*(2/3)); } }
Не, ну можно, конечно заморочиться, написать пару функций на SassScript, которые бы объеденили группу правил в одно, но зачем, если это задача постпроцессоров. В моём случае пусть не идеально, но кое-где ClosureCompiler генерирует конструкции
@media screen and (min-width:1240px){.cs,.d1,.ce{margin-left:calc((100% - 640px)*(1/3));margin-right:calc((100% - 640px)*(2/3))}} // код после обфускатора
Но суть поста не в этом. В статье я обращаю внимание на то, что с элементами интерфейса можно, а порой и нужно, работать как с объектами реального мира. Профит для разработчика — структурирование и быстрое масштабирование.Eklykti
14.12.2015 12:47Чисто по коду казалось что результат совсем страшный будет. По примерам же оказалось, что вроде бы всё не так уж и печально.
apian
14.12.2015 13:36Так точно. И код на первый взгляд страшноватый, а если разобраться, то понятно, хоть и заморочно. Но при большом количестве сущностей с релевантным представлением, подход зачёт. Благодарю за возвращение)
stardust_kid
12.12.2015 03:23Интересная тема. Было бы хорошо включить в статью примеры кода CSS, который получается в результате.
apian
12.12.2015 06:28+1stardust_kid, добавил примеры генерируемого кода в статью. Благодарю за совет.
Drag13
Где я еще не искал логику? А, да! В CSS! :D