Устройство компонента
Глава описывает, из чего состоит Smart-компонент, где живёт его разметка и как регистрируется тег. Она нужна, чтобы написать минимальный собственный компонент — библиотечный или локальный компонент проекта.
Из чего состоит
Smart-компонент — это класс JavaScript, связанный с HTML-тегом. Класс
наследуется от SfBaseElement — базового класса Framework, который сам
наследуется от стандартного HTMLElement. Разметку строит Lit — небольшая
библиотека шаблонов, которая обновляет DOM по результату html.
Библиотечный компонент живёт в исходниках Framework в своей папке:
| Файл | Назначение |
|---|---|
index.js |
Класс компонента и регистрация тега |
js/ |
Шаблоны и вспомогательные модули |
rule.js |
Правило Loader: по какому тегу подключать и от чего зависит |
smart.manifest.json |
Машинное описание компонента (см. Манифест) |
meta.mjs |
Метаданные компонента, если есть; согласованы с манифестом |
docs/docs.md |
Заметки для разработчика, если есть |
Пример показывает точку входа index.js кнопки Framework: класс импортирует
базовый класс и функцию шаблона из js/templates/default.js и регистрирует
тег.
import SfBaseElement from '../../core/js/smart-base';
import { renderButtonTemplate } from './js/templates/default';
class SfButton extends SfBaseElement {
static get props() {
return { text: { default: '' } /* … */ };
}
template() {
return renderButtonTemplate(this.templateContext());
}
}
SfButton.define('sf-button');
Что важно заметить:
smart-baseэкспортируетSfBaseElementпо умолчанию:import SfBaseElement from '../../core/js/smart-base'. Вспомогательные функции этого модуля и модуля языка страницы — именованные экспорты:import { pageText } from '../../core/js/page-language';htmlиnothingберут изlit. Именованный импорт{ SfBaseElement }даётundefined.- Функция шаблона — обычная функция, которая получает контекст и возвращает
разметку
html(см. Шаблоны).
Опубликованная поставка (distr/smart/ в ui) генерируется из
исходников. Править её вручную нельзя: правка теряется при следующей сборке и
ломает совпадение с исходником. Папка компонентов пакета продукта устроена
так же и тоже публикуется только сборкой (см. Пакеты компонентов продуктов).
Где живёт разметка
Компоненты строят разметку внутри самого элемента, в обычном DOM (light DOM). Поэтому на них действуют утилиты и токены страницы, а стили компонента — общие CSS-слои Framework. Shadow DOM компоненты Framework не используют.
Регистрация тега
Скрипт Smart-компонента собран без своей копии Lit и базового класса: их даёт
общая среда Smart-компонентов (core/js/smart-base.js, см. Подключение к странице).
Поэтому тег библиотечного компонента регистрируется не в момент загрузки его
скрипта, а когда пришла общая среда. Код проекта ждёт регистрации через
SF.whenDefined() или customElements.whenDefined().
Тег регистрируется один раз. Повторный customElements.define() для того же
имени Framework не выполняет: базовый класс защищает от двойной регистрации.
Для нового поведения создайте новый тег или обновите исходный класс до загрузки
страницы.
Префикс. Компоненты Framework используют sf-. Компоненты пакета —
префикс владельца, например shop-catalog (см. Пакеты компонентов); компонент,
перенесённый из Framework в пакет, сохраняет имя с sf-. Локальные
компоненты проекта — свой префикс, например project-counter, чтобы не
пересечься ни с теми, ни с другими.
Минимальный компонент
Пример показывает счётчик: объявленное свойство, шаблон, действие и событие
change. Работающий вариант того же счётчика — ниже, в разделе
Работающий пример.
function whenLoaderReady() {
if (window.SF?.loadSmartBase) return Promise.resolve();
return new Promise((resolve) => {
window.addEventListener('sf-loader-init', () => resolve(), { once: true });
});
}
await whenLoaderReady();
await SF.loadSmartBase();
const { SfBaseElement, html } = SF.smart;
const TEXT = {
en: { count: 'Count', add: 'Add' },
ru: { count: 'Количество', add: 'Добавить' },
};
class ProjectCounter extends SfBaseElement {
static get props() {
return { count: { type: Number, default: 0 }, addLabel: { default: '' } };
}
templateContext() {
return this.createPropsTemplateContext({
countText: SF.pageText(TEXT, 'count', this),
addText: this.getProp('addLabel') || SF.pageText(TEXT, 'add', this),
});
}
template() {
const { count, countText, addText } = this.templateContext();
return html`
<output aria-live="polite">${countText}: ${count}</output>
<button type="button"
class="sf-button sf-button--outline sf-button--on-surface sf-button--size-1/2"
@click=${() => this.increment()}>
<span class="sf-button-text-container">${addText}</span>
</button>`;
}
increment() {
const value = this.getProp('count') + 1;
this.setAttribute('count', String(value));
this.dispatchEvent(new CustomEvent('change', {
bubbles: true, composed: true, detail: { value },
}));
}
}
ProjectCounter.define('project-counter');
Что важно заметить:
- Core запускает Loader асинхронно:
SF.loadSmartBaseпоявляется вместе с событиемsf-loader-init. ФункцияwhenLoaderReady()ждёт его и работает в любой момент — до события и после (см. События загрузчика). await SF.loadSmartBase()загружает общую среду Smart-компонентов (core/js/smart-base.js), если её ещё нет на странице, и возвращаетSF.smart. После этого доступныSF.smart.SfBaseElementиSF.smart.html.- Скрипт использует
awaitна верхнем уровне, поэтому подключается как модуль:<script type="module">(см. Локальные компоненты проекта). SF.smart.html— функция шаблонов Lit, поэтому в шаблоне работают все привязки Lit:${…}— текст или значение атрибута,?disabled=${…}— булев атрибут (есть приtrue, нет приfalse),.value=${…}— свойство элемента,@click=${…}— обработчик события.SF.smartдаёт такжеnothing— пустое значение Lit для атрибута, который не нужно выводить:aria-label=${label || nothing}.- Свои слова компонент держит в словаре
TEXT: английское — в записиen, русское — вru;SF.pageText(TEXT, key, this)выбирает слово по языку страницы, а подпись из атрибутаadd-labelсильнее словаря (см. Язык). templateContext()строится черезthis.createPropsTemplateContext({…}): так контекст сохраняетcomponent,rootClass,rootStyleи все объявленные свойства (см. Шаблон и templateContext).- Сам тег блока не образует: базовый класс ставит ему встроенный
style="display: contents", и правило CSS вродеproject-greeting { display: block }на него не действует. Раскладку и стиль тега (rootStyle) шаблон переносит на свой корневой элемент. - В исходниках Framework и пакета те же сущности импортируются из
smart-baseиlit(см. Из чего состоит); в проекте достаточно загруженной браузерной поставки, сборщик не нужен.
Работающий пример
Тот же счётчик в другой записи: функция ожидания встроена в start(), а
скрипт подключается обычным <script>, потому что await стоит внутри
функции, а не на верхнем уровне. Обе записи рабочие. Нажмите «Добавить» и
сравните число с данными события под примером.
<project-counter count="1"></project-counter>
<p id="counter-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;
// The component's own words: English in `en`, Russian in `ru`.
const TEXT = {
en: { count: 'Count', add: 'Add' },
ru: { count: 'Количество', add: 'Добавить' },
};
class ProjectCounter extends SfBaseElement {
static get props() {
// A label prop defaults to empty: the English word lives in the dictionary.
return { count: { type: Number, default: 0 }, addLabel: { default: '' } };
}
templateContext() {
// Keeps component, rootClass, rootStyle and every declared prop.
return this.createPropsTemplateContext({
countText: SF.pageText(TEXT, 'count', this),
addText: this.getProp('addLabel') || SF.pageText(TEXT, 'add', this),
});
}
increment() {
const count = this.getProp('count') + 1;
this.setAttribute('count', String(count));
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true, detail: { value: count } }));
}
template() {
const { count, countText, addText, rootClass } = this.templateContext();
return html`<div class="flex gap-2 items-cross-center ${rootClass}">
<output aria-live="polite">${countText}: ${count}</output>
<button type="button"
class="sf-button sf-button--outline sf-button--on-surface sf-button--size-1/2"
@click=${() => this.increment()}>
<span class="sf-button-text-container">${addText}</span>
</button>
</div>`;
}
}
ProjectCounter.define('project-counter');
document.querySelector('project-counter').addEventListener('change', event => {
document.querySelector('#counter-log').textContent = JSON.stringify({ value: event.detail.value });
});
}
start().catch(error => { document.querySelector('#counter-log').textContent = error.message; });
props объявляет число count и подпись addLabel, templateContext()
готовит значения и слова, template() возвращает разметку,
increment() изменяет значение и отправляет событие, define() регистрирует
тег.
Локальные компоненты проекта
Локальный компонент хранится в проекте и не входит в библиотеку. Он использует тот же базовый класс, но собственное имя и собственное подключение:
<script type="module" src="/assets/project/counter.js"></script>
<project-counter count="1"></project-counter>
Так удобно сделать карточку товара, панель конкретного сервиса или внутренний редактор, который не нужен другим проектам.
Правила:
- Loader загружает только теги, которые называют его правила, и не выводит
путь к файлу из имени тега: путь
/assets/project/counter.jsон изproject-counterне получит. Чтобы Loader сам загружал компоненты проекта по тегу, оформите их пакетом: правило пакета перечисляет теги (см. Пакеты компонентов). В правила самого Framework записи не добавляют. - Проектный скрипт подключается после Core. Скрипт с
awaitна верхнем уровне (как в Минимальный компонент) подключается сtype="module"; обычный<script>такой код не выполнит. - Локальному компоненту манифест не нужен: Loader и базовый класс манифест не читают. Манифест обязателен компоненту пакета (см. Манифест).
- Классы дочерних локальных компонентов регистрируются до использования.
- Библиотечные
sf-*элементы внутри локального компонента загружает Loader по пути поставки, как на любой странице (см. Подключение к странице). - Классовые компоненты в разметке локального компонента (
sf-button,sf-icon-buttonи другие) Loader находит так же, как в любой разметке страницы, в том числе нарисованной после загрузки, и подключает их стили: отдельно подключать CSS компонента не нужно. - Проектный скрипт загружается один раз; его версия фиксируется вместе с проектом.
Смотрите также
- Атрибуты, свойства и состояние — объявление свойств.
- Шаблоны — шаблоны.
- Манифест — манифест.
- Как работает Loader — Loader.