Skip to content

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 update

No HTML crosses in either direction.

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.

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
}
...
})

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.

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
})

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
}
...
})

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.

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.