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/counterOn this page
#First steps
Read these two in order. The second adds what the first leaves out: state the server pushes.
| Example | What 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 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.
// 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
| Example | What 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 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
| Example | What 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 |
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.