Найти в Дзене
FRONTEND FLOW

Мастеркласс по стилям в Angular

Как разработчики интерфейсов, мы отвечаем за различные типы функциональных возможностей веб-приложений. На мой взгляд, второй по важности (после бизнес-логики) является управление стилем. Правильная система проектирования может сыграть решающую роль в привлечении пользователей к нашему конечному продукту.
В этой статье мы рассмотрим возможности стилизации, предлагаемые нам Angular и SCSS. Вы узнаете об инкапсуляции стилей, о том, какие SCSS-селекторы можно использовать в Angular, и, самое главное, о том, что такое система проектирования. Во — первых, давайте начнем с чего-то простого - с глобальных стилей. Это таблицы стилей, которые применяются к элементам во всем приложении, независимо от расположения HTML-файла.
Например: если мы объявим класс .red-highlight в глобальном файле styles.scss, мы сможем ссылаться на него в каждом компоненте приложения. Единственным исключением являются компоненты, которые используют инкапсуляцию ShadowDOM, но сейчас нам не нужно беспокоиться об этом
Оглавление

Как разработчики интерфейсов, мы отвечаем за различные типы функциональных возможностей веб-приложений. На мой взгляд, второй по важности (после бизнес-логики) является управление стилем. Правильная система проектирования может сыграть решающую роль в привлечении пользователей к нашему конечному продукту.

В этой статье мы рассмотрим возможности стилизации, предлагаемые нам Angular и SCSS. Вы узнаете об инкапсуляции стилей, о том, какие SCSS-селекторы можно использовать в Angular, и, самое главное, о том, что такое система проектирования.

Что такое глобальные стили?

Во — первых, давайте начнем с чего-то простого - с глобальных стилей. Это таблицы стилей, которые применяются к элементам во всем приложении, независимо от расположения HTML-файла.

Например: если мы объявим класс .red-highlight в глобальном файле styles.scss, мы сможем ссылаться на него в каждом компоненте приложения. Единственным исключением являются компоненты, которые используют инкапсуляцию ShadowDOM, но сейчас нам не нужно беспокоиться об этом

styles.scss
styles.scss

app.component.html
app.component.html

В приведенном выше фрагменте, что неудивительно, текстовое поле “Example Box” будет отображаться на красном фоне. Как вы можете видеть, использовать глобальные стили очень просто.

Глобальные стили следует использовать для:

  • объявляйте все виды служебных классов (типографика, поля, отступы).
  • сброс стилей.
  • инициализация переменных CSS/SCSS.
  • инициализируйте тему библиотеки пользовательского интерфейса, такую как Angular Material
  • переопределять или изменять стили тем

Настройка глобальных стилей

Теперь, когда мы знаем, что такое глобальные стили, мы можем приступить к их настройке. Это будет немного отличаться в зависимости от того, используем ли мы в проекте библиотеку @nrwl/nx. Глобальные стили можно настроить в:

  • angular.json (если не используем Nx)
  • project.json (если используем do)

В случае с файлом angular.json мы объявим путь к глобальной таблице стилей следующим образом:

projects > (project name) > architect > (configuration) > options > styles

angular.json
angular.json

В случае с файлом project.json мы объявим пути в соответствии с

targets > (configuration) > options > styles key.

project.json
project.json

По умолчанию каждое приложение Angular импортирует только одну глобальную таблицу стилей — styles.scss, расположенную в папке src. Список стилей также используется для импорта внешних библиотек пользовательского интерфейса, таких как Angular Material или Bootstrap.

Дополнение глобальных стилей (пример: Bootstrap)

В качестве примера настройки глобального стиля я буду использовать библиотеку Bootstrap. Она предоставляет нам служебные классы и готовые компоненты, которые значительно ускоряют процесс создания пользовательских интерфейсов. Мы можем добавить ее в наш проект с помощью команды:

npm install bootstrap bootstrap-icons

После установки библиотеки ее необходимо зарегистрировать в файле angular.json или project.json, в зависимости от того, используем ли мы Nx.

angular.json или project.json
angular.json или project.json

Любые изменения в конфигурации глобального стиля требуют от нас перезапуска скрипта ng serve. Как только это будет сделано, мы сможем использовать классы и компоненты из Bootstrap. Не забудьте также импортировать его .js филе в проект!

Как вы можете видеть, настроить глобальный стиль очень просто!! Давайте перейдем к чему — то более сложному - импорту стилей в компоненты.

Стили в компонентах

В большинстве случаев каждый компонент в Angular имеет отдельную “локальную” таблицу стилей. Это позволяет приложениям оставаться модульными и иметь прозрачную структуру.

Импорт стилей внутри компонентов

Чтобы импортировать таблицы стилей, мы можем использовать следующие свойства в @Component декораторе:

  • styleUrls — принимает список относительных путей к файлам стилей.
  • styleUrl — принимает путь к файлу в виде строки. Стоит отметить, что это свойство доступно только начиная с Angular 17.0.0-next.4.
styleUrls
styleUrls

В тех случаях, когда мы хотим предоставить CSS непосредственно в файле компонента, мы можем использовать свойство styles внутри @Component. Оно принимает список стилей. Кроме того, стоит добавить, что начиная с Angular 17.0.0-next.4, его значение также может быть строкой.

Стили внутри компонента. Без импорта файла стилей
Стили внутри компонента. Без импорта файла стилей

Однако в мире Angular написание стилей непосредственно в файле компонента является редкой практикой. Таким образом, этот вариант следует использовать только в том случае, если стили компонента очень короткие.

Различные типы инкапсуляции стилей

Существуют разные способы инкапсуляции стилей. Каждый метод работает по — своему - стоит знать различия между ними, чтобы избежать неприятных сюрпризов в будущем.

В настоящее время мы можем использовать следующие инкапсуляции:

  • emulated encapsulation (по умолчанию)
  • ShadowDOM
  • Отсутствие инкапсуляции (None)

Emulated encapsulation (Локальные стили)

Такая инкапсуляция означает, что импортированные стили влияют только на элементы внутри шаблона нашего компонента. Благодаря такому поведению мы можем быть уверены, что стили одного компонента не будут перезаписывать стили другого компонента. В результате приложения, использующие этот метод инкапсуляции, надежны — по мере добавления дополнительных функций в приложение мы полностью избегаем визуальных ошибок, вызванных конфликтами имен классов.

Мы объявляем Emulated encapsulation в свойстве encapsulation @Component. Однако его объявление необязательно — Angular использует его по умолчанию для каждого нового компонента.

Emulated encapsulation
Emulated encapsulation

ShadowDOM encapsulation

Инкапсуляция ShadowDOM использует специальный теневой корень для своих компонентов. В результате они изолированы от основного DOM и не смогут использовать глобальные стили.

Кроме того, ShadowDOM недоступен в некоторых устаревших браузерах.

ShadowDOM
ShadowDOM

Теневые корни в дереве DOM заключены в элемент #shadow-root.

На скриншоте выше показан DOM после включения инкапсуляции ShadowDOM в корневом компоненте приложения.
На скриншоте выше показан DOM после включения инкапсуляции ShadowDOM в корневом компоненте приложения.

Отсутствие инкапсуляции

Вы также можете отказаться от использования какой-либо инкапсуляции вообще, что делает все стили в компоненте глобальными.

Использование стратегии без инкапсуляции очень рискованно, поскольку при добавлении новых стилей мы можем столкнуться с визуальной регрессией — неожиданным изменением внешнего вида компонента. Наиболее распространенной причиной этого является объявление двух классов с одинаковыми именами. Из-за этого риска лучше использовать инкапсуляцию.

Без инкапсуляции (None)
Без инкапсуляции (None)

Когда мы решаем не использовать инкапсуляцию, стоит поместить стили в уникальный класс, имя которого не будет повторяться в другом месте приложения. Хорошей идеей будет назвать класс в честь его компонента.

color-picker.component.ts
color-picker.component.ts
Стили компонента
Стили компонента

Селекторы

Мы можем использовать специальные селекторы для стилизации определенных элементов.

:host

Этот селектор позволяет нам стилизовать наш тег компонента.

Стилизация тега компонента
Стилизация тега компонента

Приведенный выше фрагмент кода при импорте в компонент с помощью app-card будет скомпилирован в следующий код:

-16

По умолчанию каждый компонент Angular является встроенным. Использование :host для отображения элемента в виде блока является относительно популярным.

Стоит отметить, что у :host селектора есть ограничение: он работает только тогда, когда наш компонент использует эмулируемую инкапсуляцию или инкапсуляцию ShadowDOM. При отсутствии инкапсуляции требуется несколько иной подход:

Стилизация тега компонента без инкапсуляции стилей
Стилизация тега компонента без инкапсуляции стилей

:host-context

Этот селектор позволяет нам условно стилизовать размещающий элемент компонента на основе его родительского класса.

Например, приведенный ниже стиль будет применен к классу my-button внутри нашего компонента только в том случае, если один из предков (в нашем случае элемент body) имеет класс dark-theme:

-18

-19

::ng-deep

Этот селектор повышает стиль компонента до глобального стиля. Стоит отметить, что ::ng-deep помечен как устаревший (deprecated). Поэтому мнения о том, следует ли его вообще использовать, разделились.

Создание системы проектирования

Очень хорошей практикой при создании веб-приложений является создание системы дизайна - набора общих значений для приложения. Такие значения могут включать единицы измерения расстояния, цвета или типографику.

Чтобы продемонстрировать концепцию, мы внедрим простую систему проектирования с использованием переменных SCSS.

Мы должны правильно настроить приложение в angular.json (или project.json) для использования глобальных переменных и миксинов. После раздела, где мы добавили глобальные стили, мы добавили 2 новые записи: stylePreprocessorOptions и includePaths. Теперь, после перезапуска приложения, Angular позволит нам импортировать переменные и утилиты в локальных стилях из каталога src.

stylePreprocessorOptions и includePaths
stylePreprocessorOptions и includePaths

Давайте начнем с создания файловой структуры:

  1. Создайте папку styles и вложенную в нее папку utils
  2. Внутри папки utils создайте 3 файла SCSS — _breakpoints.scss, _colors.scss и _spacing.scss (обратите внимание на подчеркивание!).
  3. Добавьте файл index.scss в папку utils

Наша структура должна выглядеть следующим образом:

Структура файлов
Структура файлов

Как только мы создадим файлы, мы сможем перейти к написанию нашей системы оформления. Давайте начнем с определения нашей цветовой палитры в _colors.scss. Чтобы упростить нашу жизнь, мы можем воспользоваться онлайн-генератором палитр или использовать готовую палитру.

Однако, если вы хотите создать свою собственную палитру с нуля, хорошим подходом будет использовать следующие названия переменных:

  • primary color
  • accent color
  • warning color (yellow/orange)
  • errors color (red)

При создании элементов пользовательского интерфейса следуйте правилу 60/30/10: выбирайте цвета так, чтобы пользовательский интерфейс состоял из:

  1. 60% от основного цвета (чаще всего это натуральный цвет, т.е. белый/серый/черный)
  2. 30% от проводящего цвета
  3. 10% от цвета акцента

Применяя этот принцип, наш пользовательский интерфейс будет очень прозрачным, и все интерактивные элементы (например, кнопки), окрашенные в соответствующий цвет, будут сразу видны.

Пример файла _colors.scss может выглядеть следующим образом:

_colors.scss
_colors.scss

В дополнение к единой цветовой гамме, наше приложение должно иметь одинаковые интервалы, которые будут надлежащим образом разделять элементы пользовательского интерфейса. Общее правило - всегда использовать интервал в 8/12 пикселей внутри компонента пользовательского интерфейса и больший интервал для отделения компонентов друг от друга.

Пример файла с отступами:

_spacing.scss
_spacing.scss

Последнее, что нам нужно указать в нашей системе проектирования, - это брейкпоинты. При написании приложений мы должны сосредоточиться на трех областях просмотра:

  • смартфоны
  • планшеты и ноутбуки с маленьким экраном
  • компьютерные мониторы и телевизоры

Мы создадим переменные, а также будем использовать миксины, чтобы упростить работу по созданию адаптивного пользовательского интерфейса.

_breakpoints.scss
_breakpoints.scss

Вот и все для нашей маленькой системы проектирования.

Пришло время экспортировать переменные из всех наших файлов. Мы можем сделать это, используя @forward в файле index.scss. Нам также нужно будет использовать @forward в файле глобального стиля.

index.scss
index.scss

Чтобы использовать нашу систему проектирования внутри компонента, мы будем использовать @use, в котором мы объявляем путь к папке, содержащей файл index.scss.

app.component.scss
app.component.scss

К сожалению, создание собственной системы дизайна требует написания большого количества шаблонного кода. Уже существуют инструменты, такие как Tailwind CSS, которые реализуют собственную систему дизайна и позволяют нам адаптировать ее к нашим потребностям.

Директивы

Директивы - это структура Angular, с помощью которой мы можем писать повторно используемый код, отвечающий за управление поведением и внешним видом элемента.

В этой части статьи мы создадим директивы, которые будут применять радужный фон к нашему элементу. В процессе мы рассмотрим три различных способа манипулирования стилями элементов:

  • ElementRef – HTML element wrapper
  • @HostBinding decorator
  • HostProperty

Мы начнем с создания трех директив:

ng g d directives/rainbowElementRef --standalone --skip-tests
ng g d directives/rainbowHostBinding --standalone --skip-tests
ng g d directives/rainbowHostProperty --standalone --skip-tests

После этого у нас должна получиться следующая файловая структура:

Структура файлов, после создания директив
Структура файлов, после создания директив

Прежде чем мы перейдем к реализации директивной логики, нам нужно немного настроить код. Давайте начнем с объявления глобального стиля, который придает нашему элементу радужный фон, в файле styles.scss.

styles.scss
styles.scss

Затем мы импортируем наши директивы в app.component.ts

app.component.ts
app.component.ts

Наконец, давайте добавим три дива в app.component.html, чтобы мы могли видеть, как директивы влияют на HTML-элементы.

<div class="box" appRainbowHostProperty></div>
<div class="box" appRainbowHostBinding></div>
<div class="box" appRainbowElementRef></div>

app.component.scss
app.component.scss

Итак, с настройкой покончено.. Давайте перейдем к реализации директив — мы начнем с той, которая использует ElementRef.

ElementRef и native element

rainbow-element-ref.directive.ts
rainbow-element-ref.directive.ts

Мы можем разделить логику этого компонента на три этапа:

Инициализация:

Мы начинаем с того, что инжектим ElementRef и присваиваем его переменной _elementRef

Логика стилизации

Мы получаем данные из компонента (@input() set duration()), которые будут задавать продолжительность анимации

Мы получаем входные данные hideBackground, которые, в зависимости от их значения, будут добавлять или удалять класс rainbow-background

Значения по умолчанию

Мы добавляем интерфейс OnInit в класс и реализуем метод ngOnInit, который предоставляет входные значения по умолчанию

После запуска кода мы должны увидеть, что наш элемент имеет радужный фон!

-32

На мой взгляд, использование ElementRef очень неуклюже — вам приходится многое делать вручную. Это может быть полезно, когда мы хотим написать логику для более продвинутых функций, таких как элемент Badge. К счастью, в большинстве случаев мы можем стилизовать элементы, используя более простые функции Angular, такие как декоратор @HostBinding.

@HostBinding

rainbow-host-binding.directive.ts
rainbow-host-binding.directive.ts

Немного лучший способ стилизовать элементы с помощью директив - использовать декоратор @HostBinding. С его помощью Angular автоматически назначит атрибуты нашему элементу в соответствии со значением переменной или средства получения.

Давайте посмотрим на приведенный выше пример:

Инициализация:

Мы объявляем два входных параметра — duration и hideBackground.

Логика стилизации

Мы привязываем стиль animationDuration к элементу, используя декоратор @HostBinding. Стиль продолжительности анимации элемента будет иметь то же значение, что и средство получения animationDuration (по умолчанию “5s”).

Мы привязываем класс rainbow-background, который будет отображаться в элементе только тогда, когда значение @Input() hideBackground нашей директивы равно false.

Как вы можете видеть, декоратор @HostBinding позволяет нам значительно сократить логику стилизации. Вопрос в том, можем ли мы сделать что-то еще лучше?

Host property

rainbow-host-property.directive.ts
rainbow-host-property.directive.ts

Свойство host внутри декоратора нашей директивы позволяет нам легко привязывать атрибуты к нашему компоненту. В примере выше:

Инициализация:

Мы объявляем два входных параметра — duration и hideBackground.

Логика стилизации

Мы привязываем класс rainbow-background к элементу, когда значение hideBackground равно false

Мы привязываем стиль animationDuration к значению переменной duration и добавляем строку “s”.

Заключение

Отныне стили в Angular больше не должны скрывать от вас никаких секретов. Angular предоставляет нам множество возможностей в контексте CSS; благодаря селекторам, стилям с областью действия и глобальным переменным SCSS мы можем создавать согласованные пользовательские интерфейсы.

Автор статьи Dawid Kostka. Ссылка на оригинал.