Live: actions from the page
Props go down. Actions come back up. Together they close the loop: the page asks, the server decides, and the answer arrives as a prop write.
component emits 'add' -> POST /_alacris/live -> handler mutates state -> sess.Element("board").Set("items", ...) -> SSE frame -> one DOM updateNo HTML crosses in either direction.
Declaring one
Section titled “Declaring one”On forwards a CustomEvent the component emits to a named server action:
@components.Board(props). ID("board"). On(components.BoardEventAdd, "add-card"). On(components.BoardEventMove, "move-card")That renders data-ala-on="add:add-card move:move-card". The client
attaches one listener per event type to the document. alacris events are
composed and bubbling, so a single listener reaches every element, across shadow
boundaries, including elements added later.
Handling one
Section titled “Handling one”live.On(srv, "add-card", func(c *live.Ctx, d components.BoardAddDetail) error { if _, err := list.Add(d.Text, d.Column); err != nil { return nil // nothing changed; nothing to push } c.Session.Element("board").Set("items", list.Items()) return nil})live.On is generic: the event detail is decoded into the type parameter
before your handler runs, and components.BoardAddDetail is the struct the
generator wrote from the component’s @fires tag. Rename the field in the
component and this stops compiling.
The untyped form is there when you want the raw payload:
srv.On("add-card", func(c *live.Ctx) error { var d components.BoardAddDetail if err := c.Bind(&d); err != nil { return err } ...})Per-session handlers
Section titled “Per-session handlers”A handler registered on a session takes precedence over the server-wide one and can close over that page’s state:
sess.On("rename-list", func(c *live.Ctx) error { var d components.BoardRenameDetail if err := c.Bind(&d); err != nil { return err } c.Session.Set(titleKey{}, d.Title) c.Session.Element("board").Set("columns", list.Columns()) return nil})Use it for anything that belongs to one page: a selection, a wizard step, a draft. Server-wide handlers are for changes everyone shares.
What the context carries
Section titled “What the context carries”type Ctx struct { Session *Session // the page it came from Action string // the name you declared Element string // the id of the element that emitted it Detail json.RawMessage // the CustomEvent detail, still encoded Request *http.Request // headers, cookies, the request context}c.Handle() is shorthand for c.Session.Element(c.Element), which is usually
what you want:
live.On(srv, "greet", func(c *live.Ctx, d components.UserCardGreetDetail) error { c.Handle().Set("greeted", true) // patch whatever emitted the event return nil})Errors
Section titled “Errors”Returning an error logs it and answers 500. It does not reach the browser.
What a user should see is a patch, sent from the handler, in whatever terms the
page already understands.
live.On(srv, "add-card", func(c *live.Ctx, d components.BoardAddDetail) error { if _, err := list.Add(d.Text, d.Column); err != nil { // Say so in the page's own vocabulary rather than leaking an error. c.Session.Element("board").Set("busy", false) return nil } ...})Treat the detail as input
Section titled “Treat the detail as input”Keeping the names honest
Section titled “Keeping the names honest”Both halves of an action are strings, so put them in one place:
const ( actionAdd = "add-card" actionMove = "move-card")An action nobody handles is logged as a warning and answered 404, rather than
being silently dropped. A rename that only happened on one side is otherwise
very hard to find.
When not to use this
Section titled “When not to use this”If a change is entirely the browser’s business (a disclosure toggle, a hover state, a local draft), leave it in the component. Signals already handle it, and a round trip makes it worse. A card’s inline title draft stays in the component until the user commits it; only then does the server hear about it.