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

В React есть две библиотеки, которые работают вместе, чтобы сделать создание презентационных компонентов очень простым: styled-components и styled-system.

Вот пример совместной работы этих библиотек, любезно предоставленный документацией по стилевой системе.

import styled from 'styled-components'
import { color } from 'styled-system'
const Box = styled.div`
  ${color}
`

Этот код создал компонент React с именем Box, который получает реквизиты color и bg.

<Box color="#fff" bg="tomato">
  Tomato
</Box>

В этой статье я расскажу:

  • Создание компонентов React с помощью шаблонов с тегами
  • Реализация простой версии стилизованных компонентов
  • Погружаемся в то, как styled-components и styled-systems на самом деле работают вместе

Шаблонные литералы

Чтобы понять, как работают styled-components и styled-systems, лучше сначала понять, откуда эти библиотеки черпают свою силу: литералы шаблонов.

Наиболее распространенный вариант использования шаблонных литералов - это конкатенация строк.

// Template Literal
const string = `I am a template literal, because I am enclosed by the back-tick`;
// Template Literal with placeholder
const expressions = 'dynamic values';
const string = `I can contain ${expressions}`;

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

Шаблонные литералы и предыдущая функция

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

Какая функция? В двух приведенных выше примерах функция по умолчанию с задачей объединения текста и заполнителей в одну строку.

const expressions = 'dynamic values';
const example = `I can contain ${expressions}`;
console.log(example);  // >> I can contain dynamic values

Но шаблонные литералы не ограничиваются выполнением только конкатенации строк. JavaScript позволяет нам использовать наши собственные пользовательские функции, чтобы делать все, что мы хотим, с текстом и заполнителями внутри литерала шаблона. Этот тип функции называется тегом, и для его использования вы просто указываете имя функции - тег - перед литералом шаблона. В результате создается шаблон с тегами.

Например, вот простая функция, которая не принимает никаких параметров и выводит статическое сообщение на консоль.

const printStaticMessage = () => { console.log('My static message'); }

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

printStaticMessage();  // >>> My static message
printStaticMessage``;  // >>> My static message

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

Использование шаблонного литерала в качестве аргумента для тега

Более полезный шаблон с тегами будет использовать текст и заполнители внутри литерала шаблона. Давайте создадим тег, который печатает свои аргументы.

const printArguments = (...args) => { console.log(...args); }
const var1 = "my";
const var2 = "message"
printArguments`This is ${var1} custom ${var2}!`;
// 
//   ["This is "," custom ","!"],
//   "my",
//   "message"
//

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

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

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

const defaultFunction = (stringArray, ...values) => {
  return stringArray.reduce((acc, str, i) => {
    return values[i] ? acc + str + values[i] : acc + str;
  }, '');
}
const var1 = "my";
const var2 = "message"
const example = defaultFunction`This is ${var1} custom ${var2}!`;
console.log(example);  // >>> This is my custom message!

Передача функций в шаблон с тегами

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

const executeFirstPlaceholder = (textArray, placeholder) => {
  placeholder();
}
executeFirstPlaceholder`${() => { console.log('first placeholder')}}`; 
// >>> first placeholder

Возврат функций из шаблона с тегами

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

const multiply = (multiple) => (num) =>
	parseInt(num[0]) * parseInt(multiple[0]);
const double = multiply`2`
const result = double`4`;
console.log(result);  // >>> 8

Переход к React

«Функциональные компоненты» React - это очень простые функции JavaScript, которые могут быть отображены в DOM. Вот пример шаблона с тегами, возвращающего компонент функции React.

const withGreeting = ([greeting]) => 
  ({ name }) => <h1>{greeting}, {name}!</h1>;
const Greet = withGreeting`Greetings`;
// Render component
<Greet name="Chris" />
// Renders in DOM
<h1>Greetings, Chris</h1>

Это суть того, как стилизованные компоненты генерируют компоненты React.

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

Используя тегированные литералы шаблонов (недавнее дополнение к JavaScript) и мощь CSS, styled-components позволяет вам писать реальный код CSS для стилизации ваших компонентов. Он также удаляет сопоставление между компонентами и стилями - использование компонентов в качестве низкоуровневой конструкции стиля очень просто!

Https://www.styled-components.com

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

В следующем примере styled.h1 используется для создания простого компонента React, содержащего HTML-тег <h1>, отображаемого с использованием стилей CSS, указанных в литерале шаблона.

import styled from 'styled-components';
const Title = styled.h1`color: blue;`;
// Render
<Title>Regular Title</Title>
// Renders to DOM
<h1 style="color: blue;">Regular Title</h1>

Объект styled содержит ключи, названные в честь общих тегов HTML, таких как H1, H2 и div. Эти клавиши ссылаются на функцию, которую можно использовать в качестве тега в шаблоне с тегами.

Простая реализация styled.h1

Попробуем сделать простую реализацию styled.h1. В самом простом случае функция styled.h1 получает стили CSS в обратных тиках и преобразует их в объект стиля, который присоединяется к базовому элементу (например, h1).

const styledH1 = ([styles]) => ({ children })=> {
  const lines = styles
                  .split(';')
                  .map(line => line.trim())
                  .filter(line => line !== "")
  // Function courtesy of mck89 on StackOverflow
  const convertToCamelCase = (key) =>
    key.replace(/-([a-z])/g, (x, up) => up.toUpperCase())
  const style = lines.reduce((acc, line) => {
    const lineParsed = line.split(':');
    const key = convertToCamelCase(lineParsed[0]);
    const val = lineParsed[1];
    return { ...acc, [key]: val };
  }, {});
  return <h1 style={style}>{children}</h1>
}
const H1 = styledH1`
  color: red;
  font-size: 18px;
`;
// Render
<H1>Hello</H1>
// Renders in DOM
<h1 style="color: red; font-size: 18px;">Hello</h1>

На этом этапе стиль, который мы передаем функции, жестко запрограммирован и исправлен; не может динамически изменяться в зависимости от значений свойств, которые получает компонент.

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

Использование функций для доступа к свойствам в шаблонных литералах и стилизованных компонентах

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

import styled from 'styled-components';
const Button = styled.button`
  color: ${ (props) => props.primary ? 'blue' : 'red' };
`;
class StyledComponentsDemo extends Component { 
  render() {
    return(
      <>
      <Button primary>Primary</Button>
      <Button>Secondary</Button>
      <Button primary 
              onSubmit={() => handleSubmit()}>Submit
      </Button>
     </>
    )
  }
}

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

Примечание: не все реквизиты, переданные в стилизованный компонент, должны влиять на презентацию (например, onSubmit); они также могут использоваться только базовым элементом HTML.

Стилизация обычных, нестандартных компонентов

Стилизованные компоненты позволяют стилизовать любой созданный вами пользовательский компонент. Во-первых, пользовательский компонент должен получить свойство className и передать его базовому элементу DOM. Как только это будет сделано, передайте пользовательский компонент функции styled и вызовите его как шаблон с тегами, чтобы получить новый компонент со стилем.

import styled from 'styled-components';
const Button = ({ className, children }) => 
  <button className={className}>{children}</button>
const ButtonBlue = styled(Button)`color: blue`;
// Render
<ButtonBlue>Blue Button</ButtonBlue>

Стилизация стилизованных компонентов

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

const Thing = styled.button`
  color: black;
  :hover {
    color: blue;
  }
`

В синтаксисе SCSS & ссылается на текущий компонент. Вы также можете ссылаться на другие компоненты, как если бы вы ссылались на любой другой тип селектора (например, .class или #id), просто ссылаясь на ${OtherComponentName}, но только если это стилизованный компонент.

import styled from 'styled-components';
const Item = styled.div`
   color: red;
`
const Container = styled.div`
   & > ${Item} {
      font-size: 2rem;
   }
`
class StyledComponentsDemo extends Component { 
  render() {
    return(
      <Container>
        <Item>Item 1</Item>
      </Container>
    )
  }
}

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

Использование тем

Тематизация достигается путем экспорта компонента ThemeProvider, передачи объекта его theme опоре и помещения всего приложения в компонент ThemeProvider. Это предоставит каждому стилизованному компоненту доступ к объекту theme.

import styled, { ThemeProvider } from 'styled-components';
const Item = styled.div`
  color: ${( props ) => props.theme.color.primary}
`
const theme = {
  color: {
    primary: 'red'
  }
}
class StyledComponentsDemo extends Component { 
  render() {
    return(
      <ThemeProvider theme={theme}>
	      <Item>Item 1</Item>
      </ThemeProvider>
    )
  }
}

Компоненты, которые не имеют обозначения. Компоненты также могут получить доступ к теме с помощью функции withTheme.

import { withTheme } from 'styled-components';
class MyComponent extends React.Component {
  render() {
    return <p>{this.props.theme.color.primary}</p>
  }
}
export default withTheme(MyComponent);

Стильная система

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

Https://styled-system.com

Если вы создали компонент Button из стилизованных компонентов и хотите, чтобы он получал реквизиты цвета переднего плана и фона, вы можете использовать служебную функцию styled-system color и передать ее как функцию-заполнитель в литерале шаблона, чтобы включить эти реквизиты.

import styled, { ThemeProvider } from 'styled-components';
import { color } from 'styled-system'
const theme = {
  colors: {
    primary: 'blue'
  }
}
const Box = styled.div`
  ${color}
`
class StyledSystemDemo extends Component {
  render() {
    return (
      <ThemeProvider theme={theme}>
        <>
          <Box color="#fff" bg="tomato">Tomato</Box>
          <Box color="white" bg="primary">Tomato</Box>
        </>
      </ThemeProvider>
    )
  }
}

Примечание: все имена сгенерированных реквизитов указаны в системном API стиля.

Если тема доступна, служебная функция попытается сопоставить значение опоры с темой, прежде чем использовать значение в качестве необработанного значения (например, #fff).

Структурирование объектов темы

Структура объекта темы и styled-system тесно связаны. Структура соответствует спецификации незавершенной работы, которая называется Спецификация темы пользовательского интерфейса системы.

Например, ключи fontSizes и colors соответствуют этой спецификации, а их значения (массивы или объекты) также соответствуют этой спецификации.

export const theme = {
   fontSizes: [
      12, 14, 16, 20, 24, 32
   ]
   fontSizes.body = fontSizes[1]
   colors: {
      blue: '#07c',
      green: '#0fa',
   }
}

В вышеупомянутой теме опора fontSize в компоненте может получать значение индекса массива или псевдоним body.

Под капотом color

Давайте посмотрим, как styled-system реализует функцию полезности color. Помните, что служебная функция вызывается так:

const Button = styled.div`
   ${color}
`

Так выглядит функция.

// <https://github.com/styled-system/styled-system/blob/v2.3.6/src/styles.js>
export const color = props => ({
  ...textColor(props),
  ...bgColor(props)
})

Это похоже на то, чтобы написать это в литерале шаблона:

const Button = styled.div`
   ${(props) => ({
      ...textColor(props),
      ...bgColor(props)
   })}
`

Функции textColor и bgColor возвращают объекты стиля, которые распределены внутри функции. Эти функции выглядят так.

// <https://github.com/styled-system/styled-system/blob/v2.3.6/src/styles.js>
export const textColor = responsiveStyle({
  prop: 'color',
  key: 'colors', // theme key
})
export const bgColor = responsiveStyle({
  prop: 'bg',
  cssProperty: 'backgroundColor',
  key: 'colors'
})

Функция responsiveStyle обрабатывает все точки останова, откаты и именование опор. Ниже я упростил код стилизованной системы для демонстрационных целей.

// <https://github.com/styled-system/styled-system/blob/v2.3.6/src/util.js>
// I simplified this for demonstrative purposes
export const responsiveStyle = ({
  prop,
  cssProperty,
  key,
}) => {
  const fn = props => {
    cssProperty = cssProperty || prop
    const n = props[prop];
    const theme = props.theme;
		
    // ...
		
    return {
      [cssProperty]: theme[key][n]
    } 
    
    // ...
  }
  return fn
}

Это можно представить примерно так:

const Button = styled.div`
   {
     ${...(props) => (
         { color: props.theme['colors'][props.color] }
     )}
     ${...(props) => (
         { backgroundColor: props.theme['colors'][props.bg] }
     )}
   }
`

Точки останова и адаптивные темы

Для адаптивных тем styled-system позволяет установить breakpoints в теме, а затем позволяет передавать массив в качестве опоры с разными значениями для каждой точки останова. styled-system использует подход «сначала мобильные», поэтому первый индекс всегда будет наименьшей точкой останова.

<Box
  width={[
    1, // 100% below the smallest breakpoint (all viewports)
    1 / 2, // 50% from the next breakpoint and up
    1 / 4, // 25% from the next breakpoint and up
  ]}
/>
// theme.js
const breakpoints = ['40em', '52em', '64em', '80em']
export default { breakpoints };

Заключение

Меня вдохновило то, как разработчики styled-components и styled-system использовали расширенную функциональность шаблонных литералов и тегированных шаблонов, чтобы предоставить пользователям интуитивно понятный способ добавления SCSS в компоненты React.

Видели ли вы в последнее время какое-нибудь интересное использование общих функций в своей работе? Поделись, пожалуйста!

Источники

Стилизованная система

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

Магия 💅 styled-components

Веб-документы MDN: литералы шаблонов