К содержанию

Навигация

Шаблоны

Глава описывает, как компонент строит разметку: встроенный шаблон, внешний шаблон проекта и контракт внешнего модуля. Внешний шаблон позволяет проекту изменить вид компонента, не меняя его поведения.

Шаблон и templateContext

Шаблон — функция, которая получает данные и возвращает разметку Lit. Поведение остаётся в классе; шаблон определяет, что увидит человек.

Данные для шаблона готовит метод templateContext(). Шаблон читает уже разрешённые значения: если компонент вычисляет значение в геттере (с учётом имён для совместимости, языка страницы и значений по умолчанию), templateContext() передаёт именно его, а не сырой атрибут.

Пример нарушения: если шаблон читает сырой атрибут type, а не разрешённое значение kind, тег с kind="checkbox" рисует неактивный квадрат.

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

Что лежит в контексте шаблона. Контекст, который получает встроенный и внешний шаблон, состоит из:

Ключ Что это
Ключи, которые возвращает templateContext() компонента Разрешённые значения. Ключ с именем геттера компонента равен значению этого геттера: context.size — то же, что component.size
Объявленные свойства (static props) Значения, прочитанные через getProp (см. Как компонент читает свойство)
component Сам элемент
rootClass Дополнительные классы из атрибута root-class тега (по умолчанию пустая строка). Это не вычисленные классы компонента: шаблон строит свои классы сам и добавляет к ним rootClass
rootStyle Стиль из атрибутов style и root-style тега. Сам тег не образует блока, поэтому шаблон ставит этот стиль на свой корень
custom Значения, которые код передал компоненту методами вроде setAttributes() под ключами, которых компонент не объявил; они же лежат в контексте под своими именами

Полный набор ключей конкретного компонента — его templateContext() и страница компонента в справочнике.

Свой templateContext() заменяет контекст по умолчанию. Базовый templateContext() возвращает this.createPropsTemplateContext(): объявленные свойства, component, rootClass, rootStyle и custom. Если компонент переопределяет метод и возвращает собственный объект, шаблон получает только его ключи — и встроенный, и внешний шаблон этого компонента. Чтобы сохранить стандартные ключи и добавить свои, верните this.createPropsTemplateContext({ ...свои ключи }): свои ключи добавляются последними и заменяют свойство с тем же именем. Объект только со свойствами даёт this.getPropsContext() (см. Как компонент читает свойство).

Контекст кнопки. Шаблон <sf-button> получает такие ключи:

Ключ Тип Что это
component элемент Сам <sf-button>; через него шаблон читает слоты: component.hasSlotContent('text'), component.getSlotContent('text'). Слоты text, icon-start (прежнее имя icon-left) и icon-end (прежнее icon-right) важнее атрибутов: содержимое слота icon-start встроенный шаблон ставит вместо иконки по имени, так же и для icon-end
rootClass строка Классы из атрибута root-class; шаблон добавляет их к своим
rootStyle строка Стиль из атрибутов style и root-style; шаблон ставит его на корень
templateName строка Имя шаблона из атрибута template, по умолчанию default
size строка Ступень размера из атрибута size, по умолчанию 1
appearance, type строка Одно и то же значение вида: appearance или прежний атрибут type, по умолчанию default
scheme строка Цветовая схема, по умолчанию primary
status строка error или пустая строка; заданный статус сильнее схемы: цвет корня — sf-button--<status или scheme>
textPosition строка Выравнивание подписи, по умолчанию center; класс text-<значение> на контейнере подписи sf-button-text-container
text строка Подпись из атрибута text; содержимое слота text важнее её
segment строка Положение в группе кнопок или пустая строка
icon строка Имя иконки; шаблон ставит её по iconPosition на ту сторону, где не задана своя
iconStart, iconEnd строка или null Сырые атрибуты icon-start (или icon-left) и icon-end (или icon-right), а не значение, выведенное из icon: null — атрибута нет, сторона берёт icon; пустая строка — атрибут задан пустым, иконки на этой стороне нет
iconLeft, iconRight строка Прежние имена тех же сторон; без атрибута — пустая строка
iconPosition строка start (по умолчанию) или end; шаблон принимает и left, right
spacing строка Плотность: compact, normal, comfortable или spacious, из атрибута spacing или прежнего tightness. Шаблон ставит класс spacing-<значение>, для normal класса нет. Отсутствующий атрибут и явное normal дают одно значение; различить их можно через component.hasAttribute('spacing')
tightness строка Та же плотность прежним именем: low, пустая строка, high, highest; класс по нему шаблон не ставит, берите spacing
radius строка default, square, rounded или пустая строка; класс radius-<значение>
loading булево Идёт загрузка
disabled булево Кнопка заблокирована
nativeType строка Значение type у <button>, по умолчанию button
ariaLabel строка Доступное имя из атрибута aria-label или пустая строка
diagnostics массив строк Замечания о плотности: invalid-spacing, invalid-tightness, spacing-overrides-tightness
custom объект Значения под необъявленными ключами

Атрибуты disabled, aria-busy, type и aria-label корня шаблон рисует сам: при loading кнопка получает disabled и aria-busy="true". После отрисовки кнопка переносит на элемент .sf-button только атрибуты тега, которые не являются её настройками, в том числе data-*. Внешний шаблон кнопки сохраняет корень с классом sf-button; поведение (обработчики на теге) остаётся у компонента.

Разметка встроенного шаблона. Встроенный шаблон для <sf-button text="Save" icon="download" loading> рисует такую разметку; с неё удобно начинать внешний шаблон:

HTML
<button type="button" class="sf-button sf-button--size-1 sf-button--default sf-button--primary flex items-cross-center loading sf-button-state-loading" disabled aria-busy="true">
  <sf-icon icon="download" root-class="pointer-event-none" weight="400" aria-hidden="true"></sf-icon>
  <span class="sf-button-text-container text-center pointer-event-none">Save</span>
</button>

Без загрузки классов loading sf-button-state-loading и атрибутов disabled, aria-busy нет. Полосы загрузки рисует любой из двух классов, loading или sf-button-state-loading. Кнопка без иконок добавляет контейнеру подписи класс flex-1. Классы segment-…, spacing-…, radius-… и rootClass добавляются к корню, когда заданы.

Встроенный и внешний шаблон

У каждого компонента есть встроенный шаблон default. Атрибут template не ограничен этим значением: любое другое имя загружает внешний шаблон с этим именем. Внешний модуль default/index.js встроенный шаблон не подменяет. Именованный внешний шаблон загружается отдельным модулем:

HTML
<script>window.SF_SMART_TEMPLATE_PATH = '/assets/project/smart-templates';</script>
<sf-button template="compact"></sf-button>

Запрашивается /assets/project/smart-templates/button/compact/index.js — имя берётся из тега без префикса sf-. Для локального project-greeting имя сохраняется полностью.

Папка шаблонов. Компонент ищет папку в таком порядке и берёт первое заданное значение:

  1. window.SF_SMART_TEMPLATE_PATH;
  2. window.SFSmartTemplatePath;
  3. статическое поле класса static externalTemplateBasePath; у базового класса это /local/smart/templates.

Задавайте папку абсолютным адресом, например new URL('./assets', document.baseURI).href: проверка файла и загрузка модуля должны обращаться к одному адресу, а простой относительный путь модуль может вычислить от файла библиотеки.

Глобальная переменная действует на все Smart-компоненты страницы, в том числе на компоненты проекта: если она задана, статическое поле класса не используется. Переменную читают при каждой отрисовке, поэтому её достаточно задать до отрисовки элемента с внешним шаблоном.

Стили шаблона. Рядом с модулем лежит index.css. Компонент сам подключает его тегом <link data-sf-smart-css> в <head> и ждёт загрузки стилей до загрузки модуля; файл подключается один раз на страницу. Такой файл стоит вне каскадных слоёв, поэтому сильнее любого слоя Framework (см. Каскадные слои). Чтобы стили шаблона подчинялись порядку слоёв проекта, оберните их в @layer project { … }. Если index.css нет, шаблон работает без него, но браузер один раз сообщает в консоли об ошибке 404 для этого файла. Чтобы консоль оставалась чистой, положите рядом с модулем пустой index.css.

Пример внешнего шаблона

Пример подключает внешний шаблон compact двум компонентам: локальному project-greeting и библиотечной кнопке <sf-button>. Оба шаблона — настоящие JavaScript-модули с index.css в папке ./assets примера; папку задаёт window.SF_SMART_TEMPLATE_PATH до отрисовки. Вторая кнопка рисуется шаблоном compact: те же классы корня, подпись без внутреннего отступа.

HTML
<script>window.SF_SMART_TEMPLATE_PATH = new URL('./assets', document.baseURI).href;</script>
<project-greeting name="Рим" template="compact"></project-greeting>
<div class="flex gap-2 items-cross-center">
  <sf-button text="Сохранить" appearance="outline" scheme="on-surface"></sf-button>
  <sf-button text="Сохранить" appearance="outline" scheme="on-surface" template="compact"></sf-button>
  <sf-button text="Скачать" appearance="outline" scheme="on-surface" template="compact"><i slot="icon-start" class="sf-icon" aria-hidden="true">download</i></sf-button>
</div>
<p id="template-log" role="status">Загружаются шаблоны compact…</p>
JavaScript
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 ProjectGreeting extends SfBaseElement {
    static externalTemplateBasePath = new URL('./assets', document.baseURI).href;
    static get props() { return { name: { default: 'Гость' }, templateName: { attribute: 'template', default: 'default' } }; }
    // createPropsTemplateContext keeps component, rootClass, rootStyle and the props.
    templateContext() { return this.createPropsTemplateContext(); }
    template() { return html`<p>Привет, ${this.getProp('name')}</p>`; }
  }
  ProjectGreeting.define('project-greeting');
  const element = document.querySelector('project-greeting');
  await element.whenRendered();
  await SF.whenDefined('button');
  const button = document.querySelector('sf-button[template="compact"]');
  await button.whenRendered();
  const greetingOk = Boolean(element.querySelector('strong'));
  const buttonOk = Boolean(button.querySelector('.project-button-compact-text'));
  document.querySelector('#template-log').textContent = greetingOk && buttonOk
    ? 'Внешние шаблоны compact подключены: приветствие и кнопка.'
    : 'Показан резервный шаблон: проверьте путь к compact.';
}
start().catch(error => { document.querySelector('#template-log').textContent = error.message; });
JavaScript
export default function ({ html, nothing, context }) {
  return html`<p class="project-greeting-compact p-2 bg-primary-container radius-2" style=${context.rootStyle || nothing}>Здравствуйте, <strong>${context.name}</strong>!</p>`;
}
JavaScript
// External template "compact" for the library <sf-button>: the same root
// classes and attributes as the built-in template, the text without its inner
// padding. Behaviour (handlers on the tag, attribute forwarding) stays with
// the component.
export default function ({ html, nothing, context, component }) {
  const {
    size, appearance, status, scheme, text, icon, iconStart, iconEnd, iconPosition,
    segment, spacing, radius, textPosition,
    loading, disabled, nativeType, ariaLabel, rootClass, rootStyle,
  } = context;
  // iconStart and iconEnd are the raw attributes: null when absent, '' when
  // emptied on purpose. Only an absent side takes `icon`, by iconPosition.
  const atEnd = ['end', 'right'].includes(iconPosition);
  const start = iconStart ?? (icon && !atEnd ? icon : '');
  const end = iconEnd ?? (icon && atEnd ? icon : '');
  const label = component.hasSlotContent('text') ? component.getSlotContent('text') : text;
  // A slotted icon wins over the icon name, as in the built-in template;
  // icon-left and icon-right are the old slot names.
  const slotted = (...names) => {
    const name = names.find((slot) => component.hasSlotContent(slot));
    return name ? component.getSlotContent(name) : null;
  };
  const startContent = slotted('icon-start', 'icon-left');
  const endContent = slotted('icon-end', 'icon-right');
  const classes = [
    'sf-button', `sf-button--size-${size}`, `sf-button--${appearance}`, `sf-button--${status || scheme}`,
    'flex', 'items-cross-center',
    segment ? `segment-${segment}` : '',
    // An external template writes the density as spacing-*; normal has no class.
    spacing && spacing !== 'normal' ? `spacing-${spacing}` : '',
    radius ? `radius-${radius}` : '',
    loading ? 'loading sf-button-state-loading' : '',
    rootClass,
  ].filter(Boolean).join(' ');
  const textClasses = [
    'sf-button-text-container', `text-${textPosition || 'center'}`, 'pointer-event-none',
    start || end || startContent || endContent ? '' : 'flex-1', 'project-button-compact-text',
  ].filter(Boolean).join(' ');
  const iconOf = (name) => (name ? html`<sf-icon icon=${name} root-class="pointer-event-none" weight="400" aria-hidden="true"></sf-icon>` : nothing);
  // disabled, aria-busy, type and aria-label are the template's to render:
  // the component forwards only attributes that are not its settings.
  return html`<button type=${nativeType} class=${classes} style=${rootStyle || nothing}
      ?disabled=${disabled || loading} aria-label=${ariaLabel || nothing} aria-busy=${loading ? 'true' : nothing}>
    ${startContent ?? iconOf(start)}
    ${label ? html`<span class=${textClasses}>${label}</span>` : nothing}
    ${endContent ?? iconOf(end)}
  </button>`;
}
CSS
/* The template's own class. This file sits outside the cascade layers, so it
   wins over the Framework's button styles. */
.project-button-compact-text { padding-inline: 0; }

Для кнопки имя папки — имя тега без sf-: <папка шаблонов>/button/compact/index.js. Для локального компонента имя папки — полное имя тега: <папка шаблонов>/project-greeting/compact/index.js. Класс задаёт папку полем static externalTemplateBasePath, а свойство templateName связывает с атрибутом template; глобальная переменная сильнее этого поля.

Контракт внешнего модуля

JavaScript
export default function ({ html, nothing, context, component }) {
  return html`<p aria-label=${context.label || nothing}>Здравствуйте, ${context.name}!</p>`;
}
export function destroy({ component }) { /* снять подписки и таймеры */ }

Модуль экспортирует функцию шаблона по умолчанию и, если нужно, хуки:

Экспорт Когда вызывается Что получает
default При каждой отрисовке; возвращает разметку html, nothing, context, component, changedAttributes
beforeRender Перед каждой отрисовкой, до default component, root, context, changedAttributes, html, nothing
mapContext Перед default; возвращает контекст, который целиком заменяет исходный: верните {...context, ключ: значение}, а не только изменённые ключи. Пустой ответ оставляет контекст прежним context, component, html, nothing
afterRender После отрисовки и загрузки стилей component, root, context, changedAttributes, html, nothing
destroy При смене шаблона и при удалении компонента component, root, context, html, nothing

Внешний шаблон повторяет разметку встроенного шаблона того выпуска, на котором закреплён проект, и сам за новыми выпусками не следует. При каждом переходе на новый выпуск сравните его с таблицей контекста и разметкой встроенного шаблона на странице компонента и проверьте записи журнала изменений об этом компоненте.

Безопасность и поведение при ошибке

  • Интерполяция через html экранирует текст. Пользовательскую строку нельзя превращать в HTML.
  • Шаблон — доверенный код проекта. Путь к нему нельзя брать из ввода человека.
  • Если именованный модуль не найден или не экспортирует функцию, компонент рисует встроенный шаблон. Проверяйте фактический вид, а не только отсутствие ошибок.
  • Загрузка внешнего модуля асинхронна. Первая отрисовка ждёт модуль и его index.css, поэтому whenRendered() выполняется уже после отрисовки проектным шаблоном. Если за время загрузки компонент удалён или выбран другой шаблон, устаревший результат не отображается.
  • Кеш модулей живёт в пределах страницы. При обновлении шаблонов меняйте версию пути и перезагружайте страницу. Этот кеш не связан с HTTP-кешем сервера.

Смотрите также

Поиск по документации

Введите минимум 2 символа

    Настройки

    Тема
    Размер шрифта
    Ширина страницы