*let
Summary
*let runs a small piece of JavaScript in Sercrod’s sandbox to define local helper variables. These variables are available to expressions on the same element and all of its descendants. Newly created variable names are also promoted into the host data so that later elements inside the same <serc-rod> can read them.
Alias:
*letandn-letare aliases and behave the same.
Basic example
Compute a derived value once and reuse it in the element’s subtree:
<serc-rod id="invoice" data='{"price": 1200, "qty": 3}'>
<p *let="total = price * qty">
Subtotal: <span *print="total"></span> JPY
</p>
</serc-rod>
In this example:
*letruns before any other directive on the<p>.- The code
total = price * qtycreates a new variabletotalin the local scope. - The child
<span *print="total">can readtotaldirectly. - Because
totaldid not exist in the host data before, it is also promoted into the Sercrod data scope for this<serc-rod>.
Behavior
At a high level, *let behaves like “execute this code in the current data scope and keep any new variables”:
- It reads the current data for the host
<serc-rod>and any in-scope iteration variables. - It executes the
*letstring as JavaScript (expressions or simple statements) in a sandboxed scope. - It updates the effective scope for the current element and its descendants.
- It promotes newly created variable names into the host data, but does not overwrite existing ones.
Key points:
*letis a non-structural directive: it does not clone or repeat elements; it just prepares values for other directives.- The attribute remains on the element; internally Sercrod re-evaluates it on each re-render.
*letruns before*if,*switch,*each, and*foron the same element, so later directives can rely on variables created by*let.
Expression model
The value of *let is treated as JavaScript code:
- The code runs inside Sercrod’s expression sandbox using a dedicated scope object.
- You can write one or more simple statements.
Typical patterns:
-
Single assignment:
<div *let="fullName = user.first + ' ' + user.last"> <p *print="fullName"></p> </div> -
Multiple assignments or function calls:
<div *let=" subtotal = price * qty; tax = subtotal * taxRate; total = subtotal + tax; "> <p *print="total"></p> </div> -
Calling helper functions:
<div *let="displayName = formatUserName(user)"> <span *print="displayName"></span> </div>Here
formatUserNamemust be available in the scope (for example, exported via*methodson the host).
What the sandbox does:
- Reads variables from the current per-element scope (data fields, loop variables, earlier
*letvalues). - Reads built-in globals (such as
Math,Date) from the real global environment. - Writes always go into the local scope used for
*let, never directly intoglobalThis.
Evaluation timing
*let is evaluated early in the per-element pipeline:
-
It is processed before structural directives on the same element:
- Before
*if/n-if. - Before
*switch/n-switch. - Before
*each/n-each. - Before
*for/n-for.
- Before
-
It is also evaluated before
*globalon the same element.
As a result:
-
You can compute helper values in
*letand use them immediately in:*ifconditions on the same element.*switchexpressions.*eachand*forexpressions.- Any
*print, bindings, or event handlers on this element and its children.
Example:
<li *let="is_expensive = price > 1000"
*if="is_expensive">
<span *print="name"></span>
<span>(premium)</span>
</li>
Here *if can safely use is_expensive because *let runs first.
Execution model
Conceptually, Sercrod handles *let on an element like this:
-
Compute the current effective scope
effScopefor this element:- Based on the host data for the
<serc-rod>. - Including any variables from surrounding loops or parent
*letdirectives.
- Based on the host data for the
-
If the element has
*letorn-let:- Create a new scope object whose prototype points to
effScope. - Copy the current values from
effScopeinto this new scope. - Inject
$parentso that branch-local code has access to the nearest ancestor Sercrod’s data. - Inject any methods that were registered via
*methodsand Sercrod’s internal helper methods.
- Create a new scope object whose prototype points to
-
Run the
*letcode inside Sercrod’s sandbox:- Reads go through the scope or, as a fallback, the real global environment for standard objects (such as
Math). - Writes update only the local scope object created for
*let. - Unknown identifiers become new variables on this local scope when you assign to them.
- Reads go through the scope or, as a fallback, the real global environment for standard objects (such as
-
After the code runs:
- Sercrod copies any variables that did not exist in the host data into the host data object.
- Existing host data keys are not overwritten by
*let. - The new scope becomes the effective scope for this element and its descendants.
-
Sercrod schedules a re-render if necessary so that bindings see the updated values.
This model keeps *let local by default, but still lets you share newly defined helper variables with other elements in the same <serc-rod>.
Variable creation and promotion
*let distinguishes between:
- New variable names created by
*let. - Existing data properties already present in the host’s data.
Rules:
-
When
*letcreates a new name (for exampletotal):-
The new name lives in the local
*letscope and is visible to:- The current element and its descendants.
- Later elements in the same
<serc-rod>, because Sercrod promotes this name into the host data.
-
-
When
*letassigns to a name that already exists in the host data (for exampleprice):- The host data’s property is not overwritten by
*let. - Only the local
*letscope sees the updated value. - Expressions in the current element and its descendants see the updated value (because they use the
*letscope), but siblings outside this subtree still see the original host data.
- The host data’s property is not overwritten by
In practice:
- Use
*letto create new, derived variables (such astotal,label,filteredItems). - Do not rely on
*letto permanently modify existing host data properties. If you need that behavior, use*globalinstead.
Scope layering and parent access
Inside *let:
-
You can access the same variables that ordinary expressions can see:
- Fields from the host data (for example
user,items,config). - Loop variables like
item,index,row,cellwhen used inside*eachor*forbodies. - Methods referenced via
*methodsfor the host.
- Fields from the host data (for example
-
Additionally, Sercrod injects
$parentinto the scope:$parentrefers to the data of the nearest ancestor<serc-rod>component (if any).- This makes it possible to compute values based on both local data and parent data.
The local *let scope then becomes the base scope for:
- All expressions on the same element (such as
*if,*print,:class,@click). - All expressions on child elements, including nested loops and conditionals.
Use with conditionals and loops
*let works closely with conditionals and loops.
On the same element as *if / *elseif / *else:
*letis evaluated before the condition for that branch.- Each branch can have its own
*let, and Sercrod uses a branch-specific scope when checking its condition.
Example:
<li *if="kind === 'user'" *let="label = user.name">
<span *print="label"></span>
</li>
<li *elseif="kind === 'guest'" *let="label = guest.nickname">
<span *print="label"></span>
</li>
<li *else *let="label = 'Unknown'">
<span *print="label"></span>
</li>
Here:
- Each branch computes its own
labelbefore the branch condition is evaluated. - Only the chosen branch is rendered, along with its computed label.
Inside loops:
- When used inside
*eachor*forbodies,*letruns once per iteration.
Example:
<ul *each="item of items">
<li *let="label = item.name + ' (#' + item.id + ')'">
<span *print="label"></span>
</li>
</ul>
Each <li> computes its own label using the item from that iteration.
You can also use *let on the loop container:
<ul *each="item of items"
*let="hasItems = !!items && items.length > 0">
<li *if="hasItems" *print="item.name"></li>
</ul>
hasItems is computed before the *each and is available to each child *if and *print.
Best practices
-
Prefer new helper variables:
- Use
*letto create new derived names (total,label,normalizedUsers), not to overwrite existing ones. - For permanent data mutations, use
*globalor update your data outside of templates.
- Use
-
Keep
*letcode simple:- Compute values, do light branching, and call small helper functions.
- Avoid long, complex procedures inside templates; move heavy logic into reusable JavaScript functions and call them from
*let.
-
Use
*letto prepare values for multiple bindings:- If the same expression appears multiple times in one element or subtree, compute it once in
*letand reuse the variable.
- If the same expression appears multiple times in one element or subtree, compute it once in
-
Avoid unnecessary side effects:
*letre-runs on re-render, so side-effectful operations (such as network calls) should not be placed directly inside*let.- Instead, call idempotent helpers or use other mechanisms designed for side effects.
Additional examples
Sharing a derived variable with siblings:
<serc-rod id="totals" data='{"items":[{"name":"A","price":100},{"name":"B","price":200}]}'>
<section *let="
subtotal = 0;
for(const item of items){
subtotal = subtotal + item.price;
}
">
<p>Subtotal: <span *print="subtotal"></span> JPY</p>
</section>
<!-- Later element can also see subtotal, because it was a new name -->
<p>Summary: total amount is <span *print="subtotal"></span> JPY</p>
</serc-rod>
Using $parent data in a nested component:
<serc-rod id="root" data='{"currency":"JPY"}'>
<serc-rod id="child" data='{"price": 500}'>
<p *let="text = price + ' ' + $parent.currency">
<span *print="text"></span>
</p>
</serc-rod>
</serc-rod>
Here:
- The inner Sercrod host defines
pricein its own data. *letreads$parent.currencyfrom the outer host and combines it withprice.
Notes
*letandn-letare aliases.- The code runs inside Sercrod’s sandbox and uses a special scope; it is not the same as writing code directly into global script tags.
*letdoes not overwrite existing host data properties; it only promotes newly created names into the host data.- Reads can see global built-in objects like
Math, but writes go into the local scope instead of the real global environment. *letis evaluated on each render of the element. Ensure that the code is safe to run multiple times.- There are no special combination restrictions for
*letbeyond its evaluation order:- It may appear together with
*if,*switch,*each,*for, and other directives on the same element. - It simply runs first and prepares values for the rest of the directives on that element.
- It may appear together with