A signal is a Datastar value that lives in the browser, declared by a field of your composition. Bind it to an input, display it, compute over it with expr.Expr expressions: none of that sends a request. The server writes one with a Set when it has something to say.
typeChatstruct{Draftvia.Signal[string]// $chat__draft}typeInboxstruct{Countvia.Signal[int]// $countOpenvia.SignalCS[bool]// $_openFormstruct{Emailvia.Signal[string]// $form_email}ChatChat// rendered with via.Child(p.Chat)}
A signal's wire name is its Go field name with the first letter lower-cased. A plain nested struct joins with one underscore, a child rendered with via.Child prefixes its field name and a double underscore, and a via.SignalCS name starts with an underscore. via.Signal.Ref returns the name as an expression, so you never spell $chat__draft by hand; renaming the field renames the signal.
via.Signal.Bind and via.Signal.Display are two views of one field, so they share its wire name whatever order they render in. Typing reaches no handler: the browser updates the text on its own.
Hello, !
The greeting appears once you type.
Try this
Type a name. The greeting follows as you type.
Watch the Wire pane: the signal changes with each keystroke and no request appears.
Wire
Use the demo to see its requests and patches here.
Source
packagedemosimport("github.com/go-via/via""github.com/go-via/via/h")// Greeting is two views of one signal: Bind() and Display() resolve to the// same wire name, so the sentence follows the input with no request at all.typeGreetingstruct{Namevia.Signal[string]}func(g*Greeting)View()h.H{returnh.Div(h.Label(h.Str("Your name"),h.Input(g.Name.Bind(),h.Placeholder("type here"),h.AutoComplete("off")),),h.P(h.DataShow(g.Name.Ref().Ne("")),h.Str("Hello, "),g.Name.Display(),h.Str("!")),h.P(h.DataShow(g.Name.Ref().Eq("")),h.Small(h.Str("The greeting appears once you type."))),)}
A via:"init=…" tag gives the start value as JSON, so a string is quoted. The source, then the HTML it renders:
data-signals on the root declares the value, data-bind makes the input write it, and data-text shows it. The span carries the value as text too, so the first paint reads right before Datastar loads. When an action runs, the browser posts its signals back, and via.Signal.Get returns the posted value of a Bound one.
via.SignalCS is for UI state the server has no use for: an open panel, the active tab. Its name starts with an underscore, which Datastar's fetch filter drops, so it never reaches a POST. The tag gives it a start value, and the server can Set it, from OnInit or an action, but never read it: it has no Get.
Shown by data-show, not by a re-render. The server has no field for this and no handler ran.
Try this
Click the button to show and hide the panel.
Open the Wire pane: _toggle__open flips and no request appears. The flag and the handler both live in the browser.
Wire
Use the demo to see its requests and patches here.
Source
packagedemosimport("github.com/go-via/via""github.com/go-via/via/h""github.com/go-via/via/on")// Toggle holds a panel open or closed with a SignalCS, whose wire name is// _-prefixed — Datastar's fetch filter drops those, so the flag never leaves// the browser. The tag decides what it starts as at first paint.typeTogglestruct{Openvia.SignalCS[bool]`via:"init=false"`}func(t*Toggle)View()h.H{returnh.Div(h.Button(on.ClickCS(t.Open.Ref().Toggle()),h.Str("open: "),t.Open.Display()),h.Div(h.Class("panel"),h.DataShow(t.Open.Ref()),h.P(h.Str("Shown by data-show, not by a re-render. The server has no field "+"for this and no handler ran.")),),)}
via.Signal.Set writes the value and declares the signal, so the View need not render it. A Set in OnInit puts the server clock in the first paint, and the action sets it again. The action's response carries the signals it wrote and any the new render added, never one it left alone, so an input the user is editing is not overwritten.
Try this
Click read the clock and open the Wire pane: one POST, and a patch carrying the new signal value.
Wire
Use the demo to see its requests and patches here.
Source
packagedemosimport("time""github.com/go-via/via""github.com/go-via/via/h""github.com/go-via/via/on")// Seed never binds or displays its signal: Set is what declares one, so the// server clock below reaches the browser on first paint from OnInit, and the// action re-Sets the same wire name. No element here holds the time as text —// it rides on the region's signals and data-text reads it.typeSeedstruct{Clockvia.Signal[string]}func(s*Seed)OnInit(ctx*via.Ctx)error{s.Clock.Set(serverTime())returnnil}func(s*Seed)Refresh(ctx*via.Ctx){s.Clock.Set(serverTime())}func(s*Seed)View()h.H{returnh.Div(h.Class("row"),h.Button(on.Click(s.Refresh),h.Str("read the clock")),h.Output(h.DataText(s.Clock.Ref())),)}funcserverTime()string{returntime.Now().UTC().Format("15:04:05")+" UTC"}
A JavaScript island gets data from Go the same way: Set a signal the island reads, and no element has to spell the value out. Declaring is not binding: the server ignores a posted value for a Set signal unless something Binds it.
Ref hands a signal to the expr package, and the h.Data* attributes take the expression it builds. The sum and the class on it derive from the same two refs, and the browser re-evaluates both on every keystroke.
A + B = (turns amber above 10)
Try this
Raise A or B until the sum passes 10; it turns amber.
Open the Wire pane: the signals change and no request appears. A Bound signal is sent only when an action runs.
Wire
Use the demo to see its requests and patches here.
Source
packagedemosimport("github.com/go-via/via""github.com/go-via/via/expr""github.com/go-via/via/h")// Computed derives from two signals in the browser. The sum and the class on// it are expressions over the same two refs, re-evaluated on every keystroke.typeComputedstruct{Avia.Signal[int]`via:"init=2"`Bvia.Signal[int]`via:"init=3"`}// expr has no arithmetic operators (Add is the in-place +=), so the sum is// Rawf text with the two refs spliced in checked rather than spelled by hand.func(c*Computed)sum()expr.Expr{returnexpr.Rawf("(%s + %s)",c.A.Ref(),c.B.Ref())}func(c*Computed)View()h.H{sum:=c.sum()returnh.Div(h.Label(h.Str("A"),h.Input(h.Type("number"),c.A.Bind())),h.Label(h.Str("B"),h.Input(h.Type("number"),c.B.Bind())),h.P(h.Str("A + B = "),h.Span(h.Class("total"),h.DataText(sum),h.DataClass("over",sum.Gt(10))),h.Small(h.Str(" (turns amber above 10)")),),)}
An expr.Expr goes into any h.DataShow-style attribute or an on.ClickCS-style handler. The right-hand column is what each helper produces, rendered from these calls:
expr.CopyTextOf copies the text of an element next to the button, read from the page at click time. expr.Class flags the button for a moment: feedback that needs no signal. The clipboard works only in a secure context and from a click. A refused write is swallowed, so it fails with nothing in the console.
go get github.com/go-via/via
Try this
Click Copy, then paste somewhere.
Open the Wire pane: no request appears. Copying never involves the server.
Wire
Use the demo to see its requests and patches here.
Source
packagedemosimport("time""github.com/go-via/via/expr""github.com/go-via/via/h""github.com/go-via/via/on")// SignalsCopy copies the command beside its button and flags the button for a// moment. The class is feedback not worth a signal, so it has none, and the// text is read off the DOM instead of repeated in the attribute.typeSignalsCopystruct{}func(c*SignalsCopy)View()h.H{returnh.Div(h.Class("code"),h.Pre(h.Code(h.Str("go get github.com/go-via/via"))),h.Button(h.Class("copy"),h.Type("button"),on.ClickCS(expr.Do(expr.CopyTextOf("pre"),expr.Class("copied",true))),on.ClickCS(expr.Class("copied",false),on.WithDebounce(1500*time.Millisecond)),h.Span(h.Class("icon-clip"),h.Str("Copy")),h.Span(h.Class("copy-done"),h.Aria("live","polite"),h.Str("Copied")),),)}