*compose
Summary
*compose composes the contents of a single element from the result of an expression and writes it into the element as HTML via the html filter. It is the structural counterpart of *innerHTML and has an alias n-compose.
Key ideas:
- The expression is evaluated once per render in the normal Sercrod expression sandbox.
- The result is passed through
Sercrod._filters.html(raw, ctx)and then assigned toinnerHTMLof the rendered element. *composedoes not create new local variables; it only consumes the current scope.- At runtime it shares the same pipeline as
*innerHTML, but projects are expected to give*composea higher-level meaning (for example, composing from templates, partials, or hosts) by customizing thehtmlfilter.
Alias:
*composeandn-composeare aliases; they accept the same syntax and behave identically.
Basic example
A basic composition from a precomputed HTML string:
<serc-rod id="app" data='{
"cardHtml": "<div class="card"><h2>Title</h2><p>Body</p></div>"
}'>
<section class="wrapper">
<div class="slot" *compose="cardHtml"></div>
</section>
</serc-rod>
Behavior:
- Sercrod evaluates
cardHtmlin the current scope. - The result is passed through
Sercrod._filters.htmland becomes theinnerHTMLof the<div class="slot">. - The
<section>and<div>themselves are created as normal elements; only the<div>’s content is supplied by*compose.
Behavior
At a high level, *compose:
- Targets the children of the host element, not the element itself.
- Evaluates its expression once per render of the host.
- Treats
nullandfalseas “empty content”. - Uses the
htmlfilter to transform the raw result into the final HTML string.
Details:
-
Host element:
- The host element is cloned and appended to the parent as usual.
- All other bindings on the host (
:class,:style, attribute bindings,n-class,n-style, etc.) still apply.
-
Content:
-
The expression on
*composeis evaluated in the current scope. -
The value is normalized:
- If
visnullorfalse, it is treated as"". - Otherwise the value is passed as-is to the
htmlfilter.
- If
-
The final
innerHTMLof the rendered element is:el.innerHTML = Sercrod._filters.html(raw, { el, expr, scope }).
-
-
Default filter:
- By default,
Sercrod._filters.htmlreturnsrawas-is. - That means, without customization,
*composebehaves like “evaluate an expression and insert the result as raw HTML”.
- By default,
-
Interaction with children:
*composeruns before Sercrod recursively renders the template’s child nodes.- After
innerHTMLis set, Sercrod still walksnode.childNodes(the template children) and renders them into the same element. - In common usage, you normally give the host no children when using
*compose, or treat those children as optional “extra” content.
Evaluation timing
Within _renderElement, *compose takes part in the following order:
-
The host must first survive structural directives:
*if/n-ifon the same element are evaluated earlier; if they fail, the element is not rendered and*composeis not reached.- Structural loops like
*for/n-forand*each/n-eachare processed earlier and return after they finish, so*composenever runs on elements where those loops took effect.
-
Scalar text directives have priority:
*print/n-printand*textContent/n-textContentare evaluated before*compose.- When any of those directives run, they set
textContent, append the element, and return;*composeis skipped entirely.
-
Static text with
%...%expansion also runs before*compose:- If there is exactly one text child and it contains
%markers, Sercrod uses_expand_text, appends the result, and returns.
- If there is exactly one text child and it contains
-
Only when the element has not been handled by the above cases does Sercrod check for
*compose/n-composeor*innerHTML/n-innerHTML.
After *compose:
-
Sercrod continues with:
*class/n-class,*style/n-style.- Other attribute bindings.
- Recursive rendering of
node.childNodes. - Any
postApplywork for things like<select>.
Implications:
*composeis non-structural; it does not short-circuit rendering.- It is best thought of as a “content injection” step in the later part of the element pipeline.
Execution model
Conceptually, the engine behaves as follows when it hits *compose:
-
Decide which attribute to read:
- If the element has any of
*compose,n-compose,*innerHTML, orn-innerHTML, one of those names is chosen assrcAttr. - Only one attribute is used; others are ignored for this step.
- If the element has any of
-
Read the expression:
expr = node.getAttribute(srcAttr).
-
Evaluate the expression:
v = eval_expr(expr, scope, { el: node, mode: "compose" or "innerHTML" }).- Any variables in the expression are resolved from the merged scope and special helpers.
-
Normalize the result:
- If
visnullorfalse, treat it as"". - Otherwise, use it as
raw.
- If
-
Call the
htmlfilter:ctx = { el, expr, scope }.htmlString = Sercrod._filters.html(raw, ctx).
-
Write into
innerHTML:el.innerHTML = htmlString.
-
On error:
- If evaluation or filtering throws, Sercrod logs a
[Sercrod warn] html filter:message (whenerror.warnis enabled). el.innerHTMLis set to an empty string.
- If evaluation or filtering throws, Sercrod logs a
After that, the standard rendering pipeline continues, including child rendering and post-apply actions.
Integration with the html filter
*compose is tightly coupled with the html filter:
-
Default behavior:
-
The built-in
htmlfilter is:html: (raw, ctx) => raw
-
So by default,
*compose="expr"is equivalent to:el.innerHTML = exprResult.
-
-
Custom behavior:
-
Before Sercrod starts, you can define
window.__Sercrod_filter.htmlto override thehtmlfilter:Sercrod._filtersis initialized by merging the built-in filters withwindow.__Sercrod_filteronce at startup.- This is the intended hook for project-specific HTML composition.
-
The
ctxobject gives the filter access to:ctx.el- the rendered element.ctx.expr- the raw expression string.ctx.scope- the evaluation scope at the time of the call.
-
A project can:
- Treat specific values as template or partial names.
- Resolve host references and build HTML from other Sercrod hosts.
- Apply sanitization or escaping before insertion.
-
Security considerations
*compose ultimately writes to innerHTML and does not perform any built-in escaping. This is a standard XSS risk surface: if the value you compose contains active HTML (such as script tags or event handlers) and that content comes from untrusted input, it may execute in the browser.
This manual’s role is to clearly document that behavior, not to claim that a particular pattern is always safe. Even when you apply sanitization in the html filter, no single step can guarantee that all XSS vectors are eliminated. In practice, XSS protection is a multi-layer task that includes server-side validation, output encoding, and careful template design.
Within that broader context, Sercrod expects at least the following minimum precautions when using *compose:
- Do not pass raw user input directly into
*compose. - Prefer to keep
*composevalues under your control (for example, prebuilt fragments, trusted templates, or server-side generated HTML that has already been validated). - If you need to deal with untrusted data, use the
htmlfilter to sanitize or strip active content, and consider handling such data with text-based directives like*printor*textContentinstead of composing HTML around it. - Treat
*composeas a convenience for trusted or prepared HTML, not as a generic “render any user string as HTML” mechanism.
These notes should be read as a “speed limit sign”: they describe how the engine behaves and where the risks lie, and they indicate a minimum level of care. They do not replace application-level security measures, especially on the server side.
Variable creation and scope layering
*compose does not create any new local variables.
Scope behavior:
-
The expression is evaluated in the standard Sercrod expression scope:
-
All data from the current
<serc-rod>host (this._data) are visible. -
The merged scope includes:
- The local scope for the current element.
$data- current host data (if available).$root- root host data (if available).$parent- nearest ancestor host’s data (if available).- Internal methods and any methods registered via
*methods.
-
-
The directive does not inject extra names or loop variables.
-
Any variable names used in the expression follow the normal shadowing rules of Sercrod expressions.
Parent access
Because *compose is purely expression-driven and does not alter scope:
-
You can refer to outer data with:
someProp,state.items,config.layout(whatever your data shape is).$datafor the current host’s data object.$rootto reach the root host data.$parentfor the nearest ancestor host’s data.
-
There is no special “compose parent” object; everything uses the standard scope resolution.
Use with conditionals and loops
*compose often appears together with conditionals or in contexts controlled by loops, but it is not itself a loop or condition.
-
With host-level conditionals:
-
You can guard the entire composed block with
*if:<section *if="showDetails"> <div *compose="detailsHtml"></div> </section> -
*ifis evaluated on<section>before its children are rendered; if it fails, the<div>and its*composeare never processed.
-
-
Inside loops:
-
Common pattern: use
*foror*eachto repeat parent elements, then*composeinside the loop body.<ul> <li *for="item of items"> <div class="card" *compose="item.html"></div> </li> </ul> -
In this case,
*foris structural (repeats<li>), and*composejust fills each card with iteration-specific HTML.
-
-
Same-element combinations with loops:
- If you combine
*compose(orn-compose) with*foror*eachon the same element, the structural directive runs first and returns, so*composeis effectively ignored. - While the engine does not throw, you should treat such combinations as invalid; always separate structural loops and composition onto different elements.
- If you combine
Use with *include and *import
*include / *import and *compose all affect the children of a host, but at different stages:
-
*include/*import:- Resolve a template or fetch HTML.
- Assign it to
node.innerHTML(the template node). - Remove
*include/*importfrom the rendered element. - Do not return; the engine later renders the resulting children normally.
-
*compose:- Runs later, on the rendered element.
- Evaluates an expression, passes it through
Sercrod._filters.html, and setsel.innerHTML.
As a result:
-
If you attach both
*include/*importand*composeto the same element:*include/*importchange the template’s innerHTML.*composethen sets the rendered element’sinnerHTMLfrom the expression result.- After that, Sercrod still walks the (possibly replaced)
node.childNodesand appends their rendered versions under the same element.
-
This effectively means:
- The HTML created by
*composebecomes the initial content. - The included/imported children are then rendered additionally, using the modified template.
- The HTML created by
Guidance:
-
The engine allows this combination, but the outcome can be subtle and hard to reason about.
-
Recommended patterns:
-
Decide clearly which directive “owns” the content of a given element.
-
If you want an included template plus extra composition, consider wrapping:
<div *include="'card-template'"> <div *compose="extraHtml"></div> </div> -
Or use
*composealone and let yourhtmlfilter handle template resolution internally.
-
-
For most projects, it is simpler to treat
*include/*importand*composeas mutually exclusive on a single element, even though the runtime does not enforce this with an error.
Best practices
-
Do not mix multiple “content” directives on one element:
- Avoid putting any combination of
*print,*textContent,*literal,*rem,*compose, and*innerHTMLon the same element. - In the current implementation, whichever branch matches first wins and the others are ignored, which can be confusing to debug.
- Avoid putting any combination of
-
Prefer
*composeover*innerHTMLfor higher-level composition:- Keep
*innerHTMLas a low-level escape hatch for raw HTML strings. - Give
*composea meaningful semantic in your project by customizingSercrod._filters.html.
- Keep
-
Keep expressions simple:
- Use
*compose="cardHtml"rather than embedding large concatenated strings. - Precompute complex HTML or descriptors in data or methods.
- Use
-
Be careful with untrusted data:
- Remember that
*composewrites directly toinnerHTMLand does not escape by itself. - Untrusted values should either be processed by a defensive
htmlfilter or handled via text-based directives instead of being composed as HTML. - This is a minimum precaution; it does not replace server-side validation or other security layers.
- Remember that
-
Use children intentionally:
- If you rely purely on the composed HTML, do not define template children on the same element.
- If you intentionally want “compose + extra children”, document that pattern within your team so future maintainers are aware.
Examples
Composition from a pre-rendered fragment:
<serc-rod id="app" data='{
"fragments": {
"hero": "<h1>Welcome</h1><p>This is Sercrod.</p>"
}
}'>
<header *compose="fragments.hero"></header>
</serc-rod>
Using a method to produce HTML:
<serc-rod id="app" data='{"items":[{"name":"Alpha"},{"name":"Beta"}]}'>
<script type="application/json" *methods='{
"renderList": function(items){
return "<ul>" + items.map(function(it){
return "<li>" + it.name + "</li>";
}).join("") + "</ul>";
}
}'></script>
<section *compose="renderList(items)"></section>
</serc-rod>
Custom html filter for template names (conceptual pattern):
<script>
// Before Sercrod loads
window.__Sercrod_filter = {
html: function(raw, ctx){
// Example: treat values starting with "tpl:" as template names
if(typeof raw === "string" && raw.startsWith("tpl:")){
var name = raw.slice(4);
// Resolve name to preloaded HTML (implementation-specific)
var html = window.TEMPLATES && window.TEMPLATES[name];
return html || "";
}
return raw;
}
};
</script>
<serc-rod id="app" data='{"currentTpl":"tpl:card"}'>
<div class="card-container" *compose="currentTpl"></div>
</serc-rod>
In this pattern:
- The Sercrod core still only knows that
*composecallshtml(raw, ctx)and writes toinnerHTML. - All higher-level composition logic lives in the
htmlfilter.
Notes
*compose/n-composeshare their implementation with*innerHTML/n-innerHTML, differing only in which attribute name is used.- Null and
falseresults are treated as empty strings; other values are forwarded to thehtmlfilter. - The default
htmlfilter returns the raw value; projects are expected to override it (viawindow.__Sercrod_filter.html) if they need richer composition. - Combining structural directives (
*for,*each) with*composeon the same element causes the structural directive to win and*composeto be ignored; keep them on separate elements. - Combining
*composewith*include/*importis technically possible but advanced; prefer to pick a single directive as the content owner for predictable behavior. - When in doubt, treat
*composeas “evaluate once, pass throughhtmlfilter, assign toinnerHTML” and keep the rest of the element’s logic straightforward.