Examples
Nine programs under internal/example, each one main package with no dependency past via. Run them from 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
go run ./internal/example/counterOn this page
#First steps
Read these three in order. Each adds one idea to the one before.
| 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 ./internal/example/counter |
| greeting source · demo | A via.Signal: state in the browser. via.Signal.Bind and via.Signal.Display share one wire name, so typing updates the text with no request. via.Signal.Ref feeds h.DataShow.go run ./internal/example/greeting |
| 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 ./internal/example/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.
#Live
Pick by what you need to push.
| Example | What it teaches |
|---|---|
| shared source · demo | An app-owned value every tab follows. via.StateTrack on the field literal follows a topic.Topic, so the page has no OnInit at all.go run ./internal/example/shared |
| feed source · demo | via.Ctx.Listen on a topic.Topic: a server goroutine publishes and every connected tab lists each value, keeping the newest ten.go run ./internal/example/feed |
| dashboard source · demo | Several live units on one stream, each patching only itself. A canvas island fed by via.Signal.Set on a signal the View never binds, and a client-only via.SignalCS seeded by a via:"init=..." tag.go run ./internal/example/dashboard |
| 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 ./internal/example/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 also runs on the plain GET and on every
// action, 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 |
|---|---|
| poll source · demo | A list that re-sorts on every render. on.Bind carries each row's id, so a vote lands on the option clicked, wherever it now sits. h.DataAttr disables the add button while the draft is empty.go run ./internal/example/poll |
| 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 ./internal/example/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.