goxx

package module
v0.1.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 29, 2026 License: MIT Imports: 12 Imported by: 0

README

goxx

codecov Go Report Card Go Reference

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
  • HTMX attribute and handler helpers
  • 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.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewPrinter

func NewPrinter(w io.Writer, opts ...Option) gox.Printer

NewPrinter returns a GoX printer that renders gox.Comp and gox.Elem values with support for Parallel subtrees.

To run part of a template in parallel, proxy that fragment to ~>(goxx.Parallel()). NewPrinter schedules those marked fragments on its worker pool while the rest of the template continues rendering.

Output is buffered per parallel branch and drained to w in source order. By default the printer uses seven workers and gox.NewPrinter for sequential chunks. Use WithWorkers to tune or remove the worker limit. A negative worker count panics.

NewPrinter is useful when you want to pass a printer to gox.Elem.Print. In HTTP handlers, prefer Render: it returns buffered output first, so you can set headers and a custom success status after rendering succeeds and before the response body is written.

Use WriterError to check whether elem.Print failed because of the final io.Writer. Other render errors are returned before buffered output is written to w.

func Parallel

func Parallel() gox.Proxy

Parallel returns a proxy that schedules elem as a parallel subtree when it is rendered by NewPrinter.

Use it for independent fragments that may wait on database queries, external API calls, or other slow work.

Output order stays the same as the template order. When used with another printer, the subtree renders sequentially and logs a misuse warning.

func ProxyMod

func ProxyMod(mod gox.Modify) gox.Proxy

ProxyMod returns a proxy that applies mod to the first element rendered by elem.

Use it to build helpers that attach attributes or attribute modifiers to another element or component. If elem starts with a component or container, ProxyMod carries mod forward until it reaches the first element. Text or other non-element output before that element is an error. The modifier is applied once; later sibling elements are left unchanged.

Parallel markers are preserved, so the wrapped subtree can still be scheduled by NewPrinter.

func WriterError

func WriterError(err error) (error, bool)

WriterError returns the underlying writer error from err.

It reports false when err is not, and does not wrap, a WriteErr.

Types

type Classes

type Classes struct {
	// contains filtered or unexported fields
}

Classes describes class names to add and filter out.

Classes values are immutable: Add, Remove, Filter, Join, and Clone return a new value and leave the receiver unchanged.

func Class

func Class(classes ...string) Classes

Class builds a class modifier from one or more class strings.

Each argument is split with strings.Fields, so Class("a", "b c") and Class("a b c") produce the same classes. The returned value can be used as a class attribute value, as an attribute modifier, or as a proxy before an element/component.

func (Classes) Add

func (c Classes) Add(classes ...string) Classes

Add returns a new Classes value with classes appended.

Arguments are split like Class, so Add("a", "b c") adds three classes.

func (Classes) Clone

func (c Classes) Clone() Classes

Clone returns an independent copy of c.

func (Classes) Filter added in v0.1.4

func (c Classes) Filter(classes ...string) Classes

Filter returns a new Classes value that omits matching classes from output.

Removed classes are filtered regardless of whether they were added before or after Filter was called.

func (Classes) Join

func (c Classes) Join(classes ...Classes) Classes

Join returns a new Classes value that combines several class modifiers.

Both added and filtered class names are preserved, so filters from joined values still affect the final rendered class list.

func (Classes) Modify

func (c Classes) Modify(ctx context.Context, tag string, atts gox.Attrs) error

func (Classes) Mutate

func (c Classes) Mutate(name string, prev any) any

func (Classes) Output

func (c Classes) Output(w io.Writer) error

func (Classes) Proxy

func (c Classes) Proxy(cur gox.Cursor, el gox.Elem) error

func (Classes) Remove

func (c Classes) Remove(classes ...string) Classes

Remove returns a new Classes value with matching currently-added classes removed.

Removed classes are not remembered: the same class can be added again later. Use Filter when matching classes should be omitted from final output even if they are added later or come from a joined Classes value.

func (Classes) String

func (c Classes) String() string

String returns the class list as it would be rendered in a class attribute.

type CompParallel

type CompParallel gox.Elem

CompParallel marks an Elem for parallel rendering by NewPrinter.

Prefer Parallel in templates and component code. CompParallel is exported so printer and proxy integrations can preserve the marker when wrapping or forwarding components.

func (CompParallel) Main

func (p CompParallel) Main() gox.Elem

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option configures a printer created by NewPrinter.

func WithFlat added in v0.1.3

func WithFlat() Option

WithFlat makes NewPrinter expand ordinary component jobs before they reach the base printer.

Use it with WithPrinter when your custom printer wants to handle the actual content stream and does not want to render or inspect gox.JobComp values itself. Parallel markers are still handled by NewPrinter.

func WithPrinter added in v0.1.3

func WithPrinter(f func(w io.Writer) gox.Printer) Option

WithPrinter lets you add your own printer extension to the rendering pipeline.

NewPrinter calls f for each sequential chunk or parallel branch, passing the branch-local buffer that printer should write to. Use WithFlat if your printer wants expanded content instead of gox.JobComp values.

By default, NewPrinter uses gox.NewPrinter, which renders jobs directly to the provided io.Writer.

func WithWorkers added in v0.1.3

func WithWorkers(n int) Option

WithWorkers sets the maximum number of parallel background worker tasks.

The default is seven background workers, plus the caller goroutine, for eight render tasks in total. Passing zero skips the worker pool and starts plain goroutines for parallel tasks. Passing a negative value causes NewPrinter to panic.

type WriteErr

type WriteErr struct {
	// Err is the underlying writer error.
	Err error
}

WriteErr wraps an error returned by the final io.Writer.

It is returned after rendering has succeeded and NewPrinter is draining its buffered output to the writer passed to NewPrinter.

func (WriteErr) Error

func (we WriteErr) Error() string

Error returns the underlying writer error message.

func (WriteErr) Unwrap

func (we WriteErr) Unwrap() error

Unwrap returns the underlying writer error.

type WriterToCloser added in v0.1.3

type WriterToCloser interface {
	io.WriterTo
	io.Closer
}

WriterToCloser is buffered printer output returned by Render.

WriteTo writes the buffered output once and releases it. After WriteTo or Close, later WriteTo calls return an error. Close releases the output without writing it; call Close when you decide not to write the rendered output. Close is safe to call more than once.

func Render added in v0.1.3

func Render(ctx context.Context, comp gox.Comp, opts ...Option) (WriterToCloser, error)

Render renders comp into buffers and returns the buffered output.

Render is the recommended entry point for HTTP handlers. It gives the same parallel rendering behavior as NewPrinter: fragments marked with ~>(goxx.Parallel()) are scheduled on the worker pool, and their buffered output is kept in template order. Rendering finishes before anything is written to the http.ResponseWriter. If rendering succeeds, set headers or a custom success status and then call WriteTo. If rendering fails, no output is returned, so the handler can still send an error response.

Check context.Canceled and context.DeadlineExceeded separately: they mean the render context ended before rendering finished. If Render succeeds but WriteTo fails, that error came from the final writer.

Example (HttpHandler)
package main

import (
	"context"
	"errors"
	"log/slog"
	"net/http"

	"github.com/doors-dev/gox"
	"github.com/doors-dev/goxx"
)

func main() {
	page := gox.Elem(func(cur gox.Cursor) error {
		return cur.Text("ok")
	})

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		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.WriteHeader(http.StatusOK)

		if _, err := out.WriteTo(w); err != nil {
			slog.Warn("response write failed", "err", err)
		}
	})

	_ = handler
}

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL