Примечание: авторский перевод статьи Web Content Elements

В HTML разработке мы используем тег в качестве дескриминатора - тег определяет элемент. Мы используем классы, чтобы применять стили к HTML элементам. Разработчик создает структуру и описывает стили руководствуясь правилами конкретного проекта, своим опытом и общепринятыми рекомендациями.

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

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

Web Content Elements(WCE) - это концепция, шаблон проектирования, который позволяет описать любые решения подобного рода линейно и однообразно.

Согласно WCE, тег - это переменная для генерации синтаксической структуры элемента разметки.

Основная идея паттерна заключается в группировке элементов по роли, которую они представляют на странице.

???? Элементы сгруппированы по их ролям на странице, а не тегам.

Основные роли:

  • Block (div, section, main, footer, e.t.c.)

  • Text (p, span, b, e.t.c)

  • Image (img)

  • Link (a)

  • Button (button)

  • Divider (hr)

  • List (ul, ol, dl, e.t.c.)

Дополнительные роли(служебные функции для разработчиков):

  • Custom

    Создавать дополнительные HTML структуры согласно WCE паттерна

React Content Elements это библиотека, которая реализует Web Content Elements паттерн с помощью Typescript и React.

Official webpage

NPM package

Github

Basics

Content Elements(CE) названы по роли, которую они представляют в контексте страницы(DOM дерева).

Примеры использования CE элементов:

import CE from 'react-content-elements';

<CE.Block>Block Content Element</CE.Block>
// HTML
<div class="ce ce-block">Block Content Element</div>

<CE.Text className="class-name">Text Content Element</CE.Text>
// HTML
<p class="class-name ce ce-text">Text Content Element</p>

<CE.Image src="link/to/the-image.jpg" />
// HTML
<img class="ce ce-image" src="link/to/the-image.jpg" />
  
<CE.Link>Link Content Element</CE.Link>
// HTML
<a class="ce ce-link">Link Content Element</a>

<CE.Button>Button Content Element</CE.Button>
// HTML
<button class="ce ce-link" type="button">Button Content Element</button>

<CE.Divider />
// HTML
<hr class="ce ce-divider" />

Каждый CE элемент имеет:

  • дефолтный тег

    Определяется по имени элемента, напримерp’ для Text Content Element

  • базовый класс: ce ce-[name]

    напримерce ce-text” для Text Content Element

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

Как мы уже знаем, каждый CE элемент обладает базовым классом и RCE ****предлагает набор миксинов, для того чтобы просто и понятно применять стили к данным элементам:

@use 'react-content-element/styles/utils' as *;

/* by content element */
@include byName {
  color: red;
}
// CSS
.ce {
  color: red;
}

/* by content element name */
@include byName('text') {
  font-size: 16px;
}
// CSS
.ce-text {
  font-size: 16px;
}

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

Hello, Real Development World!

В данной статье будут рассмотрены следующие вопросы:

  1. Как управлять тегами и получать нестандартные значения (например, 'h1', 'section' и т.д.)?

  2. Как определять стили для конкретных элементов, а не целой группы(например Text или Image)? Просто добавить класс(через свойство JSX элемента ‘className’) и применить БЭМ?

  3. Создание базовых структур достаточно просто и очевидно, но как быть с более сложными, комплексными примерами, такими как списки, макеты, таблицы или любые другие кастомные структуры?

Каждый CE элемент кастомизируется через специальные свойства:

  • Tag

  • Modifiers

  • Content

Также есть дополнительные специальные свойства:

  • Config
    Альтернативная точка входа для любых свойств элемента (CE или JSX), имеет более высокий приоритет.

  • If
    Это свойство фильтрует элемент, приводя значение свойства к логическому типу. Если значение равно «false», то разметка не генерируется, а функция возвращает значение null.

Tag

Мы можем определить тэг элемента через свойство ‘tag’

import CEfrom 'react-content-elements';

<CE.Text tag="h1">Rule your mind or it will rule you.</CE.Text>
// HTML
<h1 class="ce ce-text">Rule your mind or it will rule you.</h1>

Modifiers

Мы можем расширить список классов CE элемента с помощью свойства "modifiers"

import CEfrom 'react-content-elements';

<CE.Link className="nav-link" modifiers={['bold']}>Navigation Link</CE.Text>
<CE.Link className="nav-link" modifiers={['bold', 'active']}>
  Active Navigation Link
</CE.Text>
// HTML
<a class="nav-link ce ce-link ce--bold">Navigation Link</a>
<a class="nav-link ce ce-link ce--bold ce--active">Active Navigation Link</a>

К классам, созданным с помощью модификаторов, можно обратиться с использованием следующих SASS-миксинов:

@use 'react-content-element/styles/utils' as *;

/* by content element modifier */
@include byModifier('bold') {
  font-weight: bold;
}
// CSS
.ce--bold {
  font-weight: bold;
}

/* by selector with content element modifier */
.nav-link {
  @include withModifier('active') {
    color: red;
  }
}
// CSS
.nav-link.ce--active {
  color: red;
}

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

import CEfrom 'react-content-elements';

<CE.Text modifiers={['title']}>Be yourself; everyone else is already taken.</CE.Text>
// HTML
<h3 class="ce ce-text ce--title">Be yourself; everyone else is already taken.</h3>

Список тегов byName(дефолтные теги) и byModifier(по модификатору) настраивается через конфигурацию CE элемента:

import CEfrom 'react-content-elements';

CE.setup({
  tags: {
    byName: {
      'text': 'p',
    },
    byModifier: {
      'title': 'h3',
    },
  },
})

Также модификаторы реализуют полезную фичу для создания адаптивных макетов:

  • Above & Beyond

    above-[breakpoint-name] - стили применяются для экранов шириной меньше заданного значения для брейкпойнта

    beyond-[breakpoint-name] - стили применяются для экранов шириной больше или равной заданному значения для брейкпойнта

import CEfrom 'react-content-elements';

<CE.Text modifiers={["title-above-xl", "accent-beyond-sm"]}>Don't be dead serious about your life – it's just a play.</CE.Text>
// HTML
<p class="ce ce-text ce--title-above-xl ce--accent-beyond-sm">Don't be dead serious about your life – it's just a play.</p>

<CE.Block modifiers={["row-above-md", "section-below-xl"]}>The way you speak to yourself matters.</CE.Block>
// HTML
<div class="ce ce-block ce--row-above-md ce--section-below-xl">The way you speak to yourself matters.</div>

Content

Это свойство определяет HTML содержимое вашего элемента. Оно обладает приоритетом над данными, переданными через свойства "children".

import CEfrom 'react-content-elements';

<CE.Text content="Simplicity is the ultimate sophistication."/>
// HTML
<p class="ce ce-text">Simplicity is the ultimate sophistication.</p>

<CE.Text content="Content by property">Creativity is intelligence having fun.</CE.Text>
// HTML
<p class="ce ce-text">Content by property</p>

Config

Мы можем определять любые свойства для элемента через “config”. Значения переданные через это свойство будут иметь высший приоритет.

import CEfrom 'react-content-elements';

<CE.Text config={{ modifiers: ['accent'], tag: 'h2' }}>Simple example with config</CE.Text>
// HTML
<h2 class="ce ce-text ce--accent">Simple example with config</h2>

<CE.Text 
  tag="h3"
  modifiers={['bold']}
  config={{
    modifiers: ['accent'], 
    tag: 'h2', 
    content: '<i>Content by config</i>' 
  }}
>
  Another example with config
</CE.Text>
// HTML
<h2 class="ce ce-text ce--accent"><i>Content by config</i></h2>

If

Булево значение, которое используется для фильтрации элементов по условию. Если передается ложное значение, элемент не будет отображаться.

import CEfrom 'react-content-elements';

<CE.Text if={0}>Nothing is impossible</CE.Text>
// HTML
// The element is not rendered

<CE.Text if={1}>Everything is possible</CE.Text>
// HTML
<p class="ce ce-text">Everything is possible</p>

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

  • List

  • Custom

List

Структура HTML списка включает в себя два элемента (ul и li). Вот как мы можем воспроизвести это с помощью CE элементов.

import CEfrom 'react-content-elements';

<CE.List />
// HTML
<ul class="ce ce-list"></ul>

<CE.List>
  <CE.Text className="first">1st item</CE.Text>
  <CE.Text modifiers={['bold']}>2nd item</CE.Text>
</CE.List>
// HTML
<ul class="ce ce-list">
  <li class="ce ce-item">
    <p class="first ce ce-text">1st item</p>
  </li>
  <li class="ce ce-item">
    <p class="ce ce-text ce--bold">2nd item</p>
  </li>
</ul>

<CE.List
  items={[
    { content: '1st item', modifiers: ['accent'] },
    { content: '2nd item', tag: 'span' }
  ]}
  ItemTemplate={CE.Text}
/>
// HTML
<ul class="ce ce-list">
  <li class="ce ce-item">
    <p class="ce ce-text ce--accent">1st item</p>
  </li>
  <li class="ce ce-item">
    <span class="ce ce-text">2nd item</span>
  </li>
</ul>

Custom

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

Для генерации базового класса по имени элемента существует вспомогательная функция getCEClassName:

import { getCEClassName } from 'react-content-elements';

getCEClassName('example', ['modifier', false && 'another-modifier']);
// 'ce ce-example ce--modifier'
/* custom element */
const CustomTable = ({
  className,
  headerCellModifiers,
  cellHeaders,
  rowsData,
}) => {
const baseClassName = getCEClassName('custom-table');
const trHeaderClassName = getCEClassName('custom-table-header');
const thClassName = getCEClassName('custom-table-cell', headerCellModifiers);

const trClassName = getCEClassName('custom-table-row');
const tdClassName = getCEClassName('custom-table-cell');

const TableRow = ({ rowData }: any) => (
    <tr className={trClassName}>
      {rowData.map((rowValue: string, rowID: string) => (
        <td key={rowID} className={tdClassName}>
          {rowValue}
        </td>
      ))}
    </tr>
  );

return (
    <table className={[className, baseClassName].join(' ')}>
      <thead>
        <tr className={trHeaderClassName}>
          {cellHeaders.map((header: string, headID: string) => (
            <th key={headID} className={thClassName}>
              {header}
            </th>
          ))}
        </tr>
      </thead>
      <tbody>
        {rowsData.map((rowData: string, rowID: string) => (
          <TableRow rowData={rowData} key={rowID} />
        ))}
      </tbody>
    </table>
  );
};

/* usage of custom element */
const headers = ['header 1', 'header 2', 'header 3', 'header 4'];
const firstRowData = ['cell 1', 'cell 2', 'cell 3', 'cell 4'];
const rowsData = [firstRowData];
const headerCellModifiers = ['bold'];

<CE.Custom
  CustomTemplate={CustomTable}
  cellHeaders={headers}
  rowsData={rowsData}
  headerCellModifiers={headerCellModifiers}
/>
/* HTML */
<table className="ce ce-custom-table">
  <thead>
  <tr className="ce ce-custom-table-header">
    <th className="ce ce-custom-table-cell ce--bold">header 1</th>
    <th className="ce ce-custom-table-cell ce--bold">header 2</th>
    <th className="ce ce-custom-table-cell ce--bold">header 3</th>
    <th className="ce ce-custom-table-cell ce--bold">header 4</th>
  </tr>
  </thead>
  <tbody>
  <tr className="ce ce-custom-table-row">
    <td className="ce ce-custom-table-cell">cell 1</td>
    <td className="ce ce-custom-table-cell">cell 2</td>
    <td className="ce ce-custom-table-cell">cell 3</td>
    <td className="ce ce-custom-table-cell">cell 4</td>
  </tr>
  </tbody>
</table>

Комментарии (2)


  1. SWATOPLUS
    19.07.2023 18:13

    Мы можем определять любые свойства для элемента через “config”. Значения переданные через это свойство будут иметь высший приоритет.
    Наоборот лучше передавать конфиг и оверрайдить его тем что в конкретных свойствах. Обычный юзкейс, когда есть большая дефолтная конфигурация и оверрайд на месте.

    Вообще это выглядит как попытка решить проблему инкапсуляции стилей. Как-то больше не получается осознать для чего это нужно. Я не вижу что это решает какую-то боль. (Может быть потому что я в основном пишу на Angular и Vue, стараясь избегать React)


    1. pkuznetsovdev Автор
      19.07.2023 18:13

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

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

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

      UPD: Также можно переопределять свойства переданные инлайн, например 'спредить' пропсы переданные выше...или найти еще 100 других способов) Но идея этого подхода как раз и заключается в том, чтобы дать наиболее простой и однозначный способ решить эту задачу.

      Тогда весь код будет выглядеть примерно так:

      <CE.Block config={{ className: 'daily-card' }}>
        <CE.Image config={{ src: image, modifiers: ['card-image'] }} />
        <CE.Block config={{ modifiers: ['card-info'] }}>
          <CE.Text config={{ modifiers: ['card-title'] }}>{title}</CE.Text>
        </CE.Block>
      </CE.Block>
      

      На мой взгляд, такой синтаксис только ухудшает читабельность кода. Для сравнения:

      <CE.Block className='daily-card'>
        <CE.Image modifiers={['card-image']} src={imageSrc} />
        <CE.Block modifiers={['card-info']}>
          <CE.Text modifiers={['card-title']}>{title}</CE.Text>
        </CE.Block>
      </CE.Block>
      

      Config, возможно, не самое удачное имя для названия пропса и вводит в заблуждение. Это в первую очередь - альтернативная, дополнительная точка входа, в случае если это по каким-то причинам будет необходимо.

      Вообще это выглядит как попытка решить проблему инкапсуляции стилей.

      Не понимаю, почему это так выглядит.

      Я не вижу что это решает какую-то боль.

      Web Content Elements(WCE) - это подход к созданию разметки и организации системы стилей.

      React Content Elements(RCE) - это реализация принципов WCE подхода для конкретной среды.

      WCE предлагает частичную стандартизацию как некоторых шагов(этапов) для создания HTML и CSS, так и готового результата(DOM tree, styles). Проще всего ощутить пользу можно будет при применении этого подхода на проектах с большой кодовой базой и большими командами.

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

      Пример готовой разметки:

      UPD: Если вернуться к проблеме инкапсуляции стилей, то на изображении выше можно заметить, что JSX свойство className используется скорее как неймспейс для группы вложенных элементов, а не селектор для конкретного элемента. Да, это является в какой-то степени решением проблемы инкапсуляции стилей, но это скорее сайд эффект данного подхода, чем его основная или даже второстепенная цель.