Compound Components

SkateJS предоставляет мощный инструмент для построения веб-компонентов, поддерживающий реактивность, слоты и декларативное управление состоянием. Одной из продвинутых концепций является использование compound components, когда один основной компонент объединяет несколько взаимосвязанных компонентов, взаимодействующих друг с другом. Эта архитектура позволяет строить гибкие и расширяемые UI-элементы без потери инкапсуляции и контроля состояния.


Основная идея

Compound components позволяют разрабатывать группы компонентов, где:

  • Один компонент выполняет роль контейнера или контроллера состояния.
  • Внутренние компоненты используют контекст контейнера для синхронизации состояния и поведения.
  • Компоненты могут взаимодействовать без прямой передачи пропсов через несколько уровней дерева.

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


Создание контейнера

Контейнерный компонент является центральным элементом, который управляет состоянием всех дочерних компонентов. Рассмотрим пример с компонентом <tab-group> для табов:

import { define, props, h } from 'skatejs';

class TabGroup extends HTMLElement {
  static props = {
    activeIndex: props.number
  };

  constructor() {
    super();
    this.activeIndex = 0;
  }

  connectedCallback() {
    this.addEventListener('tab-selected', (event) => {
      this.activeIndex = event.detail.index;
    });
  }

  renderCallback() {
    const children = Array.from(this.children);
    return h('div', {}, children.map((child, index) =>
      h('div', { class: index === this.activeIndex ? 'active' : 'inactive' }, child)
    ));
  }
}

define('tab-group', TabGroup);

Ключевые моменты:

  • activeIndex хранит текущее активное состояние.
  • tab-selected используется для уведомления контейнера о действиях дочерних элементов.
  • renderCallback динамически добавляет классы к дочерним элементам на основе состояния.

Дочерние компоненты

Дочерние компоненты (<tab-item>) не управляют состоянием напрямую, а сообщают контейнеру о действиях через кастомные события.

class TabItem extends HTMLElement {
  static props = {
    index: props.number
  };

  constructor() {
    super();
    this.index = 0;
  }

  connectedCallback() {
    this.addEventListener('click', () => {
      this.dispatchEvent(new CustomEvent('tab-selected', {
        bubbles: true,
        composed: true,
        detail: { index: this.index }
      }));
    });
  }

  renderCallback() {
    return h('button', {}, this.textContent);
  }
}

define('tab-item', TabItem);

Особенности:

  • Свойство index связывает каждый элемент с позицией в контейнере.
  • Событие tab-selected всплывает через дерево DOM, позволяя контейнеру управлять состоянием.
  • Логика управления состоянием полностью отделена от представления.

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

SkateJS поддерживает работу со слотами, что позволяет контейнеру и дочерним компонентам быть полностью независимыми в структуре DOM:

class TabGroupSlot extends HTMLElement {
  static props = { activeIndex: props.number };

  constructor() {
    super();
    this.activeIndex = 0;
  }

  connectedCallback() {
    this.addEventListener('tab-selected', e => this.activeIndex = e.detail.index);
  }

  renderCallback() {
    return h('div', {}, h('slot'));
  }
}

define('tab-group-slot', TabGroupSlot);

Использование слота позволяет помещать <tab-item> внутрь <tab-group-slot> без необходимости прямого управления DOM:

<tab-group-slot>
  <tab-item index="0">Первый</tab-item>
  <tab-item index="1">Второй</tab-item>
  <tab-item index="2">Третий</tab-item>
</tab-group-slot>

Реактивность и синхронизация

SkateJS автоматически отслеживает изменения свойств и перерисовывает компоненты. В compound components это важно, чтобы:

  • Контейнер обновлял активные состояния дочерних элементов.
  • Дочерние компоненты реагировали на изменения через свойства или события.

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


Расширение паттерна

Compound components легко расширяются:

  1. Добавление новых типов дочерних элементов.
  2. Поддержка динамического добавления/удаления элементов через слоты.
  3. Расширение событий для управления не только состоянием, но и другими атрибутами (например, disabled, highlighted).

Пример: добавление <tab-panel> для отображения содержимого каждого таба:

class TabPanel extends HTMLElement {
  static props = { index: props.number };

  renderCallback() {
    const group = this.closest('tab-group');
    const active = group?.activeIndex === this.index;
    return h('div', { style: { display: active ? 'block' : 'none' } }, this.textContent);
  }
}

define('tab-panel', TabPanel);

Преимущества подхода

  • Инкапсуляция логики: каждый компонент отвечает только за свою часть.
  • Гибкость и переиспользуемость: контейнер и дочерние компоненты можно использовать в разных комбинациях.
  • Чистый код: управление состоянием централизовано в контейнере, дочерние компоненты остаются декларативными.
  • Совместимость со слотом и Shadow DOM: структура DOM не мешает синхронизации состояний.

Compound components в SkateJS представляют собой эффективный способ создания сложных UI-компонентов с четким разделением обязанностей и декларативной реактивностью. Правильная организация контейнера и дочерних компонентов позволяет создавать масштабируемые интерфейсы без лишнего связывания данных и ручного управления DOM.