Every widget in BESPA is a Go struct that implements the Widget interface. In practice you never implement it from scratch — you embed WidgetBase[T] and write a Draw method.
The contract is six methods, but only one of them (Draw) is something you’re likely to write:
// widget/widget.go
type Widget interface {
ID() string
SetID(id string)
Children() []Widget
Draw(w io.Writer, r *http.Request) error
Drawn(r *http.Request) bool
Shown(r *http.Request) bool
}
Everything else has a sensible default in WidgetBase. ID/SetID plumb the partial-redraw protocol; Children defaults to nil (no children); Drawn and Shown are controlled by RedrawIfChanged / HideIf — see Extend → State-aware widgets.
The minimal widget — a Go struct, a constructor that wires up WidgetBase, optional builder methods, and a Draw:
type GreetingWidget struct {
*widget.WidgetBase[*GreetingWidget]
name string
}
func (f MyFactory) Greeting(name string) *GreetingWidget {
g := &GreetingWidget{name: name}
g.WidgetBase = widget.NewWidgetBase(g)
return g
}
func (g *GreetingWidget) WithName(name string) *GreetingWidget {
g.name = name
return g
}
func (g *GreetingWidget) Draw(w io.Writer, r *http.Request) error {
return widget.NewWriterAssistant(w).
WriteString("<span data-id=\"", g.ID(), "\">Hello, ", html.EscapeString(g.name), "!</span>").
Err()
}
Three things going on:
*widget.WidgetBase[*GreetingWidget] embed is generic — the type parameter tells WithFoo-style methods on the base what type to return. That’s how you can chain .WithName(...) on the concrete type.widget.NewWidgetBase(g) passes the concrete pointer back to the base so its WithID, RedrawIf, etc. can return *GreetingWidget, not *WidgetBase.Draw writes the widget’s HTML. Include data-id set to g.ID() on whatever element should be addressable by the partial-redraw swap — otherwise the framework can’t replace this widget in place.In practice almost no widget writes raw HTML. Use Tag — the framework’s tag builder that handles escaping, attribute merging, and conditional rendering:
func (g *GreetingWidget) Draw(w io.Writer, r *http.Request) error {
return factory.Tag("span").
Class("Greeting").
Attr("data-id", g.ID()).
Add("Hello, ", g.name, "!").
When(g.Shown(r)).
Draw(w, r)
}
This is the form you’ll see used by every widget under basic/ and form/. Tag("span").Add(g.name, ...) calls the framework’s Any coercion under the hood, which escapes plain strings; numbers, times, and other types convert to widgets too.
By convention each widget gets its own .go file plus optional .css and .js siblings — e.g. basic/heading.go + basic/heading.css. The factory file in the package’s root registers all the assets in one init(). See Extend → Assets & CSS for that pattern.
Composing existing widgets — for most cases you don’t need to write a Draw at all.
State-aware widgets — how Drawn / Shown / RedrawIfChanged interact with your Draw.
Read the source of basic/heading.go for a tiny worked example, or basic/menu.go for one that uses companion CSS and JS.