*case / *case.break
Summary
*case marks a branch inside a *switch / n-switch block. The branch is rendered when the *switch expression matches the case condition. *case.break is sugar for “case plus break”: it behaves like *case, and additionally stops evaluating later branches.
Key points:
*caseand*case.breakonly have an effect as direct children of an element with*switchorn-switch.- When a case matches, rendering starts at that branch and continues for later siblings (“fallthrough”) until a break is encountered.
- Branches without any
*case,*case.break,*default, orn-defaultare ignored by the switch engine.
Basic example
Simple switch over a status value:
<serc-rod id="app" data='{"status":"ready"}'>
<div *switch="status">
<p *case="'idle'">Idle</p>
<p *case="'ready'">Ready</p>
<p *default>Unknown</p>
</div>
</serc-rod>
Behavior:
- The
<div *switch="status">itself is not rendered. statusis evaluated once, and its value is exposed to children as$switch.*case="'ready'"matches the value"ready", so that<p>is rendered.- No
*breakis present, but there are no later branches in this example, so nothing else is rendered.
Behavior
Inside a *switch / n-switch host:
- Only direct element children that carry one of:
*casen-case*case.breakn-case.break*defaultn-defaultare treated as switch branches.
- Other children (elements without these attributes, or deeper descendants) are ignored by the switch engine.
Selection and fallthrough:
- The switch host evaluates its own
*switch/n-switchexpression to produce a valueswitchVal. - It then scans its direct children in DOM order.
- The first child that satisfies either:
- a matching
*case/*case.break/n-case/n-case.break, or - a
*default/n-defaultwhen no previous branch has matched, becomes the starting point.
- a matching
- That starting branch and all later branches are rendered (“fallthrough”) until a break is hit.
Cloning and attributes:
- For each branch that is rendered, the original child element is cloned.
- On the clone, control attributes are removed:
*case,n-case,*case.break,n-case.break*default,n-default*break,n-break
- The clone is then passed to Sercrod’s normal rendering pipeline with a scope that includes
$switch.
Case expression semantics
*case and *case.break both use the same matching engine. They evaluate their attribute value in the current scope (augmented with $switch) and compare it to the *switch value.
The raw attribute string is processed as follows:
-
Sercrod first tries to evaluate the full expression:
- The expression is evaluated using
eval_expr(raw, {...scope, $switch: switchVal}). - If evaluation succeeds, the resulting value
vis matched by type. - If evaluation throws, Sercrod falls back to a string-based list mechanism (see below).
- The expression is evaluated using
-
If evaluation succeeded, the result
vis matched according to its type:-
Function:
- If
typeof v === "function", Sercrod callsv(switchVal, scope). - If the call returns a truthy value, the case matches.
- If
-
Regular expression:
- If
vis aRegExp, Sercrod testsv.test(String(switchVal)). - If the test returns
true, the case matches.
- If
-
Array:
- If
Array.isArray(v)is true, the case matches when any array element is strictly equal toswitchVal(usingObject.is).
- If
-
Objects with
has:- If
vis an object and has ahasmethod (for example aSet), Sercrod callsv.has(switchVal). - If
hasreturns truthy, the case matches.
- If
-
Boolean:
- If
vis a boolean, its value is used directly. truemeans the case matches;falsemeans it does not.
- If
-
Primitive string / number / bigint:
- For
typeof vin{ "string", "number", "bigint" }, Sercrod comparesvandswitchValwithObject.is. Object.isis used rather than===, soNaNmatchesNaN, and+0and-0are distinguished.
- For
-
Any other type:
- If
vdoes not fit any of the above categories, the case does not match.
- If
-
-
If evaluation failed (expression threw), Sercrod falls back to a token list:
- The raw string is split on commas and pipes:
/[,|]/. - Each token is trimmed; empty tokens are ignored.
- For each token
t:- Sercrod tries to evaluate
tas an expression. - If that evaluation fails, it falls back to treating
tas a string literal. - The token value
vvis then compared toswitchValwithObject.is. - If any
vvmatchesswitchVal, the case matches.
- Sercrod tries to evaluate
- The raw string is split on commas and pipes:
This means you can write all of the following:
-
Simple literal:
<p *case="'ready'">Ready</p> -
Multiple values with an array:
<p *case="['ready','done']">Finished</p> -
Regular expression:
<p *case="/^admin-/">Admin section</p> -
Predicate function:
<p *case="(value, scope) => value > 10">Large</p> -
Comma- or pipe-separated list:
<p *case="'ready' | 'done'">Finished</p> <p *case="'draft, pending'">Not final</p>
Evaluation timing
Within the host that has *switch / n-switch:
- The
*switchexpression is evaluated once to obtainswitchVal. - This happens as part of the structural rendering phase, after any host-level
*if/*elseif/*elsechain has been resolved. - The
*switchhost does not render itself; only its children (cases and default) are rendered. - For each candidate case branch, the case expression is evaluated when needed:
- Cases are evaluated in DOM order, stopping at the first branch that matches or the first default that is reached with no matches.
- Once a starting branch is found, later cases are not evaluated for matching; they participate only in fallthrough rendering.
Execution model
At a high level, the switch engine works like this for *case and *case.break:
- Evaluate
switchValfrom the host’s*switch/n-switchexpression. - Prepare a child scope:
- Clone the current scope.
- Inject
$switch = switchVal.
- Scan the host’s direct element children in DOM order.
- For each child
c:- Determine whether it is:
- a
*case/n-case/*case.break/n-case.breakbranch, or - a
*default/n-defaultbranch, or - neither.
- a
- Ignore children that are neither case nor default.
- Determine whether it is:
- Before rendering has started (no branch selected yet):
- If
cis a default branch:- Start rendering from this branch (
falling = true).
- Start rendering from this branch (
- Else if
cis a case branch:- Evaluate the case expression with
$switchin scope. - If the match succeeds, start rendering from this branch (
falling = true). - If it does not match, skip this child.
- Evaluate the case expression with
- If
- After rendering has started (
falling = true):- Clone
c. - Strip control attributes (
*case,n-case,*case.break,n-case.break,*default,n-default,*break,n-break) from the clone. - Render the clone with the child scope (which includes
$switch). - Check for break:
- If the original child
chas*break,n-break,*case.break, orn-case.break, stop the switch here and do not render later branches.
- If the original child
- Clone
As a result:
*casewithout*breakparticipates in fallthrough: later branches are also rendered.*case.breakbehaves as*caseplus an implicit*break.- A branch with
*break(orn-break) stops rendering after that branch, regardless of the value of the*breakattribute.
Variable creation and scope layering
*case and *case.break do not create new local variables by themselves. Instead, they rely on the scope prepared by the switch host:
$switchis injected by the switch host and is available in:- case expressions.
- default branches.
- the body content of all rendered branches.
- All existing scope variables (from the Sercrod host and outer scopes) remain available.
- Case expressions may introduce additional values via normal Sercrod expressions (for example, calling functions or reading from the data model), but
*casedoes not persist any new names into the shared scope.
The scope used inside each branch is effectively:
- A shallow clone of the incoming scope, plus
$switch.
Use with *default and *break
*case interacts closely with *default and *break inside the same *switch block.
-
*default/n-default:- A default branch is picked only if no earlier case branch has matched.
- If a default branch is the starting point, it participates in fallthrough exactly like any other branch.
- The runtime does not enforce that there is only one default; for clarity, you should keep at most one default per switch.
-
*break/n-breakon a branch:- In a switch context,
*breakis treated as a flag, not as a conditional:- If a branch carries
*breakorn-break, the switch stops after that branch is rendered. - The attribute value is not inspected by the switch engine.
- If a branch carries
- This is separate from how
*breakis used in loops; here it simply means “stop fallthrough now”.
- In a switch context,
-
*case.break/n-case.break:- Sugar for
*case="expr" *break. - The expression part participates in case matching exactly like
*case. - The presence of
.breakalso marks the branch as a break point:- If it is selected as the starting branch, rendering stops after this branch.
- If rendering has already started earlier and execution reaches a
.breakbranch in fallthrough, it will render that branch and then stop.
- Sugar for
Example with fallthrough and break:
<serc-rod id="app" data='{"level":2}'>
<div *switch="level">
<p *case="1">Level 1</p>
<p *case="2">Level 2</p>
<p *case="3" *break>Level 3 (stop here)</p>
<p *default>Level is 4 or more</p>
</div>
</serc-rod>
- When
levelis2:- Rendering starts at
*case="2". - The branch for
2is rendered. - The branch
*case="3" *breakis also rendered. - Because of
*breakon the3branch, the*defaultbranch is not rendered.
- Rendering starts at
Use with conditionals and loops
Branches can freely combine *case with other directives:
-
*ifon the same element:- The case still participates in branch selection based on its case expression.
- Once selected, the cloned element is rendered by the normal pipeline; any
*ifon that element is evaluated as usual. - This means a selected case can still be hidden by a false
*if.
-
*for,*each, and other structural directives inside the branch:-
You can place loops inside the body of a branch:
<div *switch="$data.mode"> <section *case="'list'"> <ul *each="item of items"> <li *print="item.label"></li> </ul> </section> <section *default> <p>No list available.</p> </section> </div> -
You can also put
*foror*eachon the same element as*case:<div *switch="view"> <ul *case="'list'" *each="item of items"> <li *print="item.label"></li> </ul> <p *default>Select a view.</p> </div>In this pattern:
- The
<ul>is chosen as the branch whenviewis"list". - After selection,
*eachis evaluated on the cloned<ul>as usual.
- The
-
-
Nested switches:
- A case branch can itself contain another
*switch/n-switchblock; the inner switch sees its own$switchvalue. - Outer
$switchremains accessible via the parent scope if you store it in data or pass it through.
- A case branch can itself contain another
Best practices
-
Prefer simple case expressions where possible:
- Literal values (
'ready',1,"admin") keep the logic easy to read. - Arrays, sets, regular expressions, and predicate functions are powerful but should be used where they clearly add value.
- Literal values (
-
Use
*case.breakfor the common “match and stop” pattern:-
This keeps the markup compact:
<p *case.break="'ready'">Ready</p>
-
-
Use fallthrough intentionally:
- If you omit breaks, later branches will be rendered as part of the same switch pass.
- Use this to group related output or to build a sequence of messages.
- Avoid “accidental fallthrough” by placing
*breakor using*case.breakwhere you expect a strict one-branch behavior.
-
Keep branch ordering explicit:
- Sercrod always processes branches in DOM order.
- Place more specific cases before more general ones when using patterns such as regular expressions or predicate functions.
-
Keep switch blocks shallow:
- Since only direct children of the
*switchhost participate as branches, keep the branch markers (*case,*case.break,*default) at the top level under the host. - Nested
*caseunder additional wrappers are not seen by the switch engine.
- Since only direct children of the
Additional examples
Multiple values and regular expression:
<serc-rod id="router" data='{"path":"/admin/users"}'>
<div *switch="path">
<p *case="['/', '/home']">Home</p>
<p *case="/^\\/admin\\//">Admin area</p>
<p *default>Unknown path</p>
</div>
</serc-rod>
Predicate function:
<serc-rod id="grader" data='{"score":82}'>
<div *switch="score">
<p *case="value => value >= 90">Grade A</p>
<p *case="value => value >= 80">Grade B</p>
<p *case="value => value >= 70">Grade C</p>
<p *default>Needs improvement</p>
</div>
</serc-rod>
Here the first predicate that returns true determines the starting branch, and fallthrough behavior applies as usual.
Notes
-
*case,n-case,*case.break, andn-case.breakare only meaningful as direct children of a*switch/n-switchhost.- Outside that context, they are treated as normal attributes and have no special effect.
-
$switchis injected by the switch host and is available:- Inside case expressions.
- Inside default branches.
- Inside the rendered content of all selected branches.
-
The runtime does not enforce a single default branch.
- For readability and to avoid surprising fallthrough, it is recommended to keep at most one
*default/n-defaultper switch.
- For readability and to avoid surprising fallthrough, it is recommended to keep at most one
-
In a switch context,
*break/n-breakon a branch is treated as a flag; the attribute value is not inspected for match conditions.- Expression-based break behavior belongs to loop contexts and is documented separately.
-
*case.break/n-case.breakare pure sugar for “case plus break”.- They share the same expression semantics as
*case/n-case. - They additionally guarantee that the switch stops after rendering that branch.
- They share the same expression semantics as