nuckty
RU
All sections →

nu templates

A template describes how a block or element turns into HTML. Templates are written in nu — nuckty's internal language, in .nu.ts files. The syntax is based on the block()(...) call.

Why a dedicated syntax instead of "just functions"? A dedicated syntax is more concise for the declarative description of blocks and establishes a system of modes and specificity. For details, see the data model decision and two-pass rendering.

Basic syntax

A .nu.ts file exports a default function. The loader invokes it inside the current template collection; block outside that context throws. Place the declarations below inside the corresponding block's templates() function: for example, block('card') belongs in blocks/card/card.nu.ts. One file describes one block and its elements.

import { block } from 'nuckty';

export default function templates(): void {
    block('menu')({ tag: 'ul' });
    block('menu').elem('item')({ tag: 'li' });
}

Conditional predicate

.when(...) is an arbitrary predicate. The template applies only when the predicate is true:

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(),
    ],
});

Modes

The two passes use modes differently. All template modes — structural and display modes alike — apply in the first pass, renderToBemJson; they turn the source tree into a resolved one. The second pass, renderToHtml, applies no templates at all: it serializes the finished tree and minimizes the markup.

Structural modes

These shape the tree: they change the node and its content.

ModeAction
contentSets the node's content
replaceReplaces the node with another one
wrapWraps the node (the result contains the original node)
defDefines the node entirely, when the other modes are not enough
// replace: swap the block for another
block<{ color?: string }>('badge').replace((n) => ({
    block: 'tag',
    mods: { color: n.props?.color ?? 'default' },
}));

// wrap: wrap it
block('card').wrap((n) => ({
    block: 'card-wrapper',
    content: n,
}));

HTML display modes

These define how the node comes out in serialization: tag, attributes, classes.

ModeAction
tagSets the HTML tag
attrsSets attributes
clsSets the extra (non-BEM) class
modsSets block modifiers
elemModsSets element modifiers
mixMixes in another block's classes
jsPasses data to client-side JavaScript
bemEnables or disables BEM classes

A display-mode or content callback that returns undefined removes the field from the resolved node. Omitting the mode leaves the source field unchanged; to keep the fallback value explicitly, return next().

next()

In display modes and content, next() returns the result of a less specific declaration, or the original node field at the end of the chain. A mode replaces the field; the author combines its result with next() explicitly, for example with spread. Calling it does not merge values:

// attrs: add to the existing attributes without overwriting them
block<{ trackId?: string }>('button')({
    attrs: (n, next) => ({
        ...next(),
        'data-track': n.props?.trackId ?? '',
    }),
});

In the control modes replace, wrap, and def, next() returns the whole node with display modes and content already applied. It does not call a less specific control mode. For example, a more specific wrap does not invoke the previous wrapper.

Specificity

Specificity adds weights by selector kind: named block 1, named element 2, block modifier 4, element modifier 8, predicate 16. Repeating a condition of the same kind does not add that weight again:

block  <  elem  <  mod  <  elemMod  <  when

That is why a template with .when() is the most specific: an arbitrary predicate gets more weight than a match by block, element, or modifier. block('*') adds no block weight, and elem('*') adds no element-name weight and matches only nodes with an element name. Its other conditions still contribute their weights: a bare wildcard yields to a bare named block, while block('*').when(...) can outrank it. At equal specificity, the engine picks the template declared later.

This rule applies to declarations in one Templates collection. Across levels, only the last matching .nu.ts file is selected: earlier files are not loaded, and next() cannot reach them.

One file — all modes

An entire block is described in a single .nu.ts file: structural and display modes apply in one pass over the tree, and serialization requires no declarations at all. The engine resolves the declarations itself; the author never needs to split a block into parts.

Unlike bem.info, a block does not have to be described in two files (.bemtree.js and .bemhtml.js).

How a block finds its templates

The .nu.ts file is wired in through the dependencies (deps) mechanism: the bundle's bemdecl file lists the blocks, and the engine loads the .nu.ts files of the listed blocks. No manual registration is required. For details, see block structure.

For isolated rendering, create a collection explicitly:

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 });

The package root exports render for both HTML and resolved BEMJSON; the internal renderToBemJson and renderToHtml passes are not separate package-root exports. See the data model for output: 'bemjson' and HTML output for document rendering.