*innerHTML
Summary
*innerHTML sets the DOM innerHTML property of an element from a Sercrod expression. It is the low-level directive for inserting HTML markup as a string. The alias n-innerHTML behaves identically.
Use *innerHTML when you already have an HTML string and you want to insert it as markup, not as plain text. Unlike *print or *textContent, *innerHTML does not escape the value; it passes it through the global html filter and then assigns it directly to el.innerHTML.
Basic example
A simple example that renders an HTML fragment stored in data:
<serc-rod id="app" data='{
"contentHtml": "<strong>Hello</strong> <em>world</em>!"
}'>
<p *innerHTML="contentHtml"></p>
</serc-rod>
Behavior:
- The
contentHtmlstring is evaluated from the current scope. - Sercrod calls the global
htmlfilter with that value. - The result is assigned to the
<p>element’sinnerHTML. - The
<p>ends up containing<strong>Hello</strong> <em>world</em>!as real HTML, not as text.
Behavior
Core behavior:
*innerHTMLis a structural output directive that controls the inner markup of the host element.- For each render of the host:
- Sercrod evaluates the expression on
*innerHTMLin the current scope. - If the result is
nullorfalse, it is treated as an empty string. - Otherwise the raw value is passed to the global
htmlfilter:html(raw, { el, expr, scope }).
- The filter result is assigned to
el.innerHTML.
- Sercrod evaluates the expression on
Aliases:
*innerHTMLandn-innerHTMLare aliases.- They share the same evaluation pipeline; the only difference is the attribute name.
Relationship to *print and *textContent:
*printand*textContentsettextContentthrough thetextfilter.*innerHTMLsetsinnerHTMLthrough thehtmlfilter.*printand*textContentescape or normalize text;*innerHTMLdoes not.- If an element has both
*print/*textContentand*innerHTML, the*print/*textContentbranch runs first and returns, so*innerHTMLon that element is effectively ignored.- In practice you should not combine them on the same element.
Evaluation timing
Within the rendering pipeline of a single element:
- Host-level
*ifand conditional chains (*if,*elseif,*else) are resolved first.- If the host
*ifcondition evaluates to false, the element (and its directives) are skipped entirely;*innerHTMLis not evaluated.
- If the host
*includeand*import(if present on the same element) run earlier than*innerHTML.- They may rewrite
node.innerHTMLof the template but do not prevent*innerHTMLfrom running.
- They may rewrite
*printand*textContentrun before*innerHTML.- If they match, they set
textContent, append the element to the parent, and return; no later output directive runs.
- If they match, they set
- If none of the earlier output directives return,
*innerHTMLis evaluated. - After
*innerHTML, Sercrod continues with attribute bindings, style bindings, and finally recursive child rendering from the original template node.
Important consequence:
- The HTML string inserted by
*innerHTMLis not traversed by Sercrod’s renderer.- Sercrod still walks
node.childNodesfrom the template, not the newly inserted HTML string. - Any Sercrod directives that appear inside the inserted HTML string are not compiled or bound by Sercrod.
- Sercrod still walks
Execution model
Conceptually, for an element <div *innerHTML="expr"> the runtime behaves like this:
- Clone the template node (without children) into
el. - Resolve any earlier host-level directives (for example
*if,*include,*import,*literal,*rem,*print,*textContent). - When the
*innerHTMLbranch is reached:- Read the attribute value
exprfrom the host. - Evaluate it with
eval_expr(expr, scope, { el: node, mode: "innerHTML" }). - Map
nullorfalseto an empty string, otherwise use the returned value asraw. - Build a context object
{ el, expr, scope }. - Call
Sercrod._filters.html(raw, ctx)and assign the result toel.innerHTML.
- Read the attribute value
- Recursively render the original template children:
- For each
childinnode.childNodes, callrenderNode(child, scope, el). - This may append Sercrod-generated child content after the HTML inserted by
*innerHTML.
- For each
- At the end of
_renderElement, ifcleanup.directivesis enabled in the global config:- All attributes that are known Sercrod directives (including
*innerHTMLandn-innerHTML) are removed fromel.
- All attributes that are known Sercrod directives (including
- Append
elto the parent and run any log hooks.
The inserted HTML string is therefore a one-shot, low-level injection step; Sercrod does not automatically apply its own directives inside that string.
Variable creation and scope layering
*innerHTML does not create new variables.
Its expression is evaluated in the normal Sercrod scope:
- The current data bound to the host (for example from
dataon<serc-rod>). - Special injected variables:
$datafor the current host’s data object.$rootfor the root Sercrod host data.$parentfor the nearest ancestor Sercrod host data.
- Methods exposed via
*methodsor the global method injection mechanism. - Internal helper methods registered in
Sercrod._internal_methods.
There is no per-directive scope layering; *innerHTML simply looks at whatever is currently visible in the merged scope.
Parent access
*innerHTML does not alter parent relationships:
- You can read values from the root or parent using
$rootand$parentin the expression. - The inserted HTML string does not introduce a new Sercrod scope; it is just markup.
Example:
<serc-rod id="app" data='{
"post": { "id": 1, "summaryHtml": "<p>Summary</p>" }
}'>
<article>
<header>
<h1 *print="$parent.title"></h1>
</header>
<section *innerHTML="post.summaryHtml"></section>
</article>
</serc-rod>
Here, *innerHTML sees post.summaryHtml from the current scope. The <h1> can still use $parent or $root as usual.
Use with conditionals and loops
*innerHTML composes well with structural directives when they target different elements:
-
Host-level condition:
- If the same element has
*ifand*innerHTML,*ifacts as a gate.
<section *if="post && post.summaryHtml" *innerHTML="post.summaryHtml"></section>- If the condition is falsy, the section is not rendered and
*innerHTMLis not evaluated.
- If the same element has
-
Child-level condition:
- You can keep
*innerHTMLon a parent and put*ifon children defined in the template.
<div *innerHTML="introHtml"> <p *if="showNote" class="note">This note is rendered by Sercrod.</p> </div>- The
introHtmlstring is injected as markup. - The
<p>is still rendered or skipped by Sercrod based onshowNote.
- You can keep
-
Loops:
- You can use
*innerHTMLinside a*foror*eachloop body:
<ul *each="item of items"> <li> <h3 *print="item.title"></h3> <div *innerHTML="item.summaryHtml"></div> </li> </ul>-
Each iteration gets its own
<div>whose HTML is taken fromitem.summaryHtml. -
You can also put
*innerHTMLon the loop container if you want a static wrapper per loop, but usually it is clearer to keep HTML injection on child elements.
- You can use
Use with templates, *include, and *import
*innerHTML shares the same low-level insertion mechanism (innerHTML) as *include and *import, but with different responsibilities:
-
*include:- Resolves a named
*template. - Copies the template’s
innerHTMLintonode.innerHTML(the template node), not directly intoel. - Leaves element tags and attributes in place.
- Does not return; it allows Sercrod to walk the inserted template children and process their directives.
- Resolves a named
-
*import:- Fetches HTML from a URL (using a synchronous XHR, with optional caching).
- Writes the fetched HTML into
node.innerHTML. - Removes
*import/n-importfromel. - Also relies on later child rendering to process any directives inside the imported HTML.
-
*innerHTML:- Evaluates an expression to get an HTML string.
- Passes it through the
htmlfilter. - Writes the result directly into
el.innerHTML. - Does not touch
node.innerHTML; Sercrod still walks the original template children.
Combined effect when used together:
-
If you put
*includeor*importand*innerHTMLon the same element:*include/*importruns first and rewritesnode.innerHTMLfor the template.- Later,
*innerHTMLsetsel.innerHTMLfrom the expression value. - Finally, Sercrod recursively renders
node.childNodes(now coming from the include/import) intoel.
This means the final element can contain:
- The markup produced by
*innerHTML(through thehtmlfilter). - Plus additional children generated from the included/imported template.
-
Although this is well-defined in the current implementation, it produces two separate sources of children on the same element and can be difficult to reason about.
- In practice it is clearer to separate responsibilities:
- Use
*include/*importon one element. - Use
*innerHTMLon a nested element inside the included or imported content, or on a sibling container.
- Use
- In practice it is clearer to separate responsibilities:
Recommendation:
- Treat
*innerHTMLas a low-level primitive for HTML injection. - Use
*includeand*importfor Sercrod-managed templates and remote HTML. - Avoid designing APIs that rely on mixing them on the same element unless you have a very specific reason and fully understand the combined behavior.
Security and the html filter
The html filter is the main hook for controlling how HTML strings are inserted:
-
Default definition in the runtime:
html(raw, ctx) => raw.
-
Responsibilities:
- This filter is the extension point for:
- Sanitizing HTML to prevent XSS when values come from untrusted input.
- Post-processing HTML strings before insertion (for example, rewriting links, adding attributes, or delegating to another HTML engine).
- This filter is the extension point for:
-
When
htmlthrows:-
If
error.warnis enabled on the Sercrod instance, the runtime logs a warning:- It includes the message, the expression, the scope, and the element.
-
The element’s
innerHTMLis set to an empty string.
-
Guidelines:
- Never pass untrusted user input directly to
*innerHTMLwithout a proper sanitizer in thehtmlfilter. - For trusted static or server-generated HTML,
*innerHTMLcan be used as-is. - For Markdown or other formats:
- Convert them to HTML in normal JavaScript (for example,
renderMarkdownToHtml(text)). - Apply sanitization and then return the safe string.
- Sercrod simply calls your function through the expression.
- Convert them to HTML in normal JavaScript (for example,
Example with an external sanitizer:
<script>
function safeHtmlFromMarkdown(md){
const html = renderMarkdownToHtml(md); // your own converter
return sanitizeHtml(html); // your own sanitizer
}
</script>
<serc-rod id="doc" data='{"bodyMd": "# Title"}'>
<article *innerHTML="safeHtmlFromMarkdown(bodyMd)"></article>
</serc-rod>
Best practices
-
Prefer
*printor*textContentfor plain text.- Use
*innerHTMLonly when you intentionally need markup injection.
- Use
-
Use the
htmlfilter for security:- Override
Sercrod._filters.htmlto integrate a sanitizer when working with any untrusted input.
- Override
-
Keep responsibilities separate:
- Avoid combining
*innerHTMLwith*printor*textContenton the same element; only one of them will take effect. - Avoid designing components that rely on mixing
*innerHTMLwith*includeor*importon the same element; prefer nesting instead.
- Avoid combining
-
Keep expressions simple:
- If HTML strings need complex assembly, build them in normal JavaScript functions and call those functions from
*innerHTML. - This keeps templates readable and logic testable.
- If HTML strings need complex assembly, build them in normal JavaScript functions and call those functions from
-
Be aware of re-renders:
- On each re-render of the host,
*innerHTMLrebuilds the inner markup from scratch. - Any state stored only inside the injected HTML (for example, manual event listeners or form values) may be lost unless you manage it separately.
- On each re-render of the host,
-
Do not expect Sercrod to process directives inside injected HTML:
- The inserted string is not parsed by Sercrod.
- If you need Sercrod directives inside dynamic content, use
*includeor*importso thatnode.innerHTMLis updated before child rendering.
Additional examples
Fallback summary:
<serc-rod id="post" data='{
"post": {
"title": "Post title",
"summaryHtml": null
}
}'>
<h2 *print="post.title"></h2>
<div *innerHTML="post.summaryHtml || '<p>No summary available.</p>'"></div>
</serc-rod>
- If
summaryHtmlisnullorfalse, the expression falls back to the HTML snippet. 0or an empty string are not treated asnull/falseby the directive and will still be inserted.
Combining *include with *innerHTML on nested elements:
<template *template="post-card">
<article class="post-card">
<h2 *print="post.title"></h2>
<div class="summary" *innerHTML="post.summaryHtml"></div>
</article>
</template>
<serc-rod id="list" data='{"posts":[
{ "title": "First", "summaryHtml": "<p>First summary</p>" },
{ "title": "Second", "summaryHtml": "<p>Second summary</p>" }
]}'>
<section *each="post of posts">
<div *include="'post-card'"></div>
</section>
</serc-rod>
*includecopies thepost-cardtemplate into the innerHTML of the<div>.- Inside that template,
*innerHTMLinjectspost.summaryHtmlas markup. - This pattern keeps Sercrod templates in control while using
*innerHTMLonly where HTML strings already exist.
Notes
*innerHTMLandn-innerHTMLare functionally identical; choose one naming style per project.- The directive maps
nullandfalseto an empty string; other values (including0and empty strings) are passed to thehtmlfilter as-is. - Errors in the
htmlfilter produce an optional console warning and clear the element’s innerHTML. - Inserted HTML strings are not scanned for Sercrod directives; they are treated as plain DOM content.
- When
cleanup.directivesis enabled,*innerHTMLandn-innerHTMLattributes are removed from the output DOM like other Sercrod directives.