nuckty
EN
Все разделы →

Шаблоны 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 на выходе.