Control flow
Templates branch and repeat with brace blocks. Sumi drops Svelte’s
sigils — {if} where Svelte writes {#if} — and the expressions inside
are Go. This chapter covers conditionals, loops, and what key= buys.
Conditionals
{if count.Get() > 0}
<p>Non-empty</p>
{else}
<p>Empty</p>
{/if} The condition is a Go boolean expression. It is not template text, so a
signal needs an explicit .Get() — the auto-unwrap that applies to {count} in body text does not apply here. The {else} branch is
optional. A branch may hold any number of elements, text, and nested
blocks.
No {else if}
There is no {else if} form. Writing one produces invalid Go and fails
at generation. Chain conditions by nesting an {if} inside {else}:
{if n.Get() == 0}
<p>none</p>
{else}
{if n.Get() == 1}
<p>one</p>
{else}
<p>many</p>
{/if}
{/if} Loops
{for i, item := range items.Get() key=item.ID}
<li>{i}: {item.Title}</li>
{/for} Everything between {for and the trailing key= is a real Go range
clause, emitted verbatim into a for … range loop. Both the index and
value bindings are in scope in the body, and — being ordinary text
expressions — {i} and {item.Title} interpolate directly. items is
a signal, so it reads as items.Get(); because the clause is Go, not
template text, you write that .Get() yourself.
The key= attribute is split off the end of the clause on the last
occurrence of key=, so a range variable whose name contains key does not confuse the parser. The key is a Go expression evaluated per
item.
What key= buys
Without a key, list items diff positionally: the box at index 0 is compared with the new index 0, and so on. Reordering or inserting then rebuilds every box from the first change onward.
With a key, items are matched by identity across renders. When the list reorders, each item’s existing box is found by its key and moved rather than rebuilt, so per-box state that lives in the tree — focus, an input’s edit cursor, scroll position — travels with the item instead of staying pinned to a slot. Use a key that is stable and unique per item (a record ID, not the array index).
Nesting
Blocks nest freely and mix with elements. A loop can contain conditionals, a conditional can contain loops, and either can sit inside an element or hold elements:
<ul>
{for _, task := range tasks.Get() key=task.ID}
<li>
{if task.Done}
<span class="done">{task.Title}</span>
{else}
<span>{task.Title}</span>
{/if}
</li>
{/for}
</ul> Each block must be closed by its own terminator — {/if} for {if}, {/for} for {for} — before the enclosing block closes. An unterminated
block is a generation error.
How blocks re-render
A container holding control flow compiles so that the block’s contents
are produced by a function, and that rebuild runs inside the component’s sumi.Effect. The effect subscribes to whatever signals the condition or
range clause reads, so changing one of them re-evaluates the block in
place — picking the live branch, or ranging the current list — as part of
the component’s dynamic-node sync described in signals. The
single dirty flag then schedules one repaint, and the tree diff writes
only the cells that actually changed. Keyed loops reuse the matched boxes
across that rebuild rather than reconstructing them.