Skip to content

Live: props from the server

A prop is a signal. When the server changes something, it writes one property, which wakes one binding, which updates one DOM node. It does not render a fragment and swap it in.

Watch a prop patch land frames are scripted; the client applying them is the real one
on the server
sess.Element("demo-todos").Set("items", list.Items())
// or several as one frame
sess.Batch(func() {
sess.Element("demo-todos").Set("items", list.Items())
sess.Element("demo-todos").Set("filter", "done")
})
in the page
on the wire
nothing sent yet

The frames are scripted here because a static site has no Go server, but the client applying them is the one the Go module embeds. The runnable example in the module is a live Kanban board (`examples/todo`); the pattern is the same: one property write, every open tab.

  1. Create a server and mount it.

    srv := live.New()
    defer srv.Close()
    mux := http.NewServeMux()
    live.Mount(mux, alacris.DefaultBase, srv)

    Mount registers the runtime, the live client, and the two endpoints. Doing it by hand is fine; this only exists so the three cannot drift apart.

  2. Mint a session per page render and put its page id in the page. The capability is a cookie the call sets; see Sessions.

    func (a *app) index(w http.ResponseWriter, r *http.Request) {
    // Sets the cookie that authorises this browser, so it has to run
    // before anything is written to w.
    sess := a.live.NewSession(w, r)
    sess.OnOpen(func(s *live.Session) { a.push(s) })
    cfg := alacris.Config{
    Modules: []string{"/web/components.js"},
    UI: true,
    Live: true,
    Page: sess.ID(),
    }
    // ... render the page with cfg
    }
  3. Give the elements you will patch an id. A patch addresses an element by its id, and generated wrappers return an *Element, so:

    @components.Board(props).ID("board")
  4. Patch.

    sess.Element("board").Set("items", list.Items())
h := sess.Element("board")
h.Set("items", list.Items()) // a component prop
h.SetAttr("data-state", "ready") // an ordinary attribute; nil removes it
h.Class("is-busy", true) // toggle a class
h.SetHTML(ctx, "empty", tmpl) // replace the children of one slot
h.Props(map[string]any{...}) // several props, one frame

Set takes the JavaScript prop name from define() (maxCount, not max-count) because it writes the DOM property rather than the attribute. That is also why values need no encoding: they are JSON on the wire and become real JavaScript values on arrival.

Every Send is a frame. Wrap a group so the browser applies them together and the page does not paint half an update:

sess.Batch(func() {
sess.Element("board").Set("items", items)
sess.Element("board").Set("columns", columns)
})
srv.Broadcast(live.Prop("ticker", "price", 42.5))
for _, s := range srv.Sessions() {
s.Element("board").Set("items", items) // when each page needs different data
}

This is what makes two open tabs agree without either of them polling.

SetHTML replaces the light-DOM children assigned to one slot with markup the server rendered:

h.SetHTML(ctx, ui.ChipSlotDefault, chipText("4 left"))

Use it when the server owns the markup: a rendered document, a chunk of a report. For anything a component already models as a prop, a prop is smaller, does not touch the DOM the user is interacting with, and keeps rendering where the component put it.

Sessions live in memory, so the server is stateful and needs session affinity behind a load balancer. See Deploying. The first two layers of this library have no dependency on any of it.