Перейти к основному содержимому

Компоненты

Зачем нам нужны компоненты?

Вопрос может показаться странным, но у него есть свои причины. Упрощенное представление о реактивной разработке в веб-е говорит нам о том, что есть данные (состояние) и есть разметка (DOM), а больше ничего не нужно. Данные поменялись, разметка тоже поменялась.

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

Данные + Разметка + Преобразование

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

Данные + Преобразование + Разметка

В Chorda компонентный слой, инкапсулируя правила преобразования, существует самостоятельно:

Данные + Преобразование + Разметка

Виды компонентов

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

важно!

За управление структурой всегда отвечает родительский компонент

Для того, чтобы мы могли примешать опции к конкретному вложенному компоненту, он должен быть идентифицирован. Естественно, позиция в массиве children для этого не подходит, поскольку мы ее на самом деле не знаем - другие примеси могут изменить последовательность и количество вложенийю Остаются только key-value коллекции на базе простого Object. Ключ в них и будет тем идентификатором, который нам нужен

С другой стороны от массивов Array мы полностью отказаться тоже не можем, поскольку выстроить честную последовательность с помощью Object довольно затруднительно

Индексированные компоненты

Последовательно упорядоченный набор компонентов задается массивом items

export default () => {
return {
items: [
{text: '1'},
{text: '2'},
{text: '3'},
],
}
}

Если все компоненты имеют общий набор опций, его можно вынести отдельно, используя опцию defaultItem

export default () => {
return {
tag: 'ul',
defaultItem: {
tag: 'li',
},
items: [
{text: '1'},
{text: '2'},
{text: '3'},
],
}
}
caution

items не обладают свойством аддитивности. Это означает, что патч items с новым списком компонентов полностью заменит старый

Именованные компоненты

Непоследовательный набор компонентов, идентифицированных ключом, задается блоком components

export default () => {
return {
components: {
title: {text: 'Title'},
content: {text: 'Some text'}
},
}
}

Так же как и в случае items можно указать общий набор опций через defaultComponent

export default () => {
return {
defaultComponent: {
tag: 'i',
css: 'icon'
},
components: {
leftIcon: {css: 'icon1'},
rightIcon: {css: 'icon2'},
}
}
}

Именованные компоненты как правило соответствуют критериям:

  1. Представляют из себя разнородные компоненты, которые имеют мало общих опций
  2. Ориентированы на тонкую настройку через последующее смешивание
  3. Предполагают включение/отключение

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

defaultComponent + templates[foo] + components[foo]

Здесь видно, что если мы в качестве значения в блоке components укажем false, то примесь становится пустой и компонент исключается родителем из списка дочерних

export default () => {
return {
templates: {
header: { text: 'Title' },
body: { text: 'Content' },
footer: { text: 'Footer' }
},
components: {
header: true,
body: true,
footer: false, // подвал отключаем
}
}
}
важно!

Именованные компоненты обладают свойством аддитивности. Это означает, что частичные патчи components не удаляют предыдущие значения, а сливаются с ними

внимание!

Патч templates вызовет пересоздание компонента

Примеры компоновки

Для наглядности можно рассмотреть несколько типовых коллекций компонентов

Список

li + li + li + li

Последовательность однотипных элементов

const blueprint = {
defaultItem: {
tag: 'li'
},
items: [{}, {}, {}, {}]
}

Набор

div + span + p + button

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

const blueprint = {
items: [
{tag: 'div'},
{tag: 'span'},
{tag: 'p'},
{tag: 'button'},
]
}

или

const blueprint = {
components: {
a: {tag: 'div'},
b: {tag: 'span'},
c: {tag: 'p'},
d: {tag: 'button'},
}
}

Выбор зависит от того типа данных, которым вы хотите управлять компонентами: Array или Object

Список с аддонами

span + li + li + li + li + span

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

const blueprint = {
defaultItem: {
tag: 'li'
},
items: [{}, {}, {}, {}],
components: {
before: {tag: 'span'},
after: {tag: 'span'},
}
}

Уплощенный (flatten) список

input + label + input + label + input + label + input + label

Особенности верстки

const blueprint = {
defaultItem: {
layout: passthruLayout,
components: {
check: {tag: 'input'},
label: {tag: 'label'},
}
},
items: [{}, {}, {}, {}],
}

Используется компоновка passthruLayout, которая говорит, что компонент исключен из отрисовки, а его дочерние компоненты передаются родительскому

Связывание с данными

Статическое

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

// внешняя константа, задающая список дочерних элементов
const itemList = [{text: 'Item1'}, {text: 'Item2'}]

export default () => {
return {
items: itemList
}
}

Динамическое

Динамическое изменение дочерних компонентов выполняется так же как и изменение других свойств компонента - через реактивный скоуп

// внешняя переменная, через которую мы управляем списком дочерних компонентов
const observableItems = observable([{text: 'Item1'}, {text: 'Item2'}])

export default () => {
return {
reactions: {
// реакция патчит items
data: v => ({items: v})
},
injections: {
// инжектируем в скоуп реактивную переменную
data: () => observableItems
}
}
}

Управление потоком

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

Ветвления

Для статических компонентов легко можно использовать императивный if/else или ?:

export default (showButton) => {
return {
components: {
content: showButton ? {tag: 'button'} : {tag: 'div'}
}
}
}

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

export default (showButton) => {
return {
// сами компоненты вынесены в шаблоны, чтобы их можно было переопределить
templates: {
button: {tag: 'button'},
other: {tag: 'div'},
},
reactions: {
// реакция патчит компоненты, используя их в качестве переключателей
isShowButton: v => ({
components: {
button: v,
other: !v
}
})
},
defaults: {
// создаем источник сигналов
isShowButton: () => observable(showButton)
}
}
}

Циклы

Решение по сути своей не отличается от императивного подхода.

Для статических компонентов используются for или forEach

export default (names) => {
return {
tag: 'ul',
items: names.forEach(name => ({tag: 'li', text: name}))
}
}

Динамические коллекции можно создавать через реактивные переменные

export default (names) => {
return {
tag: 'ul',
defaultItem: {
tag: 'li'
},
reactions: {
it: v => ({items: v})
},
injections: {
// создаем реактивную переменную names
names: () => observable(names),
// создаем вычисляемую переменную, которая содержит список новых компонентов
it: ($) => computable(() => $.names.map(name => ({text: name}))),
}
}
}

Проекция скоупа

При создании дочерних компонентов через патч items или components, скоуп новых компонентов будет соответствовать родительскому. Если необходимо сузить (спроецировать) скоуп, это следует указать явно с использованием iterator

export default () => {
return {
defaults: {
data: () => observable([{/* значение 1 */}, {/* значение 2 */}])
},
reactions: {
// явно указываем, что в items передаются не конфигурации, а дочерние скоупы
data: (v) => ({items: iterator(v)})
}
}
}

Свойства элементов коллекции будут спроецированы на дочерние скоупы