Page Content
Соображения на тему использования режимов и состояний компонентов пользовательского интерфейса.
Рассмотрим применение подхода при работе с препроцессорами стилей⁠. Осторожно, в статье слишком много примеров кода⁠.
⁠, опубликовано на habr.ru ⁠.
Концепция
Компонент интерфейса может иметь несколько состояний: зелёная кнопка может быть нажата и отжата⁠. Компонент интерфейса может иметь несколько наборов состояний: кнопка может быть не только зелёная, но и голубая⁠, и обе они могут быть как нажаты⁠, так и отжаты⁠.
Каждому состоянию кнопки соответствует некоторое оформление. Оформление компонента интерфейса может повторяться в различных состояниях одного режима и в разных режимах. Количество состояний режима зависит от конкретного режима⁠.
Таким образом, можно условиться, что
- View / Представление
- набор стилей характеризующих сущность пользовательского интерфейса (⁠в конкретном Состоянии);
- State / Состояние
- условия влияющие на Представление⁠;
- Mode / Режим
- множество Состояний одной сущности.
В роли объекта Режима выступает компонент интерфейса не ниже уровня Блок в терминах БЭМ⁠.
Два режима не могут иметь Представления с одинаковыми свойствами. Если Режимы влияют на одни и те же свойства⁠, объедините их⁠.
Пример простой
В интерфейсе кнопка имеет несколько состояний: link — в ожидании действия пользователя, hover — пользователь навёл указатель мыши на кнопку⁠, active — пользователь зажал левую клавишу мыши над кнопкой⁠. Кнопка может быть синей и зелёной⁠.
В данном примере обе кнопки (голубая и зелёная⁠) представлены в трёх состояниях. Для каждой из кнопок набор состояний идентичен. Набор состояний образует некоторый режим⁠.
Я формирую стили для кнопок следующим образом.
// set modes
@each $skinName, $skinColor in (
( 'green', $green ),
( 'blue', $blue )
) {
// set state «link»
.button_color_#{ $skinName } {
// call view
@include skinView($skinName, 'link', $skinColor);
}
// set state «hover»
.button_color_#{ $skinName }:hover:not(.button_disabled) {
// call view
@include skinView($skinName, 'hover', $skinColor);
}
// set state «active»
.button_color_#{ $skinName }:active:not(.button_disabled) {
// call view
@include skinView($skinName, 'active', $skinColor);
}
}
Описание Представлений отделено от описания Состояний:
// 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-- {
// call view
@include slimContent__view($mode, 'screenBig');
}
@include screen_--s {
// call view
@include slimContent__view($mode, 'screenSmall');
}
}
// set state for phone
@include device(phone) {
// call view
@include slimContent__view($mode, 'screenSmall');
}
}
Для описания Представлений используем хелпер:
// set views
@mixin slimContent__view($mode, $screenSize) {
@if $mode == 'wide' {
@if $screenSize == 'screenBig' {
// call view-helper
@include slimContent__view-helper(padding, 0);
}
@if $screenSize == 'screenSmall' {
// call view-helper
@include slimContent__view-helper(padding, 0);
}
}
@if $mode == 'tricky' {
@if $screenSize == 'screenBig' {
// call view-helper
@include slimContent__view-helper(margin, auto);
}
@if $screenSize == 'screenSmall' {
// call view-helper
@include slimContent__view-helper(margin, 0);
}
}
@if $mode == 'slim' {
@if $screenSize == 'screenBig' {
// call view-helper
@include slimContent__view-helper(padding, auto);
}
@if $screenSize == 'screenSmall' {
// call view-helper
@include slimContent__view-helper(padding, $d);
}
}
}
Содержание хелпера таково:
// 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);
}
}
Такой подход позволяет структурировать код и избежать многократного дублирования, которое имело бы место⁠, при описании данного поведения у каждого контентного блока в отдельности⁠.