Соображения на тему использования режимов и состояний компонентов пользовательского интерфейса. Рассмотрим применение подхода при работе с препроцессорами стилей. Осторожно, в статье слишком много примеров кода.
Концепция
Компонент интерфейса может иметь несколько состояний: зелёная кнопка может быть нажата и отжата. Компонент интерфейса может иметь несколько наборов состояний: кнопка может быть не только зелёная, но и голубая, и обе они могут быть как нажаты, так и отжаты.
Каждому состоянию кнопки соответствует некоторое оформление. Оформление компонента интерфейса может повторяться в различных состояниях одного режима и в разных режимах. Количество состояний режима зависит от конкретного режима.
Таким образом, можно условиться, что
- View / Представление
— набор стилей характеризующих сущность пользовательского интерфейса (в определённом Состоянии); - State / Состояние
— условия влияющие на Представление; - Mode / Режим
— множество Состояний одной сущности.
В роле объекта Режима выступает компонент интерфейса не ниже уровня Блок в терминах БЭМ.
Два режима не могут иметь Представления с одинаковыми свойствами. Если Режимы влияют на одни и те же свойства, объедините их.
Пример простой
В интерфейсе кнопка имеет несколько состояний: link — в ожидании действия пользователя, hover — пользователь навёл указатель мыши на кнопку, active — пользователь зажал левую клавишу мыши над кнопкой. Кнопка может быть синей и зелёной.
В данном примере обе кнопки (голубая и зелёная) представлены в трёх состояниях. Для каждой из кнопок набор состояний идентичен. Набор состояний образует некоторый режим.
Я формирую стили для кнопок следующим образом:
// 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
.И каждый из них может быть как «широким», так и «узким» в зависимости от различных условий.
Одним из параметров отображения списков является ширина контентной части, которая влияет на ширину самого списка.
Если экран пользователя, относительно, большой, то список занимает ровно 640px по ширине, если экран маленький, — всю ширину контентной области страницы.
Схожая зависимость присуща и другим контентным блокам. Контентная область сниппета кода на большом экране узкая, его ширина составляет 640px, а на маленьком — узкая, но другая.
А, «широкое» изображение, широкое на всех экранах — оно занимает 100% ширины родителя. Таким образом комбинаций Представлений контентных блоков слишком большое.
Большое количество контентных блоков и наличие нескольких условий отображения заставляют создать одну универсальную абстрактную сущность, поведение / характеристики которой наследовали бы все контентные блоки. Всё как в ООП.
На моём сайте каждый контентный блок принимает один из трёх Режимов поведения:
- wide / широкий
широкий на большом экране, широкий на маленьком; - tricky / хитрый
узкий на большом экране, широкий на маленьком; - slim / узкий
узкий на большом экране, узкий на маленком.
Условиями отображения являются: тип устройства и размер экрана. Сложности добавляет Режим «Хитрый», который включает Представления от двух других режимов: на большом экране он «узкий», а на маленьком — «широкий».
Режимы и 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