via

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/counter
On this page

#First steps

Read these three in order. Each adds one idea to the one before.

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 ./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/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.

#Live

Pick by what you need to push.

ExampleWhat 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/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 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

ExampleWhat 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
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.