sumi
Chapters

Components and reactivity

A .sumi file is a single-file component: a <script> block of Go, a <style> block of CSS, and a template of HTML elements. sumi generate compiles each file to a Go constructor (counter.sumiNewCounter(CounterProps) *sumi.Component) that your main.go — or another component — mounts.

Signals

State lives in signals — fine-grained reactive values in the Solid.js style. Reading a signal inside a template expression subscribes that expression; writing the signal re-renders exactly what depends on it.

count := sumi.New(0)                 // *Signal[int]
count.Get()                          // read
count.Set(5)                         // write
count.Update(func(n int) int { return n + 1 })

doubled := sumi.From(func() int {    // derived: recomputes when count changes
	return count.Get() * 2
})

In the template, {count} renders the current value and stays live — signals are auto-unwrapped in text expressions. Attribute expressions are raw Go: write class={barClass.Get()} explicitly.

Template expressions

  • {expr} in text — any Go expression; signals auto-unwrap.
  • name="literal" — string attribute.
  • name={expr} — expression attribute (raw Go, no auto-unwrap).
  • {name} — shorthand for name={name}.

Control flow

{if count.Get() > 0}
	<p>Non-empty</p>
{else}
	<p>Empty</p>
{/if}

{for _, item := range items.Get() key=item.ID}
	<div>{item.Title}</div>
{/for}

The {for} clause is a real Go range clause. key= enables identity-based diffing so reordered items keep their boxes (and focus) instead of being rebuilt positionally.

Event handlers

Declare functions in <script> and reference them from on<type> attributes:

<script>
func save(evt *sumi.DOMEvent) {
	evt.PreventDefault()
}

func handleKey(evt sumi.Event) {
	if evt.Rune == 'q' { sumi.Quit() }
}
</script>

<div onkey="handleKey">
	<button onclick={save}>Save</button>
</div>

Handlers taking *sumi.DOMEvent participate in capture/bubble dispatch with StopPropagation / PreventDefault (see the events sections in elements). A function named handleKey taking sumi.Event becomes the component’s raw event handler — it sees every input event after DOM dispatch. Deviation: that wiring is name-based; only handleKey is picked up.

Zero-argument handlers get automatic Ctrl+C/quit-signal handling; declaring any event-aware handler hands you full control (call sumi.Quit() yourself).

Props

Declare props as plain var declarations; consumers pass them via the generated props struct:

<!-- greeting.sumi -->
<script>
var name string
</script>
<p>Hello, {name}!</p>
greeting.NewGreeting(greeting.GreetingProps{Name: "Tom"})

Callback props are function-typed vars, passed the same way. bind:value on an input-like component binds a parent signal to the child’s value in both directions.

Composition

Components compose two ways:

  • In a template<counter label="Clicks" /> mounts a sibling component (its constructor must be importable by the generate CLI).
  • In Go — embed a child’s tree directly:
c1 := counter.NewCounter(counter.CounterProps{Label: "Clicks"})
root := parent.Tree // any *layout.Input
root.Children = append(root.Children, c1.Tree)

Each component carries its own stylesheet; styles are scoped by the component’s own cascade and do not leak into embedded children.

Slots and snippets

A component template can declare <slot:name /> placeholders with optional fallback content; consumers fill them with {slot name}...{/slot} blocks. {snippet name(params)}...{/snippet} defines a local template function invoked with {render name(args)}.

Lifecycle

sumi.Quit() ends the app from any handler. A component’s Dispose runs when the app exits (or when a FrameLog frame is archived or removed). There is no polling: a single dirty flag set by signal writes schedules the next render pass.