Compound компоненты

Compound компоненты в Atomico представляют собой способ объединения нескольких логически связанных элементов в единый компонент, сохраняя при этом высокую степень переиспользуемости и управляемости состояния. Это особенно важно при построении сложных интерфейсов, где один визуальный элемент может состоять из множества вложенных подкомпонентов с разными обязанностями.


Создание compound компонента

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

Простейший пример:

import { component, useState, html } from "atomico";

function Tabs({ children }) {
    const [activeIndex, setActiveIndex] = useState(0);

    return html`
        <div class="tabs">
            <div class="tabs-header">
                ${children.map((child, index) => html`
                    <button
                        class="${index === activeIndex ? "active" : ""}"
                        oncl ick=${() => setActiveIndex(index)}
                    >
                        ${child.props.label}
                    </button>
                `)}
            </div>
            <div class="tabs-content">
                ${children[activeIndex]}
            </div>
        </div>
    `;
}

Tabs.props = {
    children: Array
};

export const tabs = component(Tabs);

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


Использование дочерних компонентов

Compound компоненты часто используют дочерние компоненты, чтобы структура оставалась гибкой. Для передачи дочерних элементов применяется стандартный JSX-подобный синтаксис через слоты или children.

import { component, html } from "atomico";
import { tabs } from "./Tabs";

function Tab({ children }) {
    return html`<div class="tab-panel">${children}</div>`;
}

Tab.props = {
    label: String,
    children: String
};

export const tab = component(Tab);

Теперь Tabs и Tab могут быть объединены:

html`
<tabs>
    <tab label="Первый">Содержимое первого таба</tab>
    <tab label="Второй">Содержимое второго таба</tab>
    <tab label="Третий">Содержимое третьего таба</tab>
</tabs>
`;

Ключевой момент: Tabs не зависит от конкретной реализации Tab. Любой компонент, соответствующий интерфейсу дочернего элемента (наличие label и children), может быть использован.


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

Atomico предлагает реактивные состояния через useState и useEffect. В compound компонентах важно синхронизировать состояние между контейнером и дочерними элементами.

Пример расширенного Tabs с внешним контролем:

function ControlledTabs({ children, value, onChange }) {
    return html`
        <div class="tabs">
            <div class="tabs-header">
                ${children.map((child, index) => html`
                    <button
                        class="${index === value ? "active" : ""}"
                        oncl ick=${() => onChange(index)}
                    >
                        ${child.props.label}
                    </button>
                `)}
            </div>
            <div class="tabs-content">
                ${children[value]}
            </div>
        </div>
    `;
}

ControlledTabs.props = {
    children: Array,
    value: Number,
    onChange: Function
};

export const controlledTabs = component(ControlledTabs);

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


Передача дополнительных данных дочерним компонентам

Для расширения возможностей часто используется механизм передачи дополнительных пропсов через контекст или специальные атрибуты. В Atomico это реализуется через Context:

import { createContext } from "atomico";

const TabsContext = createContext();

function TabsProvider({ children, activeIndex, setActiveIndex }) {
    return html`
        <${TabsContext.Provider} value=${{ activeIndex, setActiveIndex }}>
            ${children}
        </${TabsContext.Provider}>
    `;
}

Дочерние компоненты могут получать доступ к состоянию:

import { useContext } from "atomico";
import { TabsContext } from "./TabsContext";

function TabPanel({ children, index }) {
    const { activeIndex } = useContext(TabsContext);
    return html`${activeIndex === index ? children : null}`;
}

Такой подход полностью отделяет логику управления состоянием от визуальных компонентов.


Стилизация compound компонентов

Atomico позволяет использовать scoped CSS через объект style внутри функции компонента или через внешние CSS-файлы. При построении compound компонентов рекомендуется:

  • Вынести основные стили контейнера отдельно от подкомпонентов.
  • Использовать классы для конкретных частей, чтобы дочерние компоненты не вмешивались в стили родителя.
  • Поддерживать темизацию через CSS-переменные.

Пример:

function Tabs({ children }) {
    return html`
        <style>
            .tabs-header { display: flex; gap: 8px; }
            .tabs-content { padding: 16px; border: 1px solid #ccc; }
            .active { font-weight: bold; }
        </style>
        <div class="tabs">
            <div class="tabs-header">
                ${children.map((child, index) => html`<button>${child.props.label}</button>`)}
            </div>
            <div class="tabs-content">${children[0]}</div>
        </div>
    `;
}

Резюме ключевых принципов

  1. Разделение логики и структуры — контейнер управляет состоянием, дочерние компоненты отвечают за отображение.
  2. Переиспользуемость — любой компонент, удовлетворяющий интерфейсу, может быть вставлен как дочерний.
  3. Реактивность — управление состоянием через useState и useEffect, возможность контролировать состояние извне.
  4. Контекст — передача состояния и функций между контейнером и дочерними элементами без прямой передачи пропсов.
  5. Изоляция стилей — использование scoped CSS и классов для предотвращения конфликтов между компонентами.

Compound компоненты в Atomico создают гибкую архитектуру интерфейса, обеспечивая чистое разделение обязанностей и удобное управление состоянием сложных UI-модулей.