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 — для передачи состояния от родителя к дочерним
компонентам.Такой подход гарантирует, что состояние интерфейса централизовано, и компонент остаётся предсказуемым.
Передача контекста через 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>
));
}
@State и @Prop упрощает
синхронизацию между родителем и детьми.Compound Components в Stencil — это инструмент построения масштабируемых, поддерживаемых и гибких интерфейсов, сочетающий преимущества веб-компонентов с современной реактивной архитектурой.