via

Examples

Four programs under internal/site/snippet/src/examples, each one main package with no dependency past via. Run them from internal/site in a checkout of the repo; each listens on :8080, or on VIA_ADDR when set, so two can run side by side.

git clone https://github.com/go-via/via
cd via/internal/site
go run ./snippet/src/examples/counter
On this page

#First steps

Read these two in order. The second adds what the first leaves out: state the server pushes.

ExampleWhat it teaches
counter
source · demo
The whole model with nothing live: a struct, a View method, handler methods, and on.Click wiring them. A click POSTs, the handler mutates a store, the page morphs.
go run ./snippet/src/examples/counter
pulse
source · demo
A via.State: state on the server, pushed over SSE by via.Ctx.Tick in OnInit. Its via.Meta styles the page off data-via-connection when the stream drops.
go run ./snippet/src/examples/pulse
counter/main.go
// Counter holds the injected dependency, not reactive state.
type Counter struct{ count *Store }

func (c *Counter) Inc(ctx *via.Ctx) { c.count.Add(1) }
func (c *Counter) Dec(ctx *via.Ctx) { c.count.Add(-1) }

func (c *Counter) View() h.H {
	return h.Div(
		h.H1(h.Str(c.count.Value())),
		h.Button(on.Click(c.Dec), h.Str("-")),
		h.Button(on.Click(c.Inc), h.Str("+")),
	)
}

The store is a plain dependency held by pointer, so every tab reads the same count; a second tab sees a change on its next click or reload, since nothing in counter streams.

pulse/main.go
// PageMeta carries the page's own stylesheet: Assets is CSP-governed, so
// declaring it here is what gets its sha256 into style-src.
func (Pulse) PageMeta() via.Meta {
	return via.Meta{
		Title:       "Server pulse",
		Description: "A live server pulse, pushed over SSE.",
		Assets: via.Assets{Styles: []via.Style{{
			Inline: `[data-via-connection="offline"] body{opacity:.5}` +
				`[data-via-connection="offline"] #pulse-state::after{content:" (disconnected)"}` +
				`[data-via-connection="connecting"] #pulse-state::after{content:" (reconnecting...)"}`,
		}}},
	}
}

via sets data-via-connection on <html> while the stream is connecting or offline, so one inline style is pulse's whole answer to a drop. The style is in via.Assets, so its sha256 is in the page's CSP.

#Live

ExampleWhat it teaches
chat
source · demo
Both topic shapes: via.Ctx.Listen appends each message to a via.List, via.State.Track follows the head-count. via.Ctx.OnConnect and via.Ctx.OnDispose count presence; via.Router.Close runs before Shutdown.
go run ./snippet/src/examples/chat
chat/main.go
// Chat is the mounted root, one instance per connected tab.
type Chat struct {
	room *Room

	Who    via.Signal[string] // round-trips so Send can author the message
	Draft  via.Signal[string] // two-way bound composer, cleared on send
	Log    via.List[Message]  // server-authoritative, pushed over SSE
	Online via.State[int64]   // presence count, pushed over SSE
}

func (c *Chat) OnInit(ctx *via.Ctx) error {
	ctx.Listen(c.room.bus, c.onMessage)
	c.Online.Track(ctx, c.room.presence, c.room.count)

	// Registered, not performed: OnInit runs on the plain GET as well as the
	// connect, and only a real connection gets an OnConnect/OnDispose pair.
	ctx.OnConnect(c.room.join) // tell everyone the head-count rose
	ctx.OnDispose(c.room.part) // …and that it fell when this tab leaves
	return nil
}

func (c *Chat) onMessage(ctx *via.Ctx, m Message) { c.Log.Append(m) }

The via.List is per tab and there is no history: a tab that joins late starts empty and sees only what is published after its stream subscribes.

#Apps

ExampleWhat it teaches
forum
source · demo
via.NewRouter and via.Mount across five pages: sessions, via.PostForm with via.Ctx.Redirect, via.Ctx.Param, an avatar upload, and via.WithErrorPage.
go run ./snippet/src/examples/forum
forum/main.go
func (p *ThreadPage) OnInit(ctx *via.Ctx) error {
	if _, ok := ctx.Session().Get[User](); !ok {
		ctx.Redirect("/login")
		return nil
	}
	p.id = ctx.Param[int]("id")
	return p.OnReload(ctx)
}

func (p *ThreadPage) OnReload(ctx *via.Ctx) error {
	p.subject, p.posts, p.found = p.store.thread(p.id)
	return nil
}

// Send needs no Redirect: OnReload re-reads the thread, so the reply is in
// the response render.
func (p *ThreadPage) Send(ctx *via.Ctx) {
	if !p.found {
		return
	}
	u, _ := ctx.Session().Get[User]()
	p.store.reply(p.id, u.Name, ctx.Request().FormValue("body"))
}

A page guards itself in OnInit: no session, redirect. OnReload re-reads the store after an action, so a reply is in the render that answers it without a redirect.