На главную

Режимы и состояния в SCSS

Режимы и состояния в SCSS

Соображения на тему использования режимов и состояний компонентов пользовательского интерфейса. Рассмотрим применение подхода при работе с препроцессорами стилей. Осторожно, в статье слишком много примеров кода.

Публикация от . Статья опубликована на habrahabr.ru .

Концепция

Компонент интерфейса может иметь несколько состояний: зелёная кнопка может быть нажата и отжата. Компонент интерфейса может иметь несколько наборов состояний: кнопка может быть не только зелёная, но и голубая, и обе они могут быть как нажаты, так и отжаты.

Каждому состоянию кнопки соответствует некоторое оформление. Оформление компонента интерфейса может повторяться в различных состояниях одного режима и в разных режимах. Количество состояний режима зависит от конкретного режима.

Соотношение Режим-Состояние
Соотношение Режим-Состояние

Таким образом, можно условиться, что

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);
        }
    }

Такой подход позволяет структурировать код и избежать многократного дублирования, которое имело бы место, при описании данного поведения у каждого контентного блока в отдельности.