Компоненты
Зачем нам нужны компоненты?
Вопрос может показаться странным, но у него есть свои причины. Упрощенное представление о реактивной разработке в веб-е говорит нам о том, что есть данные (состояние) и есть разметка (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'},
}
}
}
Именованные компоненты как правило соответствуют критериям:
- Представляют из себя разнородные компоненты, которые имеют мало общих опций
- Ориентированы на тонкую настройку через последующее смешивание
- Предполагают включение/отключение
Для того, чтобы разделить управление именованным компонентом и его базовые опции, существует блок 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)})
}
}
}
Свойства элементов коллекции будут спроецированы на дочерние скоупы