[Разработка веб-сайтов, CSS, TypeScript] Продвинутый CSS-in-TS
Автор
Сообщение
news_bot ®
Стаж: 6 лет 8 месяцев
Сообщений: 27286
Здравствуйте, меня зовут Дмитрий Карловский и я… автор одного из первых фреймворков целиком и полностью написанных на тайпскрипте — $mol. Он по максимуму использует возможности статической типизации. И сегодня речь пойдёт о максимально жёсткой статической фиксации стилей.
Это расшифровка выступления на PiterJS#46. Вы можете либо посмотреть видео запись, либо открыть в интерфейсе проведения презентаций, либо читать как статью...
Подопытное приложение
Разбирать мы будем весьма простое приложение $my_profile, которое состоит из двух панелей: меню и детализация. А каждая панель состоит из двух частей: заголовка и тела.
Генерация классов
Описывается такое приложение крайне простым кодом..
$my_profile $mol_view sub /
<= Menu $my_panel
<= Details $my_panel
$my_panel $mol_view sub /
<= Head $mol_view
<= Body $mol_scroll
Не буду грузить вас этим птичьим языком. Скажу, лишь, что из него генерируется typescript класс следующего вида..
interface $my_profile extends $mol_view {
Menu(): $my_panel
Details(): $my_panel
} )
interface $my_panel extends $mol_view {
Head(): $mol_view
Body(): $mol_scroll
} )
Каждый компонент — это некоторый класс, который наследуется от другого компонента. И в нём, для каждого вложенного компонента генерится метод, который возвращает экземпляр соответствующего класа. Такие методы обычно пишутся с большой буквы.
Генерация DOM
Моловский компонент, когда рендерится, генерирует некоторый DOM-элемент. Имя этого элемента по умолчанию совпадает с именем инстанцированного класса компонента, что довольно удобно при обзоре сгенерированного DOM-дерева. При необходимости имя элемента может быть переопределено через свойство dom_name. Кроме того, автоматически генерируется набор атрибутов, предназначенных для подключения стилей. При этом учитывается не только собственное глобальное имя компонента, но и глобальные имена всех его предков, а так же его локальное имя во внешнем компоненте, глобальные имена внешнего компонента и его предков и так далее рекурсивно до корня приложения.
interface $my_profile extends $mol_view {
Menu(): $my_panel
Details(): $my_panel
} )
interface $my_panel extends $mol_view {
Head(): $mol_view
Body(): $mol_scroll
} )
<mol_view
mol_view
my_panel_body
my_profile_details_body
>
<mol_button_major
mol_view
mol_button
mol_button_major
my_profile_signup
>
В данном примере мы видим следующие атрибуты:
mol_view — базовый класс для всех моловских компонент, через него можно, например, сделать reset для вообще всех компонент, без риска поломать, не моловские компоненты.
my_panel_body — значит это компонент с локальным именем Body внутри внешнего компонента $my_panel.
my_profile_details_body — значит тот $my_panel имеет локальное имя Details в приложении $my_profile.
Наложение стилей
Таким образом мы получаем исчерпывающий набор атрибутов для стилизации компонент, без традиционного ручного прописывания классов элементам. И как бы глубоко ни располагался элемент специфичность селектора остаётся постоянной, что очень удобно, когда надо переопределять стили.
<mol_view
mol_view
my_panel_body
my_profile_details_body
>
<mol_button_major
mol_view
mol_button
mol_button_major
my_profile_signup
>
[my_profile_details_body] {
overflow: 'overlay';
}
[my_profile_details_body] [mol_button] {
border-radius: .5rem;
}
Но у такого написания CSS есть ряд недостатков. Например, эти длинные селекторы утомительно писать руками. Среда разработки не понимает, что это имена компонент и ничего не подсказывает при наборе. Наконец, тайпскрипт тоже ничего про это соглашение не знает и не может протайпчекать селекторы.
Генерация стилей
Было бы классно описать наши стили прямо в тайпскрипте в простой лаконичной форме и автоматически сгенерировать из этого CSS.
[my_profile_details_body] {
overflow: 'overlay';
}
[my_profile_details_body] [mol_button] {
border-radius: .5rem;
}
$mol_style_define( $my_profile , {
Details: {
Body: {
overflow: 'overlay',
$mol_button: {
border: {
radius: rem(.5),
},
},
},
},
} )
Тут мы вызываем функцию $mol_style_define, которая генерит StyleSheet. Передаём в неё класс компонента $my_profile и JSON, говорящий, что внутри компонента Details и его внутреннего компонента Body стили такие-то, а для всех вложенных в него кнопок — сякие-то.
CSSOM: проблема с редактированием через DevTools
Генерировать стили можно двумя способами: либо через CSSOM, либо через генерацию портянки CSS и подклеивания его через элемент style. Если использовать первый подход, то в Chrome Dev Tools такие стили становятся не редактируемыми, что очень не удобно при разработке. Поэтому приходится использовать второй подход.
Сверху на скриншоте вы видите стили, сгенеренные библиотекой aphrodite, а снизу — обычный CSS. Кстати, обратите внимание на порнографию в качестве селектора и сравните с теми именами, что генерит $mol_view.
Генерация CSS — довольно простая операция. У меня это заняло 3КБ кода. Так что не будем особо на этом останавливаться и перейдём к типизации..
CSSStyleDeclaration: слабая типизация
В идеальном мире мы бы взяли стандартный тип CSSStyleDeclaration, поставляемый вместе с тайпскриптом. Это просто словарь из 500 свойств, типизированных как string.
type CSSStyleDeclaration = {
display: string
// 500 lines
}
Соответственно, вы можете легко опечататься в каком-нибудь слове, а тайпскрипт это злополучно пережуёт и даже не подавится.
{
display: 'black' //
}
csstype: кривая типизация
Можно взять популярную библиотеку csstype, которая генерируется из выгрузки всех свойств и их значений из MDN. Во второй её версии генерируются не очень полезные типы..
type DisplayProperty =
| Globals
| DisplayOutside
| DisplayInside
| DisplayInternal
| DisplayLegacy
| "contents" | "list-item" | "none"
| string
Кто знаком с тайпскриптом, понимаю, что этот код эквивалентен следующему..
type DisplayProperty = string
Всё дело в том, что строковые литералы являются подтипами строки, потому тайпскрипт оставляет лишь наиболее общий тип.
csstype@3: кривая типизация с подсказками
Но ничего, в тетьей версии они это "починили" — string соединили с пустым интерфейсом, из-за чего слияния с литералами уже не происходит:
type Display =
| Globals
| DisplayOutside
| DisplayInside
| DisplayInternal
| DisplayLegacy
| "contents" | "list-item" | "none"
| (string & {})
Теперь типы не сливаются и подсказки всё же работают, но вы по прежнему можете опечататься и никто не ругнётся..
{
display: 'black' //
}
Простые свойства
Мы пойдём своим путём и самостоятельно опишем типы для нужных нам свойств в том виде, который будет нам удобнее всего. Для этого заведём интерфейс Properties и будем заполнять его..
interface Properties {
/**
* Whether an element is treated as a block or inline element
* and the layout used for its children, such as flow layout, grid or flex.
*/
display?: 'block' | 'inline' | 'none' | ... | Common
// etc
}
type Common = 'inherit' | 'initial' | 'unset'
Подсказки по свойствам
Для каждого свойства будем писать докстринг, который будет выводиться над именем свойства при наведении, что полезно тем, кто ещё не вызубрил все 500 CSS-свойств.
Группы свойств
Многие CSS свойства имеют общий префикс и было бы удобно не писать его каждый раз, а сгруппировать в один объект..
overflow: {
x: 'auto' ,
y: 'scroll',
anchor: 'none',
}
interface Properties {
overflow? : {
x?: Overflow | Common
y?: Overflow | Common
anchor?: 'auto' | 'none' | Common
}
}
type Overflow = 'visible' | 'hidden' | ... | Common
Свойства размеров
Для размерностей есть не только предопределённый список значений, но произвольные юниты (1px, 2rem) и функции (calc(1rem + 1px)).
interface Properties {
width?: Size
height?: Size
}
type Size =
| 'auto' | 'max-content' | 'min-content' | 'fit-content'
| Length | Common
type Length = 0 | Unit< 'rem' | ... | '%' > | Func<'calc'>
Единицы измерения
Юнит можно объявить как просто класс, который параметризирован литералом и который умеет правильно себя сериализовывать.
class Unit< Lit extends string > {
constructor(
readonly val: number,
readonly lit: Lit,
) { }
toString() {
return this.val + this.lit
}
}
Так как литерал мы передаём в конструкторе, то типы экземпляров созданных с разными литералами будут различны, что не позволит, например, передать проценты туда, где ожидаются градусы.
function rem( val : number ) {
return new Unit( val , 'rem' )
}
{
width: rem(1) // Unit<'rem'>
}
Функции
Аналогичным образом можно реализовать и функции. В них класс параметризируется не только именем функции, но и типом её содержимого.
Func<
Name extends string,
Value = unknown,
> {
constructor(
readonly name: Name,
readonly val: Value,
) { }
toString() {
return `${ this.name }(${ this.val })`
}
}
function calc( val : string ) {
return new Func( 'calc' , val )
}
{
// Func< 'calc' , string >
width: calc( '1px + 1em' )
}
Сокращённые свойства
Многие CSS свойства имеют как полную так и сокращённую формы. Например, для margin можно указать от 1 до 4 значений. И если для 1 и 2 всё более-менее понятно, то с болшим числом начинаются головоломки типа: если значения 4, то margin-left — это последнее значение, а если 3, то предпоследнее. Чтобы такого не происходило, оставим лишь пару сокращённых форм, а если хочется большего контроля — изволь написать в полной форме какому направлению какое значение. Получаем чуть больше писанины, но и улучшаем понятность кода.
interface Properties {
margin?: Directions<Length>
padding?: Directions<Length>
}
type Directions< Value > =
| Value
| [ Value , Value ]
| {
top?: Value ,
right?: Value ,
bottom?: Value ,
left?: Value ,
}
margin: rem(.5)
padding: [ 0 , rem(.5) ]
margin: {
top: 0,
right: rem(.5),
bottom: rem(.5),
left: rem(.5),
}
Цвета
Для цветов у нас есть словарь $mol_colors из всех стандартных цветов — просто берём из него ключи. Плюс добавляем несколько функций..
type Color =
| keyof typeof $mol_colors
| 'transparent' | 'currentcolor'
| $mol_style_func< 'hsla' | 'rgba' | 'var' >
color?: Color | Common
{
color: 'snow',
background: {
color: hsla( 0 , 0 , 50 , .1 ),
},
}
hsl и rgb специально не добавлены, ибо написать лишнюю единичку для hsla и rgba — не проблема, зато АПИ несколько упростили.
Списки
Порой свойство может содержать не одно значение, а целый список. Например, список фоновых картинок..
background?: {
image?: [ $mol_style_func<'url'> ][]
}
background: {
image: [
[url('/foo.svg')],
[url('/bar.svg')],
],
},
Тут можно было бы запариться со специальными хелперными функциями, но писать их постоянно не очень практично, поэтому используется следующая простая эвристика: если в массиве лежат простые значения, то они просто соединяются через пробел. А если сложные (массивы, объекты), то через запятые. А вложенные в них значения уже через пробел.
Списки структур
Более сложный пример — список из структур как в описании теней..
box?: {
shadow?: readonly {
inset: boolean
x: Length
y: Length
blur: Length
spread: Length
color: Color
}[]
}
box: {
shadow: [
{
inset: true,
x: 0,
y: 0,
blur: rem(.5),
spread: 0,
color: 'black',
},
],
},
У тени много параметров, в которых легко запутаться, поэтому лучше писать имена параметров явно. При сериализации имена параметров отбрасываются, а значения соединяются через пробел. Особый случай — булево свойство, если оно ложно, то игнорируется, а если истинно, то добавляется имя свойства.
БЭМ-элементы
Наконец, мы дошли до самой мякотки. У нас есть компонент $my_profile, в котором есть элемент Details, который сам является компонентом $my_panel, в котором есть элемент Body, который является компонентом $mol_scroll. И было бы неплохо уметь стилизовать любой из этих элементов, через стили, задаваемые компоненту $my_profile.
interface $my_profile {
Details(): $my_panel
} )
interface $my_panel {
Body(): $mol_scroll
} )
$mol_style_define( $my_profile , {
padding: rem(1),
Details: {
margin: 'auto',
Body: {
overflow: 'scroll',
},
},
} )
При этом хочется, чтобы тайпскрипт проверил, что указанные в стилях элементы действительно присутствуют в компонентах.
Поиск всех БЭМ-элементов
У нас есть специальная типофункция $mol_type_pick, которой вы передаёте некоторый интерфейс и желаемый тип полей, а возвращает она тот же интерфейс, но без полей, которые не соответвуют желаемому типу.
$mol_type_pick<
$my_profile,
()=> $mol_view,
>
interface $my_profile extends $mol_view {
title(): string
Menu(): $my_panel
Details(): $my_panel
} )
⇩
{
Menu(): $my_panel
Details(): $my_panel
}
В данном примере, у компонента есть свойство title, возвращающее строку. Но было бы странно вешать стили на строку. Стили можно вешать лишь на Menu и Details ибо они возвращают экземпляры компонент.
БЭМ-блоки
Вложенные компоненты порой хочется стилизовать не только по их локальному имени, но и по имени их класса. Например, мы хотим сделать вообще все кнопки по умолчанию красными, а кнопки на панели детализации — синими.
interface $my_profile {
Menu(): $my_panel
Details(): $my_panel
} )
interface $mol_button {
} )
$mol_style_define( $my_profile , {
$mol_button: {
color: 'red',
},
Details: {
$mol_button: {
color: 'blue',
},
},
} )
Поиск всех подклассов
Найти все имена компонент не сложно, так как все классы, функции и прочие глобальные переменные объявляются в едином неймспейсе с именем $. Это позволяет воспользоваться тем же $mol_type_pick, чтобы выбрать все компоненты.
type $mol_view_all = $mol_type_pick<
typeof $,
$mol_view,
>
namespace $ {
export class $mol_view {}
export class $my_panel {}
export class $mol_tree {}
}
⇩
{
$mol_view: typeof $mol_view
$my_panel: typeof $my_panel
}
Декларативные ограничения
Наконец, мы добрались до типизации функции-генератора в которую мы передаём компонент и описание стилей. Наивное описание типов для неё выглядит в декларативной форме — по классу компонента генерируется развесистый рекурсивный тип со всеми возможными вариантами полей.
function $mol_style_define<
View extends typeof $mol_view,
Config extends Styles< View >
>(
view : View,
config : Config
)
type Config< View extends $mol_view > = {
$my_panel: {
$my_panel: {
...
}
$my_deck: {
$my_panel: {
...
}
}
...
}
...
}
Проблема тут в том, что вкладывать друг в друга компоненты вы можете практически как угодно, и число различных комбинаций растёт экспоненциально от глубины. В результате тайпскрипт выжирает всю память и падает. А если и не падает, то начинает дико тормозить.
Не понятные типошибки
Но даже если вы дождётесь вывода типа, то сообщения ошибки вас не обрадуют, ибо из него не будет понятно где и почему возникла ошибка.
Императивные ограничения
Лучше в таком случае использовать императивный подход, где вы берёте фактический тип написанного пользователем JSON, в типогуарде проходитесь по нему и переданному компоненту и генерируете новый не рекурсивный тип, где оставлены лишь валидные поля и значения. А на не валидных тайпскрипт не сматчится и сообщит об ошибке.
function $mol_style_define<
View extends typeof $mol_view,
Config extends StylesGuard<
View,
Config,
>
>(
view: View,
config: Config
)
{
Details: {
foo: 'bar',
},
}
⇩
{
Details: {
foo: never,
},
}
Работает такой подход гораздо быстрее и кушает меньше памяти. Важно б этом помнить, когда пишете сложные типы, иначе придётся ждать подсказок по несколько секунд и порой получать фигу.
Всё ещё непонятные ошибки
Если просто возвращать never для не валидных значений, то сообщения об ошибке хоть и будут иметь координаты, но не будет понятна причина, почему написанный код не валиден.
Понятные ошибки
Вместо never лучше возвращать специальный тип, который ни с чем не сматчится и в котором литералом прописать суть сообщения об ошибке и дополнительную информацию.
В примерах мы сначала попытались навесить стили на несуществующий атрибут, потом на свойство, которое возвращает не компонент, а в конце — просто написали какую-то балалайку, которую не удалось распознать ни как имя компонента, ни как имя элемента, ни как имя свойства.
Атрибуты
Кстати, об атрибутах. В $mol_view они задаются как метод возвращающий словарь из строки в примитив.
attr *
^
mol_link_current <= current false
⇩
attr() {
return {
... super.attr(),
mol_link_current: this.current(),
}
}
Стили для атрибутов
Для описания стилей для атрибутов просто вкладываем друг в друга: собачка -> имя атрибута -> значение -> (стили, элементы и прочий фарш).
{
'@': {
mol_link_current: {
true: {
zIndex: 1
}
}
}
}
Псевдоклассы и псевдоэлементы
Разумеется есть предопределённый список всех псевдоклассов и псевдоэлементов, которые можно использовать на любом уровне..
{
':hover': {
zIndex: 1
}
}
Медиа запросы
Так же можно вкладывать медиа-запросы. Это гораздо удобнее, чем в CSS, где медиа-запросы пишутся вне и требуют повторения селектора внутри.
{
$mol_scroll: {
overflow: 'scroll',
'@media': {
'print': {
overflow: 'visible',
},
},
},
}
Непосредственно вложенные блоки
Наконец, с помощью угловой скобочки можно задавать стили лишь для непосредственно вложенных элементов.
{
'>': {
$mol_view: {
margin: rem(1),
},
$mol_button: {
margin: rem(0),
},
},
}
Что получилось
- Каскадные переопределения стилей
- Тайпчек всех ключей и значений
- Подсказки по всем ключам и значениям
- Описание всех свойств
- Понятные сообщения об ошибках
- Удобство описания стилей
Что можно улучшить
- Рантайм чтение стилей до рендеринга (полезно для виртуализации)
- Типизация всех свойств (прогресс 10 из 500)
- Добавить все функции
- Поддержать анимации
- Типизированные выражения в calc (а не строка)
Попробовать вне $mol
Ести вас заинтересовал мой расказ и вы хотели бы попробовать поиграться с этим, но не готовы ещё перейти на $mol, то можете воспользоваться библиотекой mol_style_all, позволяющей описывать CSS-свойства.
import {
$mol_style_unit,
$mol_style_func,
$mol_style_properties,
} from 'mol_style_all'
const { em , rem } = $mol_style_unit
const { calc } = $mol_style_func
const props : $mol_style_properties = {
margin: [ em(1) , rem(1) ],
height: calc('100% - 1rem'),
}
codesandbox.io/s/molstyleall-ked9t
Там, конечно, нет генератора CSS, так как он сильно завязан на то, что генерирует $mol_view. Тем не менее, если вы заходите добавить эту функциональность к своему любимому фреймворку, то заходите к нам в чат mam_mol — вместе подумаем, как бы это сделать.
Продолжение следует...
К сожалению, материала оказалось слишком много, поэтому пришлось разбить выступление на две части. Это была лайтовая чать про использование, а следующая уже будет хардкорная, где мы поговорим про программирование на типах и как реализовывать всё то, о чём я рассказал..
- Сравнение типов
- Типофункции
- Типотесты
- Типошибки
- Типогуарды
- Фильтрация интерфейсов
- Брендированные примитивы
Куда пойти
- eigenmethod/mol/style — исходники
- mam_mol — обсуждение
- nin_jin — связь со мной
- habhub.hyoo.ru — статьи
- _jinnin — заметки
Обратная связь
- Хороший доклад. Хотелось бы побольше конкретных примеров. Вот у нас есть отрисованный дизайн, и вот, что мы написали, чтобы получился pixel-perfect.
- Очень быстро говорит! Не успеваешь понять и думаешь, что надо пересматривать и гуглить.
- Не работал с ТС, поэтому не могу обьективно оценить ценность трудов Дмитрия.
- Для меня не сильно профильно, непонятно и не очень интересно.
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript, ReactJS, Визуализация данных, Инфографика, Разработка веб-сайтов] Визуализация сложных данных с использованием D3 и React
- [Разработка веб-сайтов, Python, Открытые данные] Разработка онлайн-сервиса для инвесторов на pythonanywhere.com с использованием данных Yahoo Finance
- [CSS, Usability] Темный режим: Hello darkness, my old friend (перевод)
- [Разработка веб-сайтов, JavaScript, VueJS] Отдаем корректный код 404 в связке VUE SPA + SSR
- [Разработка веб-сайтов, CSS, HTML] 10 современных раскладок в одну строку CSS-кода (перевод)
- [PHP, Отладка, Проектирование и рефакторинг, Разработка веб-сайтов] Я сомневался в юнит-тестах, но…
- [Разработка веб-сайтов, CSS, JavaScript, Canvas, ReactJS] 24 октября приглашаем на онлайн-митап Hot Frontend в Казани
- [JavaScript, ReactJS, Программирование, Разработка веб-сайтов] Кастомные хуки. Part 1
- [Разработка веб-сайтов, PHP, Laravel] Laravel–Дайджест (5–11 октября 2020)
- [Разработка веб-сайтов, JavaScript, Программирование, VueJS] Создание блога с помощью Nuxt Content(часть первая) (перевод)
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_css, #_typescript, #_$mol, #_$mol_view, #_$mol_style, #_$mol_type, #_cssints, #_cssinjs, #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_css, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 01-Ноя 11:39
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 8 месяцев |
|
Здравствуйте, меня зовут Дмитрий Карловский и я… автор одного из первых фреймворков целиком и полностью написанных на тайпскрипте — $mol. Он по максимуму использует возможности статической типизации. И сегодня речь пойдёт о максимально жёсткой статической фиксации стилей. Это расшифровка выступления на PiterJS#46. Вы можете либо посмотреть видео запись, либо открыть в интерфейсе проведения презентаций, либо читать как статью... Подопытное приложение Разбирать мы будем весьма простое приложение $my_profile, которое состоит из двух панелей: меню и детализация. А каждая панель состоит из двух частей: заголовка и тела. Генерация классов Описывается такое приложение крайне простым кодом.. $my_profile $mol_view sub /
<= Menu $my_panel <= Details $my_panel $my_panel $mol_view sub / <= Head $mol_view <= Body $mol_scroll Не буду грузить вас этим птичьим языком. Скажу, лишь, что из него генерируется typescript класс следующего вида.. interface $my_profile extends $mol_view {
Menu(): $my_panel Details(): $my_panel } ) interface $my_panel extends $mol_view { Head(): $mol_view Body(): $mol_scroll } ) Каждый компонент — это некоторый класс, который наследуется от другого компонента. И в нём, для каждого вложенного компонента генерится метод, который возвращает экземпляр соответствующего класа. Такие методы обычно пишутся с большой буквы. Генерация DOM Моловский компонент, когда рендерится, генерирует некоторый DOM-элемент. Имя этого элемента по умолчанию совпадает с именем инстанцированного класса компонента, что довольно удобно при обзоре сгенерированного DOM-дерева. При необходимости имя элемента может быть переопределено через свойство dom_name. Кроме того, автоматически генерируется набор атрибутов, предназначенных для подключения стилей. При этом учитывается не только собственное глобальное имя компонента, но и глобальные имена всех его предков, а так же его локальное имя во внешнем компоненте, глобальные имена внешнего компонента и его предков и так далее рекурсивно до корня приложения. interface $my_profile extends $mol_view {
Menu(): $my_panel Details(): $my_panel } ) interface $my_panel extends $mol_view { Head(): $mol_view Body(): $mol_scroll } ) <mol_view
mol_view my_panel_body my_profile_details_body > <mol_button_major mol_view mol_button mol_button_major my_profile_signup > В данном примере мы видим следующие атрибуты: mol_view — базовый класс для всех моловских компонент, через него можно, например, сделать reset для вообще всех компонент, без риска поломать, не моловские компоненты. my_panel_body — значит это компонент с локальным именем Body внутри внешнего компонента $my_panel. my_profile_details_body — значит тот $my_panel имеет локальное имя Details в приложении $my_profile. Наложение стилей Таким образом мы получаем исчерпывающий набор атрибутов для стилизации компонент, без традиционного ручного прописывания классов элементам. И как бы глубоко ни располагался элемент специфичность селектора остаётся постоянной, что очень удобно, когда надо переопределять стили. <mol_view
mol_view my_panel_body my_profile_details_body > <mol_button_major mol_view mol_button mol_button_major my_profile_signup > [my_profile_details_body] {
overflow: 'overlay'; } [my_profile_details_body] [mol_button] { border-radius: .5rem; } Но у такого написания CSS есть ряд недостатков. Например, эти длинные селекторы утомительно писать руками. Среда разработки не понимает, что это имена компонент и ничего не подсказывает при наборе. Наконец, тайпскрипт тоже ничего про это соглашение не знает и не может протайпчекать селекторы. Генерация стилей Было бы классно описать наши стили прямо в тайпскрипте в простой лаконичной форме и автоматически сгенерировать из этого CSS. [my_profile_details_body] {
overflow: 'overlay'; } [my_profile_details_body] [mol_button] { border-radius: .5rem; } $mol_style_define( $my_profile , {
Details: { Body: { overflow: 'overlay', $mol_button: { border: { radius: rem(.5), }, }, }, }, } ) Тут мы вызываем функцию $mol_style_define, которая генерит StyleSheet. Передаём в неё класс компонента $my_profile и JSON, говорящий, что внутри компонента Details и его внутреннего компонента Body стили такие-то, а для всех вложенных в него кнопок — сякие-то. CSSOM: проблема с редактированием через DevTools Генерировать стили можно двумя способами: либо через CSSOM, либо через генерацию портянки CSS и подклеивания его через элемент style. Если использовать первый подход, то в Chrome Dev Tools такие стили становятся не редактируемыми, что очень не удобно при разработке. Поэтому приходится использовать второй подход. Сверху на скриншоте вы видите стили, сгенеренные библиотекой aphrodite, а снизу — обычный CSS. Кстати, обратите внимание на порнографию в качестве селектора и сравните с теми именами, что генерит $mol_view. Генерация CSS — довольно простая операция. У меня это заняло 3КБ кода. Так что не будем особо на этом останавливаться и перейдём к типизации.. CSSStyleDeclaration: слабая типизация В идеальном мире мы бы взяли стандартный тип CSSStyleDeclaration, поставляемый вместе с тайпскриптом. Это просто словарь из 500 свойств, типизированных как string. type CSSStyleDeclaration = {
display: string // 500 lines } Соответственно, вы можете легко опечататься в каком-нибудь слове, а тайпскрипт это злополучно пережуёт и даже не подавится. {
display: 'black' // } csstype: кривая типизация Можно взять популярную библиотеку csstype, которая генерируется из выгрузки всех свойств и их значений из MDN. Во второй её версии генерируются не очень полезные типы.. type DisplayProperty =
| Globals | DisplayOutside | DisplayInside | DisplayInternal | DisplayLegacy | "contents" | "list-item" | "none" | string Кто знаком с тайпскриптом, понимаю, что этот код эквивалентен следующему.. type DisplayProperty = string
Всё дело в том, что строковые литералы являются подтипами строки, потому тайпскрипт оставляет лишь наиболее общий тип. csstype@3: кривая типизация с подсказками Но ничего, в тетьей версии они это "починили" — string соединили с пустым интерфейсом, из-за чего слияния с литералами уже не происходит: type Display =
| Globals | DisplayOutside | DisplayInside | DisplayInternal | DisplayLegacy | "contents" | "list-item" | "none" | (string & {}) Теперь типы не сливаются и подсказки всё же работают, но вы по прежнему можете опечататься и никто не ругнётся.. {
display: 'black' // } Простые свойства Мы пойдём своим путём и самостоятельно опишем типы для нужных нам свойств в том виде, который будет нам удобнее всего. Для этого заведём интерфейс Properties и будем заполнять его.. interface Properties {
/** * Whether an element is treated as a block or inline element * and the layout used for its children, such as flow layout, grid or flex. */ display?: 'block' | 'inline' | 'none' | ... | Common // etc } type Common = 'inherit' | 'initial' | 'unset' Подсказки по свойствам Для каждого свойства будем писать докстринг, который будет выводиться над именем свойства при наведении, что полезно тем, кто ещё не вызубрил все 500 CSS-свойств. Группы свойств Многие CSS свойства имеют общий префикс и было бы удобно не писать его каждый раз, а сгруппировать в один объект.. overflow: {
x: 'auto' , y: 'scroll', anchor: 'none', } interface Properties {
overflow? : { x?: Overflow | Common y?: Overflow | Common anchor?: 'auto' | 'none' | Common } } type Overflow = 'visible' | 'hidden' | ... | Common Свойства размеров Для размерностей есть не только предопределённый список значений, но произвольные юниты (1px, 2rem) и функции (calc(1rem + 1px)). interface Properties {
width?: Size height?: Size } type Size = | 'auto' | 'max-content' | 'min-content' | 'fit-content' | Length | Common type Length = 0 | Unit< 'rem' | ... | '%' > | Func<'calc'> Единицы измерения Юнит можно объявить как просто класс, который параметризирован литералом и который умеет правильно себя сериализовывать. class Unit< Lit extends string > {
constructor( readonly val: number, readonly lit: Lit, ) { } toString() { return this.val + this.lit } } Так как литерал мы передаём в конструкторе, то типы экземпляров созданных с разными литералами будут различны, что не позволит, например, передать проценты туда, где ожидаются градусы. function rem( val : number ) {
return new Unit( val , 'rem' ) } { width: rem(1) // Unit<'rem'> } Функции Аналогичным образом можно реализовать и функции. В них класс параметризируется не только именем функции, но и типом её содержимого. Func<
Name extends string, Value = unknown, > { constructor( readonly name: Name, readonly val: Value, ) { } toString() { return `${ this.name }(${ this.val })` } } function calc( val : string ) {
return new Func( 'calc' , val ) } { // Func< 'calc' , string > width: calc( '1px + 1em' ) } Сокращённые свойства Многие CSS свойства имеют как полную так и сокращённую формы. Например, для margin можно указать от 1 до 4 значений. И если для 1 и 2 всё более-менее понятно, то с болшим числом начинаются головоломки типа: если значения 4, то margin-left — это последнее значение, а если 3, то предпоследнее. Чтобы такого не происходило, оставим лишь пару сокращённых форм, а если хочется большего контроля — изволь написать в полной форме какому направлению какое значение. Получаем чуть больше писанины, но и улучшаем понятность кода. interface Properties {
margin?: Directions<Length> padding?: Directions<Length> } type Directions< Value > = | Value | [ Value , Value ] | { top?: Value , right?: Value , bottom?: Value , left?: Value , } margin: rem(.5)
padding: [ 0 , rem(.5) ] margin: { top: 0, right: rem(.5), bottom: rem(.5), left: rem(.5), } Цвета Для цветов у нас есть словарь $mol_colors из всех стандартных цветов — просто берём из него ключи. Плюс добавляем несколько функций.. type Color =
| keyof typeof $mol_colors | 'transparent' | 'currentcolor' | $mol_style_func< 'hsla' | 'rgba' | 'var' > color?: Color | Common {
color: 'snow', background: { color: hsla( 0 , 0 , 50 , .1 ), }, } hsl и rgb специально не добавлены, ибо написать лишнюю единичку для hsla и rgba — не проблема, зато АПИ несколько упростили. Списки Порой свойство может содержать не одно значение, а целый список. Например, список фоновых картинок.. background?: {
image?: [ $mol_style_func<'url'> ][] } background: {
image: [ [url('/foo.svg')], [url('/bar.svg')], ], }, Тут можно было бы запариться со специальными хелперными функциями, но писать их постоянно не очень практично, поэтому используется следующая простая эвристика: если в массиве лежат простые значения, то они просто соединяются через пробел. А если сложные (массивы, объекты), то через запятые. А вложенные в них значения уже через пробел. Списки структур Более сложный пример — список из структур как в описании теней.. box?: {
shadow?: readonly { inset: boolean x: Length y: Length blur: Length spread: Length color: Color }[] } box: {
shadow: [ { inset: true, x: 0, y: 0, blur: rem(.5), spread: 0, color: 'black', }, ], }, У тени много параметров, в которых легко запутаться, поэтому лучше писать имена параметров явно. При сериализации имена параметров отбрасываются, а значения соединяются через пробел. Особый случай — булево свойство, если оно ложно, то игнорируется, а если истинно, то добавляется имя свойства. БЭМ-элементы Наконец, мы дошли до самой мякотки. У нас есть компонент $my_profile, в котором есть элемент Details, который сам является компонентом $my_panel, в котором есть элемент Body, который является компонентом $mol_scroll. И было бы неплохо уметь стилизовать любой из этих элементов, через стили, задаваемые компоненту $my_profile. interface $my_profile {
Details(): $my_panel } ) interface $my_panel { Body(): $mol_scroll } ) $mol_style_define( $my_profile , {
padding: rem(1), Details: { margin: 'auto', Body: { overflow: 'scroll', }, }, } ) При этом хочется, чтобы тайпскрипт проверил, что указанные в стилях элементы действительно присутствуют в компонентах. Поиск всех БЭМ-элементов У нас есть специальная типофункция $mol_type_pick, которой вы передаёте некоторый интерфейс и желаемый тип полей, а возвращает она тот же интерфейс, но без полей, которые не соответвуют желаемому типу. $mol_type_pick<
$my_profile, ()=> $mol_view, > interface $my_profile extends $mol_view {
title(): string Menu(): $my_panel Details(): $my_panel } ) ⇩ { Menu(): $my_panel Details(): $my_panel } В данном примере, у компонента есть свойство title, возвращающее строку. Но было бы странно вешать стили на строку. Стили можно вешать лишь на Menu и Details ибо они возвращают экземпляры компонент. БЭМ-блоки Вложенные компоненты порой хочется стилизовать не только по их локальному имени, но и по имени их класса. Например, мы хотим сделать вообще все кнопки по умолчанию красными, а кнопки на панели детализации — синими. interface $my_profile {
Menu(): $my_panel Details(): $my_panel } ) interface $mol_button { } ) $mol_style_define( $my_profile , {
$mol_button: { color: 'red', }, Details: { $mol_button: { color: 'blue', }, }, } ) Поиск всех подклассов Найти все имена компонент не сложно, так как все классы, функции и прочие глобальные переменные объявляются в едином неймспейсе с именем $. Это позволяет воспользоваться тем же $mol_type_pick, чтобы выбрать все компоненты. type $mol_view_all = $mol_type_pick<
typeof $, $mol_view, > namespace $ {
export class $mol_view {} export class $my_panel {} export class $mol_tree {} } ⇩ { $mol_view: typeof $mol_view $my_panel: typeof $my_panel } Декларативные ограничения Наконец, мы добрались до типизации функции-генератора в которую мы передаём компонент и описание стилей. Наивное описание типов для неё выглядит в декларативной форме — по классу компонента генерируется развесистый рекурсивный тип со всеми возможными вариантами полей. function $mol_style_define<
View extends typeof $mol_view, Config extends Styles< View > >( view : View, config : Config ) type Config< View extends $mol_view > = {
$my_panel: { $my_panel: { ... } $my_deck: { $my_panel: { ... } } ... } ... } Проблема тут в том, что вкладывать друг в друга компоненты вы можете практически как угодно, и число различных комбинаций растёт экспоненциально от глубины. В результате тайпскрипт выжирает всю память и падает. А если и не падает, то начинает дико тормозить. Не понятные типошибки Но даже если вы дождётесь вывода типа, то сообщения ошибки вас не обрадуют, ибо из него не будет понятно где и почему возникла ошибка. Императивные ограничения Лучше в таком случае использовать императивный подход, где вы берёте фактический тип написанного пользователем JSON, в типогуарде проходитесь по нему и переданному компоненту и генерируете новый не рекурсивный тип, где оставлены лишь валидные поля и значения. А на не валидных тайпскрипт не сматчится и сообщит об ошибке. function $mol_style_define<
View extends typeof $mol_view, Config extends StylesGuard< View, Config, > >( view: View, config: Config ) {
Details: { foo: 'bar', }, } ⇩ { Details: { foo: never, }, } Работает такой подход гораздо быстрее и кушает меньше памяти. Важно б этом помнить, когда пишете сложные типы, иначе придётся ждать подсказок по несколько секунд и порой получать фигу. Всё ещё непонятные ошибки Если просто возвращать never для не валидных значений, то сообщения об ошибке хоть и будут иметь координаты, но не будет понятна причина, почему написанный код не валиден. Понятные ошибки Вместо never лучше возвращать специальный тип, который ни с чем не сматчится и в котором литералом прописать суть сообщения об ошибке и дополнительную информацию. В примерах мы сначала попытались навесить стили на несуществующий атрибут, потом на свойство, которое возвращает не компонент, а в конце — просто написали какую-то балалайку, которую не удалось распознать ни как имя компонента, ни как имя элемента, ни как имя свойства. Атрибуты Кстати, об атрибутах. В $mol_view они задаются как метод возвращающий словарь из строки в примитив. attr *
^ mol_link_current <= current false ⇩ attr() { return { ... super.attr(), mol_link_current: this.current(), } } Стили для атрибутов Для описания стилей для атрибутов просто вкладываем друг в друга: собачка -> имя атрибута -> значение -> (стили, элементы и прочий фарш). {
'@': { mol_link_current: { true: { zIndex: 1 } } } } Псевдоклассы и псевдоэлементы Разумеется есть предопределённый список всех псевдоклассов и псевдоэлементов, которые можно использовать на любом уровне.. {
':hover': { zIndex: 1 } } Медиа запросы Так же можно вкладывать медиа-запросы. Это гораздо удобнее, чем в CSS, где медиа-запросы пишутся вне и требуют повторения селектора внутри. {
$mol_scroll: { overflow: 'scroll', '@media': { 'print': { overflow: 'visible', }, }, }, } Непосредственно вложенные блоки Наконец, с помощью угловой скобочки можно задавать стили лишь для непосредственно вложенных элементов. {
'>': { $mol_view: { margin: rem(1), }, $mol_button: { margin: rem(0), }, }, } Что получилось
Что можно улучшить
Попробовать вне $mol Ести вас заинтересовал мой расказ и вы хотели бы попробовать поиграться с этим, но не готовы ещё перейти на $mol, то можете воспользоваться библиотекой mol_style_all, позволяющей описывать CSS-свойства. import {
$mol_style_unit, $mol_style_func, $mol_style_properties, } from 'mol_style_all' const { em , rem } = $mol_style_unit const { calc } = $mol_style_func const props : $mol_style_properties = { margin: [ em(1) , rem(1) ], height: calc('100% - 1rem'), } codesandbox.io/s/molstyleall-ked9t Там, конечно, нет генератора CSS, так как он сильно завязан на то, что генерирует $mol_view. Тем не менее, если вы заходите добавить эту функциональность к своему любимому фреймворку, то заходите к нам в чат mam_mol — вместе подумаем, как бы это сделать. Продолжение следует... К сожалению, материала оказалось слишком много, поэтому пришлось разбить выступление на две части. Это была лайтовая чать про использование, а следующая уже будет хардкорная, где мы поговорим про программирование на типах и как реализовывать всё то, о чём я рассказал..
Куда пойти
Обратная связь
=========== Источник: habr.com =========== Похожие новости:
Разработка веб-сайтов ), #_css, #_typescript |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 01-Ноя 11:39
Часовой пояс: UTC + 5