Skip to content

Package renderers ​

Some database and framework types carry both a value and application policy. pgtype.Timestamptz, for example, can represent SQL NULL, finite timestamps, and positive or negative infinity. A package renderer keeps the policy for those states in one application-owned function while letting every consumer render the value directly.

This pattern puts that function in a small .gsx package. Returning gsx.Node lets the renderer own semantic markup as well as text formatting.

The recipe ​

1. Define the renderer in .gsx ​

Create ds/renderers/timestamptz.gsx:

gsx
package renderers

import (
	"time"

	"github.com/gsxhq/gsx"
	"github.com/jackc/pgx/v5/pgtype"
)

func Timestamptz(v pgtype.Timestamptz) gsx.Node {
	if !v.Valid {
		return <></>
	}
	switch v.InfinityModifier {
	case pgtype.Infinity:
		return <time>infinity</time>
	case pgtype.NegativeInfinity:
		return <time>-infinity</time>
	default:
		return <time datetime={v.Time.Format(time.RFC3339)}>{v.Time.Format(time.DateTime)}</time>
	}
}

Each branch makes an application decision:

  • An invalid value, representing SQL NULL, renders no node.
  • Positive and negative infinity get explicit labels, without pretending they have a finite machine-readable timestamp.
  • A finite value uses RFC 3339 for the <time datetime> value and time.DateTime for its visible label.

2. Register the type and function ​

Map the exact pgx type to the renderer's fully qualified function in gsx.toml:

toml
[renderers]
"github.com/jackc/pgx/v5/pgtype.Timestamptz" = "example.com/app/ds/renderers.Timestamptz"

Replace example.com/app with your module path. Renderer registrations apply module-wide, so consumers do not import or call this function themselves.

3. Generate the renderer package ​

Generate the package like any other .gsx package:

bash
go tool gsx generate ./ds/renderers

This command works from a clean checkout: because the configured renderer target is inside the active module, gsx bootstraps its declarations directly from the local .gsx source. It does not require a pre-existing .x.go file or a Go companion file.

That source bootstrap is deliberately module-local. If a configured renderer target is outside the active module, its package is loaded as an external Go dependency and must already be buildable — including any generated Go it needs. Generate and publish that package before consuming it from another module.

4. Render the value directly ​

Consumers write the typed value exactly where it belongs:

gsx
component AuditRow(row AuditRecord) {
	<tr>
		<td>{row.CreatedAt}</td>
	</tr>
}

The registration makes gsx call renderers.Timestamptz at that interpolation; the consumer stays free of validity checks, formatting calls, and wrapper components.

Keep policy in the application ​

The recipe is a starting policy, not a hidden pgx default. Your application owns what SQL NULL means on the page, how both infinity values are presented, which timezone a finite value uses, the machine-readable datetime value, and the visible label. Change the renderer deliberately when any of those decisions change; every direct consumer then follows the same policy.

For the complete registration contract — supported signatures, contextual escaping, and exact type matching — see [renderers] in the configuration guide.