Summary
*print evaluates an expression and writes the result into the element as plain text by setting textContent. The value is passed through Sercrod’s text filter and then rendered as a string, with null, undefined, and false treated as empty. The alias n-print behaves the same and shares the same implementation.
Related low level variants:
*textContentandn-textContentuse the same code path as*printandn-print.- All four directives end up assigning to
el.textContentafter applying thetextfilter.
Basic example
The simplest case, printing a single property from host data:
<serc-rod id="app" data='{"name":"Alice"}'>
<p *print="name"></p>
</serc-rod>
A simple greeting based on host data:
<serc-rod id="app" data='{"user":{"name":"Alice"}}'>
<p *print="`Hello, ${user.name}!`"></p>
</serc-rod>
At runtime:
- Sercrod evaluates
`Hello, ${user.name}!`in the current scope. - The result "Hello, Alice!" is given to the
textfilter. - The
<p>element’stextContentbecomesHello, Alice!. - Any child nodes inside the original
<p>template are ignored when*printis present.
Behavior
Core behavior of *print and n-print:
-
Sercrod reads the expression from the attribute (
*printorn-print). -
The expression string is optionally normalized by
normalizeTplif it is defined on the Sercrod class (otherwise it is used as is). -
The expression is evaluated with
eval_expr(expr, scope, { el: node, mode: "print" })ormode: "n-print". -
The result value
vis normalized:- If
visnull,undefined, orfalse, Sercrod treats it as an empty value. - For any other value, Sercrod uses it as is.
- If
-
The value is passed to
Sercrod._filters.text:- Default
textfilter simply returnsString(raw ?? "").
- Default
-
The final string is assigned to
el.textContent.
Important details:
- Because
textContentis used, any<or>characters in the value are treated as literal text by the browser. They do not become HTML markup. - When
cleanup.directivesis enabled in the global config, Sercrod removes*print,n-print,*textContent, andn-textContentfrom the output DOM after rendering. - On unexpected internal errors in this pass, the element’s
textContentfalls back to an empty string.
Value normalization and filters
The implementation normalizes values in two steps:
-
Expression result:
v = eval_expr(...)- If
visnull,undefined, orfalse, it is treated as empty and replaced with "". - All other values (including
0,true, and objects) are kept asraw.
-
Text filter:
-
The normalized
rawvalue is passed tothis.constructor._filters.text(raw, { el, expr, scope }). -
Default
textfilter is:text: (raw, ctx) => String(raw ?? "")
-
This means:
null,undefined, orfalsebecome "".0becomes "0".truebecomes "true".- Objects become "[object Object]" unless you override the
textfilter.
-
You can override Sercrod._filters.text (or provide window.__Sercrod_filter.text before Sercrod is loaded) to change how *print and *textContent values are normalized. The same text filter is also used by the fallback path that renders %expr% text expansions into plain text.
Evaluation timing
*print is a non structural directive. It is handled in the per element rendering stage after structural directives and before child nodes are rendered.
More precisely:
- Structural directives such as
*if,*elseif,*else,*for,*each,*switch, and*letare processed inrenderNode.- They may clone or skip the element before
_renderElementis called. - If a structural directive decides that the element should be skipped,
_renderElementis never reached and*printnever runs.
- They may clone or skip the element before
- Only when none of the structural branches take over, Sercrod calls
_renderElement(node, scope, parent).
Inside _renderElement:
- The
*print/n-print/*textContent/n-textContentblock runs before:%expr%text expansions on the element as a whole.- Recursing into child nodes.
As a result:
- When
*printorn-printis present, the element is rendered as a plain text node and its children are not processed. - Other text modes on that element, such as
%expr%expansion on the same element, are skipped because*printshort circuits the rendering of children.
Execution model
Conceptually, the runtime behaves as follows for *print and n-print:
-
Detect directive:
- If the element has
*printorn-printor*textContentorn-textContent, Sercrod enters the text assignment path.
- If the element has
-
Choose the active attribute:
-
Priority is:
*printn-print*textContentn-textContent
-
The first one that exists on the element provides the expression string.
-
-
Prepare the expression:
- Read the raw attribute value.
- If
normalizeTplexists on the Sercrod class, pass the expression string through it. - Otherwise use the original string.
-
Evaluate:
- Run
eval_exprwith the current effective scope and the element as context. - If
eval_exprfails internally, it logs a warning withmode:"print"or "textContent" and returnsfalse.
- Run
-
Normalize and filter:
- Map
null,undefined, andfalseto "". - Pass the result to the
textfilter with{ el, expr, scope }.
- Map
-
Assign:
- Set
el.textContentto the filtered string.
- Set
-
Cleanup and append:
- Optionally remove
*print,n-print,*textContent, andn-textContentfrom the element ifcleanup.directivesis enabled. - Append the element to its parent.
- Return from
_renderElementwithout rendering child nodes.
- Optionally remove
For *textContent and n-textContent, the same steps are taken, except that mode is "textContent" or "n-textContent" for logging purposes. Functionally they are equivalent to *print and n-print in the current implementation.
Variable creation
*print does not create any new variables.
-
It only reads from the current effective scope.
-
Any variables available in expressions come from:
- Host data (
data="..."ordata={...}on<serc-rod>). - Variables introduced by
*letorn-leton this element or ancestors. - Variables introduced by loops, such as
itemin*for="item of items"or*each="item of items". - Special helper variables injected by
eval_expr:$datafor the host data object.$rootfor the root Sercrod host’s data.$parentfor the nearest ancestor Sercrod host’s data.
- Functions and methods injected via
*methodsand internal helper methods.
- Host data (
The expression on *print is evaluated in exactly the same environment as other expression based directives.
Scope layering
*print uses the same scope that is effective at the point where the element is rendered.
-
Structural directives such as
*letand*formay replace or extend the scope before_renderElementis called. -
For a typical pattern:
<ul *for="item of items"> <li *print="item.label"></li> </ul>- The
*fordirective creates a per iteration scope withitembound. _renderElementis called on each<li>with that scope.*print="item.label"readsitemfrom the per iteration scope.
- The
*print itself does not change or wrap the scope; it only reads from it.
Parent access
Because *print uses eval_expr, it supports the usual Sercrod special variables for accessing parent data:
$datarefers to the data of the current Sercrod host.$rootrefers to the data of the root Sercrod host.$parentrefers to the data of the nearest ancestor Sercrod host.
Examples:
<serc-rod id="app" data='{"title":"Dashboard","user":{"name":"Alice"}}'>
<header>
<h1 *print="$data.title"></h1>
<p *print="`Signed in as ${$data.user.name}`"></p>
</header>
</serc-rod>
In nested hosts, you can use $parent or $root when the inner host wants to display some outer header information inside a *print expression.
Use with conditionals and loops
*print is often used inside elements that are controlled by *if, *for, or *each.
-
With
*ifon the same element:<p *if="user" *print="user.name"></p>- The
*ifchain is processed inrenderNode. - If the condition is falsy, the element is not rendered and
*printnever runs. - If the condition is truthy,
_renderElementis called on the element, and*printrenders the name.
- The
-
Inside
*for:<ul> <li *for="item of items"> <span *print="item.label"></span> </li> </ul>*forclones<li>for eachitem.*printrenders the label within each clone.
-
Inside
*each:<table> <tbody *each="row of rows"> <tr> <td *print="row.id"></td> <td *print="row.name"></td> </tr> </tbody> </table>*eachrepeats the<tbody>children for eachrow.*printdisplays fields fromrowin each cell.
There is no special interaction with loops beyond using the scope that loops provide.
Comparison with text interpolation (%expr%)
Sercrod supports %expr% style text interpolation inside plain text nodes using the configured delimiters. By default the delimiters are % and %, and interpolation is implemented by _expand_text.
Key differences between *print and %expr%:
-
Where the expression lives:
*printuses an attribute expression.%expr%syntax uses inline expressions inside text content.
-
Filter used:
*printand*textContentuse thetextfilter.%expr%expansions are combined with theplaceholderfilter.
-
How the element is rendered:
*printreplaces the entire content of the element and prevents child rendering.%expr%runs only when:- There is no
*print,n-print,*textContent, orn-textContenton the element. - The text node (or single text child) contains the delimiters.
- There is no
Example with %expr% only:
<serc-rod id="app" data='{"user":{"name":"Alice"}}'>
<p>Hello, %user.name%!</p>
</serc-rod>
Example with *print taking precedence:
<serc-rod id="app" data='{"user":{"name":"Alice"}}'>
<p *print="`Hello, ${user.name}!`">
This inner text, including %user.name%, is ignored.
</p>
</serc-rod>
In this second case:
*printruns, setstextContentfrom the expression, and skips child rendering.- The inline
%user.name%inside the children is never evaluated.
Related directives: *textContent and *innerHTML
-
*textContentandn-textContent:- Use the same pipeline as
*printandn-print. - They are logically equivalent in the current implementation, differing mainly in naming and the
modelabel passed toeval_expr. - All four are handled by the same code branch that assigns to
el.textContent.
- Use the same pipeline as
-
*innerHTMLandn-innerHTML:- Are separate directives that assign to
el.innerHTML. - They pass the raw value through the
htmlfilter instead oftext. - They do not use the
textfilter and are intended for inserting HTML strings. - When you want to insert HTML markup rather than text, use
*innerHTMLand provide a suitablehtmlfilter.
- Are separate directives that assign to
Combination rules and caveats:
-
On a single element, you should not rely on combining
*printor*textContentwith*innerHTMLorn-innerHTML.- If both
*printand*textContentare present, only the first one in the internal priority order is used. - If
*printor*textContentis present, the*innerHTMLblock on the same element is skipped, because*printshort circuits child rendering. - Although the attributes can be written in HTML, only one text or HTML mode will effectively control the element.
- If both
To keep behavior predictable, treat *print, *textContent, *innerHTML, and n-innerHTML as mutually exclusive on a single element and choose one per element.
Best practices
-
Use
*printfor text only content:- When an element is meant to show only an expression result, prefer
*printinstead of mixing static text and%expr%. - Example: badges, counters, titles that are entirely dynamic.
- When an element is meant to show only an expression result, prefer
-
Use
%expr%for small inline substitutions:- When the text is mostly static and you want a few interpolated pieces, prefer
%expr%inside a text node. - Example: Hello, %user.name% (id: %user.id%).
- When the text is mostly static and you want a few interpolated pieces, prefer
-
Do not rely on children with
*print:- When
*printorn-printis present, children are not rendered. - Avoid putting important markup or directives inside the element body if you also use
*print.
- When
-
Do not mix
*printand*innerHTMLon the same element:- Only one set of semantics will effectively apply.
- If you need both plain text and HTML portions, split them into separate child elements.
-
Remember the
textfilter:- If you need custom normalization (for example trimming whitespace or mapping specific values), override
Sercrod._filters.text. - Keep in mind that this affects all uses of
*printand*textContentas well as the fallback%expr%text expansion path that uses the same basic stringification rules.
- If you need custom normalization (for example trimming whitespace or mapping specific values), override
-
Prefer simple expressions:
- For maintainability, keep
*printexpressions short. - For complex formatting, move the logic into a helper function and call that function from
*print.
- For maintainability, keep
Additional examples
Display a number with a suffix:
<serc-rod id="counter" data='{"count": 42}'>
<p *print="`${count} items`"></p>
</serc-rod>
Fallback for missing values:
<serc-rod id="app" data='{"user":{}}'>
<p *print="user.name || 'Anonymous'"></p>
</serc-rod>
Using methods:
Assume window.formatUser is a function that returns a display name.
function formatUser(user) {
return user && user.name ? `User: ${user.name}` : "Guest";
}
<serc-rod id="app" data='{"user":{"name":"Alice"}}' *methods="formatUser">
<p *print="formatUser(user)"></p>
</serc-rod>
The *methods="formatUser" directive makes formatUser available in the expression scope for *print and other directives.
Notes
*printandn-printshare the same manual entry in*man.*printis implemented as a non structural directive inside_renderElementand does not affect layout or siblings beyond replacing the element’s text content.*textContentandn-textContentare currently implemented with the same behavior as*printandn-print, using the same value normalization andtextfilter.- Behavior described here is based on the current
sercrod.jsimplementation and may evolve if the text or HTML filters are customized through the official extension points.