goxx
May 7, 2026 ยท View on GitHub
goxx is an extension package for github.com/doors-dev/gox.
GoX itself is intentionally minimal. goxx adds a few more specific,
but still common, rendering helpers on top:
- a parallel printer for independent slow template fragments
- composable class helpers
- small proxy utilities for building attribute-oriented helpers
Suggestions for extending this package are welcome. If you need another common helper, please create an issue.
Doors Compatibility
goxx is not fully compatible with github.com/doors-dev/doors.
When you are building a Doors app, prefer helpers from Doors itself or from the
GoX core package unless you know this package fits your rendering pipeline.
Install
go get github.com/doors-dev/goxx
Printer
Parallel Rendering
Use goxx.Render in HTTP handlers, or goxx.NewPrinter when an API expects a
printer. Mark independent fragments with ~>(goxx.Parallel()).
func handlePage(w http.ResponseWriter, r *http.Request) {
out, err := goxx.Render(r.Context(), Page())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
if _, err := out.WriteTo(w); err != nil {
slog.Warn("response write failed", "err", err)
}
}
elem Page() {
<main>
<h1>Dashboard</h1>
~>(goxx.Parallel()) <section>
~(SlowStats())
</section>
~>(goxx.Parallel()) <aside>
~(SlowSidebar())
</aside>
</main>
}
Use Parallel for fragments that can wait independently, such as database
queries, external API calls, filesystem reads, or expensive calculations. Output
order stays the same as the template order, even if a later branch finishes
first.
By default, NewPrinter uses seven background workers plus the caller goroutine.
// Use 16 background workers.
printer := goxx.NewPrinter(w, goxx.WithWorkers(16))
// Use plain goroutines instead of a bounded worker pool.
printer = goxx.NewPrinter(w, goxx.WithWorkers(0))
Printer Extensions
WithPrinter lets you add your own printer to the pipeline. It is a factory
because parallel rendering writes each branch to its own buffer.
printer := goxx.NewPrinter(w, goxx.WithPrinter(func(w io.Writer) gox.Printer {
return MyPrinter(w)
}))
If your custom printer wants expanded content instead of *gox.JobComp values,
use WithFlat.
printer := goxx.NewPrinter(
w,
goxx.WithFlat(),
goxx.WithPrinter(func(w io.Writer) gox.Printer {
return MyPrinter(w)
}),
)
HTTP And Error Handling
Render buffers output and returns it before anything is written to the final
io.Writer. This is the recommended shape for HTTP handlers: if rendering
fails, you can still send an error response; if it succeeds, you can set headers
and choose a custom success status before writing the body.
Check context cancellation separately. It usually means the client went away or the request deadline expired before rendering finished.
out, err := goxx.Render(r.Context(), Page())
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
slog.Debug("render stopped before completion", "err", err)
return
}
if err != nil {
http.Error(w, "render failed", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
if _, err := out.WriteTo(w); err != nil {
slog.Warn("response write failed", "err", err)
}
For non-HTTP code that passes NewPrinter directly to elem.Print,
WriterError detects errors from the final writer:
if err := Page().Print(ctx, goxx.NewPrinter(dst)); err != nil {
if err, ok := goxx.WriterError(err); ok {
slog.Warn("write failed", "err", err)
}
}
Class Helpers
Class builds immutable class modifiers. Inputs are split with strings.Fields,
so variadic and space-separated forms are equivalent.
goxx.Class("button", "primary")
goxx.Class("button primary")
goxx.Class("button hidden").Remove("hidden").Add("primary")
You can use Classes as an attribute modifier:
elem Button() {
<button (goxx.Class("button primary")) class="wide">Save</button>
}
Or as a class attribute value:
elem Button() {
<button class=(goxx.Class("button", "primary"))>Save</button>
}
Or as a proxy. The class modifier propagates through components and containers until it reaches the first real element:
elem Button() {
~>(goxx.Class("button primary")) <button>Save</button>
}
Remove edits the current class list. The removed class can be added again
later:
goxx.Class("button hidden").Remove("hidden").Add("hidden").String() // "button hidden"
Filter is useful when wrapping a component that already has a class:
elem BaseButton() {
<button class="button disabled">Save</button>
}
elem EnabledButton() {
~>(goxx.Class("primary").Filter("disabled")) ~(BaseButton())
}
Filter omits matching classes no matter whether they were added before or
after the filter:
goxx.Class("button hidden").Filter("hidden").String() // "button"
goxx.Class("button").Filter("hidden").Add("hidden").String() // "button"
Proxy Helpers
ProxyMod is useful when building helpers that attach an attribute modifier to
another element or component. It carries the modifier through leading components
or containers until the first real element, applies it once, and leaves later
siblings unchanged.
One practical use is adding test or integration attributes to components without adding those attributes to every component API:
func TestID(id string) gox.Proxy {
return goxx.ProxyMod(gox.ModifyFunc(func(_ context.Context, _ string, attrs gox.Attrs) error {
attrs.Get("data-testid").Set(id)
return nil
}))
}
elem SaveButton() {
<button class="button">Save</button>
}
elem Toolbar() {
~>(TestID("save-button")) ~(SaveButton())
}
goxx.Class(...).Proxy(...) is built on this behavior.