Шаблоны nu
Шаблон описывает, как блок или элемент превращается в HTML. Шаблоны пишутся на nu — внутреннем языке nuckty, в файлах .nu.ts. Синтаксис основан на вызове block()(...).
Почему отдельный синтаксис, а не «просто функции»? Отдельный синтаксис лаконичнее для декларативного описания блоков и задаёт систему режимов и специфичности. Подробнее — в решении о модели данных и двухпроходном рендеринге.
Базовый синтаксис
Файл .nu.ts экспортирует функцию по умолчанию. Загрузчик вызывает её в контексте текущей коллекции шаблонов; block вне этого контекста вызывает ошибку. Декларации ниже помещайте в templates() соответствующего блока: например, block('card') — в blocks/card/card.nu.ts. Один файл описывает один блок и его элементы.
import { block } from 'nuckty';
export default function templates(): void {
block('menu')({ tag: 'ul' });
block('menu').elem('item')({ tag: 'li' });
}
Условный предикат
.when(...) — произвольный предикат. Шаблон применяется, только если предикат истинен:
block<{ submenu?: import('nuckty').BEMTree }>('menu').elem('item').when((n) => Boolean(n.props?.submenu))({
content: (n, next) => [
{ block: 'menu', content: n.props?.submenu ?? null },
next(),
],
});
Режимы
Оба прохода задействуют режимы по-разному. Все режимы шаблонов — и структурные, и режимы отображения — применяются на первом этапе, в renderToBemJson; они превращают исходное дерево в разрешённое. Второй проход, renderToHtml, шаблоны не применяет вовсе: он сериализует готовое дерево и минимизирует разметку.
Структурные режимы
Формируют дерево: меняют узел и его содержимое.
| Режим | Действие |
|---|---|
content | Задаёт содержимое узла |
replace | Заменяет узел другим |
wrap | Оборачивает узел (результат содержит исходный узел) |
def | Полностью задаёт узел, когда остальных режимов недостаточно |
// replace: заменить блок на другой
block<{ color?: string }>('badge').replace((n) => ({
block: 'tag',
mods: { color: n.props?.color ?? 'default' },
}));
// wrap: обернуть
block('card').wrap((n) => ({
block: 'card-wrapper',
content: n,
}));
Режимы отображения в HTML
Задают, каким узел выйдет при сериализации: тег, атрибуты, классы.
| Режим | Действие |
|---|---|
tag | Задаёт тег HTML |
attrs | Задаёт атрибуты |
cls | Задаёт дополнительный класс (не БЭМ) |
mods | Задаёт модификаторы блока |
elemMods | Задаёт модификаторы элемента |
mix | Подмешивает классы другого блока |
js | Передаёт данные клиентскому JavaScript |
bem | Включает или отключает БЭМ-классы |
Если функция режима отображения или content возвращает undefined, поле удаляется из готового узла. Если режим не задан, исходное поле сохраняется; чтобы явно сохранить значение из цепочки деклараций, верните next().
next()
В режимах отображения и content вызов next() возвращает результат менее специфичной декларации, а в конце цепочки — исходное поле узла. Значение режима заменяет поле; для дополнения автор явно объединяет результат с next(), например через spread. Сам вызов ничего не объединяет:
// attrs: добавить к существующим атрибутам, не затирая их
block<{ trackId?: string }>('button')({
attrs: (n, next) => ({
...next(),
'data-track': n.props?.trackId ?? '',
}),
});
В управляющих режимах replace, wrap и def вызов next() возвращает весь узел с уже применёнными режимами отображения и content. Он не вызывает менее специфичный управляющий режим. Например, более специфичный wrap не запускает предыдущую обёртку.
Специфичность
Приоритет складывается из весов видов селекторов: именованный блок — 1, именованный элемент — 2, модификатор блока — 4, модификатор элемента — 8, предикат — 16. Повторение условия одного вида не добавляет ещё один такой вес:
block < elem < mod < elemMod < when
Поэтому шаблон с .when() — самый специфичный: произвольный предикат получает больший вес, чем совпадение по блоку, элементу или модификатору. block('*') не добавляет веса имени блока, а elem('*') — веса имени элемента; такой селектор применяется только к узлам с именем элемента. Без дополнительных условий такой селектор уступает именованному блоку, а block('*').when(...) может иметь больший приоритет. При равной специфичности движок выбирает шаблон, объявленный позже.
Это правило действует для деклараций в одной коллекции Templates. По уровням выбирается только последний файл .nu.ts блока: более ранние файлы не загружаются, и next() к ним не обращается.
Один файл — все режимы
Весь блок описывается в одном файле .nu.ts: структурные режимы и режимы отображения применяются в одном проходе над деревом, а сериализация не требует деклараций вовсе. Движок разбирает декларации сам; делить блок на части автору не нужно.
В отличие от bem.info, блок не приходится описывать в двух файлах (.bemtree.js и .bemhtml.js).
Как блок находит свои шаблоны
Файл .nu.ts подключается через механизм зависимостей (deps): файл bemdecl бандла перечисляет блоки, а движок загружает .nu.ts перечисленных блоков. Ручной регистрации не требуется. Подробнее — об устройстве блока.
Для отдельного рендера создайте коллекцию явно:
import { block, Templates, runWithTemplates, render } from 'nuckty';
const templates = new Templates();
runWithTemplates(templates, () => block('greeting')({ tag: 'p' }));
const html = render({ block: 'greeting', content: 'Hello' }, { templates });
Из корня пакета экспортируется render для HTML и готового BEMJSON; внутренние проходы renderToBemJson и renderToHtml отдельно из корня пакета не экспортируются. Режим output: 'bemjson' описан в модели данных, а рендеринг документа — в разделе HTML на выходе.