Составные компоненты
Составной Smart-компонент собирает несколько Smart-компонентов в один сценарий. Глава описывает, как распределяется ответственность между уровнями, какие требования предъявляет стандарт и как связать компоненты без кода проекта.
Определение
Составной Smart-компонент (Composite Smart Component) объединяет несколько Smart-компонентов в один сценарий и согласует их настройки, события и состояния. Дети тоже могут быть составными. Несколько компонентов, просто стоящих рядом, составным компонентом не являются. Подключается составной компонент так же, как любой другой.
В манифесте у него kind: "composite".
Примеры: выбор даты (sf-datepicker: поле, кнопки и календарь),
представление данных (таблица с панелью инструментов и меню),
административное меню, редактор страницы.
Составной компонент принадлежит своему владельцу. Framework содержит
универсальные составные компоненты. Составной компонент, который нужен одному
продукту, — например, представление записей с панелью инструментов, — продукт
хранит в своём пакете (см. Пакеты компонентов продуктов и Пакеты компонентов) и связывает с данными приложения
через публичный порт Framework (см. Порт хоста).
Так устроены компоненты Larena из пакета larena (см. Подключение к странице).
Ответственность уровней
Пример: заказ → карточка товара → выбор количества.
- выбор количества сообщает новое число;
- карточка отвечает за товар;
- заказ связывает карточки с данными проекта.
Нижний уровень не ищет родителя через фиксированное число parentElement:
такая связь ломается при изменении разметки. Он отправляет событие, родитель
слушает.
Сама вложенность не требует нового уровня API для каждого контейнера: объединяйте компоненты, когда повторяется целый сценарий с общей логикой.
Пример
Три класса — заказ, карточка товара и выбор количества — зарегистрированы
отдельно. Нажмите кнопку в карточке: событие change самого глубокого
элемента всплывает до заказа, а заказ показывает, от какого элемента оно
пришло. Код JavaScript показывает все уровни и подписку родителя.
<project-order></project-order>
<p id="order-log" role="status">Выберите количество в карточке.</p>
async function start() {
if (typeof window.SF?.loadSmartBase !== 'function') {
await new Promise((resolve, reject) => {
const ready = () => { clearTimeout(timeout); resolve(); };
const timeout = setTimeout(() => { window.removeEventListener('sf-loader-init', ready); reject(new Error('Loader не запустился')); }, 15000);
window.addEventListener('sf-loader-init', ready, { once: true });
});
}
await SF.loadSmartBase();
const { SfBaseElement, html } = SF.smart;
class ProjectQuantity extends SfBaseElement {
static get props() { return { count: { type: Number, default: 1 } }; }
template() {
return html`<button type="button" @click=${() => {
const value = this.getProp('count') + 1;
this.setAttribute('count', String(value));
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true, detail: { value } }));
}}>Количество: ${this.getProp('count')}</button>`;
}
}
class ProjectProduct extends SfBaseElement {
template() { return html`<article class="p-2 bg-surface-container radius-2"><h3>Кружка</h3><project-quantity></project-quantity></article>`; }
}
class ProjectOrder extends SfBaseElement {
template() { return html`<section aria-label="Заказ"><h2>Мой заказ</h2><project-product></project-product></section>`; }
}
ProjectQuantity.define('project-quantity');
ProjectProduct.define('project-product');
ProjectOrder.define('project-order');
document.querySelector('project-order').addEventListener('change', event => {
document.querySelector('#order-log').textContent = `Заказ получил событие от ${event.target.localName}: ${event.detail.value}`;
});
}
start().catch(error => { document.querySelector('#order-log').textContent = error.message; });
Готовность составного компонента
Регистрация родителя не гарантирует готовность детей: тег каждого
библиотечного компонента регистрируется, когда пришли его скрипт и общая среда
Smart-компонентов. Используйте customElements.whenDefined(tag) для класса и
whenRendered() конкретного элемента. Собственное условие готовности
составного сценария задавайте явно.
Требования стандарта
Составной компонент соответствует стандарту simai.composite-component.
Основные требования:
- объявляет свои слоты и порядок их отрисовки;
- глубина вложенности ограничена, повтор пары «компонент и вид» отклоняется как цикл;
- два экземпляра на странице не делят ни идентификаторы в DOM, ни ключи хранилища, ни записи в реестрах;
- отрисовка детерминирована: те же данные и язык дают ту же разметку;
- компонент меняет только своё поддерево и созданные им поверхности (например, панель в портале) и удаляет их вместе с собой;
- одна разметка работает в обоих направлениях письма и в любой теме;
- без скриптов показывается осмысленный эквивалент или прямое сообщение;
- обработчики привязаны к своему экземпляру, а не к первому совпадению в документе.
Связь компонентов без кода проекта
Составные компоненты можно связать через общего родителя декларативно:
документ композиции с типом layout.scope описывает маршруты «из выхода одного
компонента во вход другого». Пример связывает выбор строк в таблице заказов со
входом таблицы строк заказа:
{
"id": "workspace",
"type": "layout.scope",
"props": {"routes": [
{"id": "order-lines",
"from": {"endpoint": "orders", "port": "selection"},
"to": {"endpoint": "lines", "port": "context"}}
]}
}
Что важно заметить:
- маршрут называет только конечные точки и опубликованные порты;
- выбор строк в одной таблице приходит во вход
contextдругой; та входит в загрузку и просит данные у приложения намерением; - применяется только самый новый ответ;
- значение маршрута имеет тип из закрытого перечня, и типы выхода и входа совпадают (Типы значений маршрутов).
Браузерная вложенность и документ композиции
Число уровней вложенности тегов в браузере и ограничения JSON-документа композиции — разные вещи. У документа свои лимиты глубины и числа узлов, а работающий браузерный класс не означает, что для него зарегистрирован декларативный тип.
Смотрите также
- Содержимое и слоты — слоты.
- События — события и их всплытие.
- Связь с приложением: порты хоста и намерения — динамический составной компонент и порты хоста.
- Разделы манифеста — поле
kindманифеста. - Правила взаимодействия компонентов — свойства вниз и события вверх, маршруты между соседями.