Compound components

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


Создание родительского компонента

Родительский компонент выполняет роль контейнера для дочерних компонентов и управляет их поведением. В Stencil он создаётся как стандартный веб-компонент с использованием декораторов @Component и @State или @Prop для управления внутренним состоянием.

Пример структуры родительского компонента:

import { Component, h, State } from '@stencil/core';

@Component({
  tag: 'tabs-container',
  styleUrl: 'tabs-container.css',
  shadow: true,
})
export class TabsContainer {
  @State() activeTab: string = 'tab1';

  setActiveTab(tabId: string) {
    this.activeTab = tabId;
  }

  render() {
    return (
      <div class="tabs-container">
        <slot name="tab-buttons" />
        <slot name="tab-panels" />
      </div>
    );
  }
}

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


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

Дочерние компоненты реализуют отдельные части интерфейса, которые должны взаимодействовать с родителем. Например, кнопки вкладок:

import { Component, h, Prop, Event, EventEmitter } from '@stencil/core';

@Component({
  tag: 'tab-button',
  styleUrl: 'tab-button.css',
  shadow: true,
})
export class TabButton {
  @Prop() tabId: string;
  @Prop() active: boolean = false;

  @Event() selectTab: EventEmitter<string>;

  handleClick = () => {
    this.selectTab.emit(this.tabId);
  }

  render() {
    return (
      <button class={this.active ? 'active' : ''} onCl ick={this.handleClick}>
        <slot />
      </button>
    );
  }
}

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


Связывание компонентов через события

Stencil использует события для передачи информации от дочерних компонентов к родителю. Родитель подписывается на событие и изменяет своё состояние:

<tabs-container>
  <tab-button slot="tab-buttons" tab-id="tab1" onSelect Tab={(e) => this.setActiveTab(e.detail)}>Вкладка 1</tab-button>
  <tab-button slot="tab-buttons" tab-id="tab2" onSelect Tab={(e) => this.setActiveTab(e.detail)}>Вкладка 2</tab-button>

  <tab-panel slot="tab-panels" panel-id="tab1" active={this.activeTab === 'tab1'}>Контент 1</tab-panel>
  <tab-panel slot="tab-panels" panel-id="tab2" active={this.activeTab === 'tab2'}>Контент 2</tab-panel>
</tabs-container>

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


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

Stencil позволяет использовать @State, @Prop и реактивные методы для организации Compound Components:

  • @State — для внутреннего состояния родителя, которое влияет на рендеринг дочерних компонентов.
  • @Prop — для передачи состояния от родителя к дочерним компонентам.
  • События — для обратной связи от дочерних компонентов к родителю.

Такой подход гарантирует, что состояние интерфейса централизовано, и компонент остаётся предсказуемым.


Преимущества использования Compound Components

  1. Гибкость: возможность комбинировать различные дочерние компоненты без дублирования логики.
  2. Повторное использование: дочерние компоненты можно использовать вне контекста конкретного родителя.
  3. Прозрачность состояния: родительский компонент является единственным источником истины.
  4. Лёгкость тестирования: отдельные элементы можно тестировать независимо.

Расширенные техники

Передача контекста через Provider: иногда необходимо, чтобы все дочерние компоненты имели доступ к общему состоянию без прямого связывания через props. Для этого используют контекстные объекты (Context API) или глобальные провайдеры состояния.

Динамическая генерация дочерних компонентов: родитель может создавать дочерние компоненты на лету, используя массив данных и map в render().

render() {
  return this.tabs.map(tab => (
    <tab-button 
      slot="tab-buttons" 
      tab-id={tab.id} 
      active={this.activeTab === tab.id} 
      onSelect Tab={(e) => this.setActiveTab(e.detail)}
    >
      {tab.title}
    </tab-button>
  ));
}

Особенности Stencil для Compound Components

  • Shadow DOM обеспечивает изоляцию стилей для дочерних компонентов.
  • Реактивность @State и @Prop упрощает синхронизацию между родителем и детьми.
  • События DOM позволяют легко интегрироваться с другими библиотеками или vanilla JS.
  • Слоты дают возможность гибкого размещения элементов без жёсткой структуры.

Compound Components в Stencil — это инструмент построения масштабируемых, поддерживаемых и гибких интерфейсов, сочетающий преимущества веб-компонентов с современной реактивной архитектурой.