ui

package module
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Jul 2, 2026 License: MIT Imports: 18 Imported by: 0

README

gas-ui

Test Go Reference Go Version License

Template rendering, static file serving, and UI infrastructure for the Gas ecosystem. Implements gas.UIProvider on top of a pluggable gas.TemplateProvider backend — filesystem, database, memory, or composite (see gas-template).

What It Does

gas-ui is a Gas service that provides:

  • Template rendering using Go's html/template with layout/partial support
  • Static file serving with directory listing blocked
  • Pluggable template backends via gas.TemplateProvider — filesystem, database, memory, or composite (see gas-template)
  • Template function registration — other services can contribute helpers via RegisterFuncs
  • gas.UIProvider interface — other services accept this for rendering without importing gas-ui

Installation

go get github.com/gasmod/gas-ui

Quick Start

package main

import (
	"github.com/gasmod/gas"
	tmpl "github.com/gasmod/gas-template/fs"
	ui "github.com/gasmod/gas-ui"
)

func main() {
	app := gas.NewApp(
		gas.WithSingletonService[gas.TemplateProvider](tmpl.NewStore("templates")),
		gas.WithSingletonService[*ui.Service](
			ui.New[gas.TemplateProvider](ui.WithConfig(&ui.Config{
				UI: ui.Settings{
					StaticDir:  "static",
					StaticPath: "/static/*",
					LayoutName: "base",
				},
			})),
		),
	)
	app.Run()
}

Template Directory Convention

templates/
  layouts/
    base.html           <-- parsed into every render, defines the entry point
  partials/
    header.html         <-- parsed into every render, available via {{template}}
    footer.html
  home.html             <-- page template, rendered with Render("home", data)
  about.html
  dashboard/
    index.html          <-- nested pages work too: Render("dashboard/index", data)
  • layouts/ — Base layout templates. Define the HTML skeleton with {{define "base"}} and declare blocks with {{block "content" .}}.
  • partials/ — Reusable fragments. Define named templates with {{define "nav"}} and use them anywhere with {{template "nav" .}}.
  • Everything else — Page templates. Override blocks defined in the layout.

Template Examples

layouts/base.html

{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{block "title" .}}My App{{end}}</title>
    <link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
{{template "nav" .}}
<main>{{block "content" .}}{{end}}</main>
{{template "footer" .}}
</body>
</html>
{{end}}

partials/nav.html

{{define "nav"}}
<nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
</nav>
{{end}}

partials/footer.html

{{define "footer"}}
<footer>&copy; 2026 {{.SiteName}}</footer>
{{end}}

home.html

{{define "title"}}Home - My App{{end}}

{{define "content"}}
<h1>Welcome, {{.Name}}!</h1>
<p>{{.Message}}</p>
{{end}}

Rendering in a handler:

func (s *Service) handleHome(w http.ResponseWriter, r *http.Request) {
	s.ui.Render(w, "home", map[string]any{
		"SiteName": "My App",
		"Name":     "Ahmed",
		"Message":  "Hello from Gas.",
	})
}

Rendering Without a Layout

Page templates that don't use {{define}} blocks render standalone — no layout wrapping:

widget.html


<div class="widget">{{.Label}}: {{.Value}}</div>
uiSvc.Render(w, "widget", map[string]any{"Label": "Count", "Value": 42})
// Output: <div class="widget">Count: 42</div>

HTMX Fragment Rendering

RenderFragment renders a page template without the layout wrapper. Useful for HTMX partial responses:

// Full page render (with layout):
uiSvc.Render(w, "users/list", data)

// Fragment render (no layout, just the page content):
uiSvc.RenderFragment(w, "users/list", data)

Passing gas-ui to Other Services

gas-ui satisfies the gas.UIProvider interface. Other services receive it through DI constructor injection without importing gas-ui:

// gas-blog receives gas.UIProvider via DI — no import of gas-ui needed
type Service struct {
	ui gas.UIProvider
}

func New(ui gas.UIProvider) *Service {
	return &Service{ui: ui}
}

If your service needs both standard rendering and fragment rendering, gas.UIProvider supports both:

func (s *Service) handleHTMX(w http.ResponseWriter, r *http.Request) {
    if r.Header.Get("HX-Request") == "true" {
        s.ui.RenderFragment(w, "content", data)
        return
    }
    s.ui.Render(w, "content", data)
}

Registration:

app := gas.NewApp(
	gas.WithSingletonService[gas.TemplateProvider](tmpl.NewStore("templates")),
	gas.WithSingletonService[*ui.Service](
		ui.New[gas.TemplateProvider](),
	),
	gas.WithSingletonService[*blog.Service](
		blog.New,
	),
)

Inside gas-blog:

func (s *Service) handlePost(w http.ResponseWriter, r *http.Request) {
	post := s.getPost(r)
	s.ui.Render(w, "blog/post", map[string]any{
		"Title": post.Title,
		"Body":  post.Body,
	})
}

Registering Template Functions from Other Services

Services can contribute template helpers via RegisterFuncs. Templates are built lazily on the first render, so funcs registered during Init() are available.

// gas-blog/service.go
func (s *Service) Init() error {
	s.ui.RegisterFuncs(template.FuncMap{
		"formatDate": func(t time.Time) string {
			return t.Format("January 2, 2006")
		},
		"markdownToHTML": s.renderMarkdown,
	})
	return nil
}

Templates can now use them:

{{define "content"}}
<article>
    <time>{{formatDate .PublishedAt}}</time>
    {{safe (markdownToHTML .Body)}}
</article>
{{end}}

Registering Templates from Other Services

Services can contribute their own templates — layouts, partials, and pages — by registering them with the gas.TemplateProvider. Templates are classified by name prefix: layouts/ for layouts, partials/ for partials, everything else is a page.

Single template
tp.Register("partials/blog-card.html", []byte(`{{define "blog-card"}}...{{end}}`))
tp.Register("blog/post.html", []byte(`{{define "content"}}...{{end}}`))

Services can embed their templates with //go:embed and register the entire tree via the template provider:

// gas-blog/service.go
package blog

import (
	"embed"
	"io/fs"

	"github.com/gasmod/gas"
)

//go:embed templates
var blogTemplates embed.FS

type Service struct {
	tp gas.TemplateProvider
	ui gas.UIProvider
}

func (s *Service) Init() error {
	sub, _ := fs.Sub(blogTemplates, "templates")
	s.tp.RegisterFS(sub)
	return nil
}

With a directory like:

gas-blog/
  templates/
    partials/
      blog-card.html    <-- available as {{template "blog-card" .}} in all templates
    blog/
      post.html         <-- renderable with Render("blog/post", data)
      index.html

Embedded Static Files

By default, static files are served from a directory on disk (UI.StaticDir). To serve embedded static files — for example when shipping a single binary — use WithStaticFS:

package main

import (
	"embed"
	"io/fs"

	"github.com/gasmod/gas"
	ui "github.com/gasmod/gas-ui"
)

//go:embed static
var staticFiles embed.FS

func main() {
	sub, _ := fs.Sub(staticFiles, "static")

	app := gas.NewApp(
		// ... template provider registration ...
		gas.WithSingletonService[*ui.Service](
			ui.New[gas.TemplateProvider](ui.WithStaticFS(sub)),
		),
	)
	// ...
}

When WithStaticFS is set, UI.StaticDir is ignored and its directory validation is skipped.

You can also use StaticHandlerFS directly if you need a standalone handler without the service:

mux.Handle("/static/", ui.StaticHandlerFS("/static/", sub))

Template Provider

gas-ui receives its template backend via DI as a gas.TemplateProvider. Template storage and retrieval is fully decoupled from rendering. Implementations are provided by the gas-template package:

  • fs.Store — filesystem-backed, sandboxed via os.Root
  • memory.Store — pure in-memory
  • db.Store — database-backed (PostgreSQL, MySQL, SQLite)
  • composite.Store — chains multiple providers

To use a custom template provider, implement gas.TemplateProvider and register it in the DI container. The type parameter on New[T] ensures the DI container resolves the correct concrete type:

gas.WithSingletonService[*MyCustomStore](NewMyCustomStore),
gas.WithSingletonService[*ui.Service](ui.New[*MyCustomStore]()),

Built-in Template Functions

Function Signature Description
safe (string) HTML Mark string as trusted HTML
safeAttr (string) HTMLAttr Mark string as trusted attribute
safeURL (string) URL Mark string as trusted URL
upper (string) string Uppercase
lower (string) string Lowercase
title (string) string Title case
trimSpace (string) string Trim whitespace
contains (s, substr) bool String contains
hasPrefix (s, prefix) bool String has prefix
hasSuffix (s, suffix) bool String has suffix
replace (s, old, new) string Replace all occurrences
join ([]string, sep) string Join strings
split (s, sep) []string Split string
truncate (n int, s string) string Truncate to n chars with ...
now () time.Time Current time
formatTime (layout, time) string Format a time value
formatTimePtr (layout, *time.Time) string Format a pointer to time; returns "" if nil
add (a, b int) int Addition
sub (a, b int) int Subtraction
dict (pairs ...any) map Create a map from key-value pairs
list (items ...any) []any Create a slice
json (any) json.RawMessage Marshal to JSON
buildId () string Stable build ID; fresh UUID per call in dev

dict is especially useful for passing data to partials:

{{template "user-card" dict "Name" .UserName "Role" "admin"}}

Configuration

ui.Config{
	FuncMap template.FuncMap  // Additional template functions
	UI      ui.Settings{
		StaticDir       string    // Root directory for static files (empty = disabled)
		StaticPath      string    // URL route pattern for static assets (default: "/static/*")
		StaticPaths     []string  // Multiple URL route patterns; overrides StaticPath when set
		StaticStripPrefix string  // URL prefix to strip before file lookup (empty = no stripping)
		LayoutName      string    // Entry-point template name (default: "base")
	}
}
Static file serving configuration

Static file serving has three independent concerns:

Setting Purpose Example
StaticDir (or WithStaticFS) What to serve — the source directory or FS "static"
StaticPath / StaticPaths Where to serve — the URL route pattern(s) "/static/*", ["/css/*", "/js/*"]
StaticStripPrefix What to strip — prefix removed before file lookup "/static/"
  • StaticPath registers a single route. StaticPaths registers multiple routes. When StaticPaths is set, StaticPath is ignored.
  • If both StaticPath and StaticPaths are provided, only StaticPaths is used.
  • StaticStripPrefix controls http.StripPrefix. When empty (the default), no prefix is stripped — the request URL path is used as-is to look up files in the static directory or FS.

Examples:

// FS contains: css/main.css, js/app.js
// Serve at /css/* and /js/*, no stripping needed (FS mirrors URL structure)
ui.Settings{
    StaticPaths: []string{"/css/*", "/js/*"},
}

// FS contains: main.css, app.js (flat, no subdirectories)
// Serve at /static/*, strip "/static/" so "/static/main.css" looks up "main.css"
ui.Settings{
    StaticPath:        "/static/*",
    StaticStripPrefix: "/static/",
}

// FS contains: main.css, app.js (flat)
// Serve at /assets/css/* and /assets/js/*, strip "/assets/" so
// "/assets/css/main.css" looks up "css/main.css"
ui.Settings{
    StaticPaths:       []string{"/assets/css/*", "/assets/js/*"},
    StaticStripPrefix: "/assets/",
}

Dev mode is driven by the GasEnv embedded field (from gas-config). When GasEnv is set to a development-like environment, templates are rebuilt on every request so changes are picked up immediately.

Test Mock

The uitest package provides MockUI, a configurable mock of gas.UIProvider for unit tests. Each method delegates to its Fn field if set, otherwise is a no-op that returns the zero value without touching the ResponseWriter. All calls are recorded for assertions and the mock is safe for concurrent use.

import "github.com/gasmod/gas-ui/uitest"

mock := &uitest.MockUI{}
mock.RenderFn = func(w http.ResponseWriter, name string, data any) error {
    w.Write([]byte("<h1>Hello</h1>"))
    return nil
}

// inject mock as gas.UIProvider in tests
svc := blog.New(mock)
svc.handlePost(rec, req)

mock.CallCount("Render") // 1
mock.Calls[0].Args       // ["blog/post", map[...]]
mock.Reset()             // clear recorded calls

DI Registration

app := gas.NewApp(
	gas.WithSingletonService[gas.TemplateProvider](tmpl.NewStore("templates")),
	gas.WithSingletonService[*ui.Service](
		ui.New[gas.TemplateProvider](ui.WithConfig(&ui.Config{
			UI: ui.Settings{
				StaticDir: "static",
			},
		})),
	),
)

The New[T] function is generic over the template provider type and returns a curried DI constructor. The container injects the template provider, *gas.Router, *gas.EventBus, gas.ConfigProvider, and gas.Logger automatically. Configuration options like WithConfig and WithStaticFS are captured in the outer closure.

The service logs structured diagnostics throughout its lifecycle: errors at each Init failure point, a DEBUG message listing all registered template function names on successful init, and WARN messages when a function name collision is detected via RegisterFuncs or when RegisterFuncs is called before the engine is ready.

Readiness

*Service implements gas.ReadyReporter. CheckReady returns an error before Init completes (template engine not built) or after Close is called (draining), and nil otherwise — suitable for a Kubernetes readinessProbe. gas.HealthReporter is intentionally not implemented, since the service has no external state a process restart would recover.

Documentation

Overview

Package ui provides HTML template rendering, static file serving, layouts, partials, and HTMX fragment rendering. Implements gas.UIProvider.

See the module README for usage examples and design rationale.

SPDX-License-Identifier: MIT

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultFuncMap

func DefaultFuncMap(e env.Environment) template.FuncMap

DefaultFuncMap returns the template functions available in every template.

func New

New constructs a gas-ui service. It captures configuration options and returns a DI-injectable constructor that receives infrastructure deps. The type parameter T allows users to provide a custom TemplateProvider implementation so the DI container resolves the correct concrete type.

func StaticHandler

func StaticHandler(prefix, dir string) http.HandlerFunc

StaticHandler returns an http.HandlerFunc that serves static files from dir under the given URL path prefix (e.g. "/static/").

func StaticHandlerFS

func StaticHandlerFS(prefix string, fsys fs.FS) http.HandlerFunc

StaticHandlerFS returns an http.HandlerFunc that serves static files from the given fs.FS under the given URL path prefix. This is useful for serving files embedded via Go's embed package.

Types

type Config

type Config struct {
	// Embedded GasEnv
	env.WithGasEnv

	// FuncMap supplies additional template functions. These are merged on
	// top of the built-in defaults; collisions override the default.
	FuncMap template.FuncMap

	UI Settings
}

Config holds all user-facing settings for the gas-ui service.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns a Config with sensible defaults.

func (*Config) Validate

func (c *Config) Validate(hasStaticFS bool) error

Validate checks that the Config fields are valid. hasStaticFS should be true if a custom fs.FS has been provided for static files, in which case UIStaticDir is not required.

type Engine

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

Engine compiles and caches templates from a gas.TemplateProvider.

Template directory convention:

layouts/   — base layout templates (parsed into every page render)
partials/  — reusable fragments (parsed into every page render)
*          — page templates (each one combined with layouts + partials)

A layout file typically defines an entry point template (default name "base") using {{define "base"}} and declares blocks with {{block "content" .}}. Page templates override those blocks with {{define "content"}}.

func NewEngine

func NewEngine(provider gas.TemplateProvider, funcMap template.FuncMap, layout string, devMode bool, logger gas.Logger) *Engine

NewEngine creates a template engine backed by a gas.TemplateProvider.

func (*Engine) AddFuncs

func (e *Engine) AddFuncs(funcs map[string]any)

AddFuncs merges additional template functions into the engine's funcmap and invalidates the cache so the next Render triggers a rebuild.

func (*Engine) Build

func (e *Engine) Build() error

Build loads all templates from the provider, classifies them, and compiles page templates into cached *template.Template instances.

func (*Engine) Render

func (e *Engine) Render(w http.ResponseWriter, name string, data any) error

Render executes the named page template, writing the result to w with a 200 status code. The name is the page path without extension (e.g. "home", "dashboard/index").

func (*Engine) RenderFragment

func (e *Engine) RenderFragment(w http.ResponseWriter, name string, data any) error

RenderFragment renders the page template's own content, bypassing the layout. Useful for HTMX partial responses.

func (*Engine) RenderWithStatus

func (e *Engine) RenderWithStatus(w http.ResponseWriter, status int, name string, data any) error

RenderWithStatus is like Render but sets the HTTP status code first.

type Option

type Option func(*Service)

Option configures the Service during construction.

func WithConfig

func WithConfig(cfg *Config) Option

WithConfig sets the service configuration explicitly. When set, automatic binding from the ConfigProvider is skipped.

func WithStaticFS

func WithStaticFS(fsys fs.FS) Option

WithStaticFS sets a custom fs.FS for serving static files (e.g. embedded via Go's embed package). When set, Config.UIStaticDir is ignored.

type Service

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

Service is the gas-ui service. It provides template rendering and static file serving. It satisfies the gas.UIProvider interface so other services can render HTML without depending on this package directly.

func (*Service) CheckReady

func (s *Service) CheckReady(_ context.Context) error

CheckReady reports whether the service is ready to serve render requests. Returns an error if the service has been closed (shutdown draining) or has not yet completed Init.

func (*Service) Close

func (s *Service) Close() error

Close marks the service as closed. Subsequent Render calls return 503.

func (*Service) Engine

func (s *Service) Engine() *Engine

Engine returns the underlying template engine for advanced use cases.

func (*Service) Init

func (s *Service) Init() error

Init initializes the template engine and registers the static file route.

func (*Service) Name

func (s *Service) Name() string

Name returns the service identifier.

func (*Service) RegisterFuncs

func (s *Service) RegisterFuncs(funcs template.FuncMap)

RegisterFuncs merges the given functions into the template engine's funcmap. Templates are rebuilt lazily on the next Render call. Safe to call during Init() from other services.

func (*Service) Render

func (s *Service) Render(w http.ResponseWriter, name string, data any) error

Render executes the named page template with a 200 status code.

func (*Service) RenderFragment

func (s *Service) RenderFragment(w http.ResponseWriter, name string, data any) error

RenderFragment renders a page template without wrapping it in the layout. Useful for HTMX partial responses. Part of the gas.UIProvider contract.

func (*Service) RenderWithStatus

func (s *Service) RenderWithStatus(w http.ResponseWriter, status int, name string, data any) error

RenderWithStatus executes the named page template with the given status code.

type Settings

type Settings struct {
	// StaticDir is the root directory for static files (CSS, JS, images).
	// Leave empty to disable static file serving.
	StaticDir string

	// StaticPath is the URL route pattern for static assets. Default: "/static/*".
	StaticPath string

	// LayoutName is the {{define}} name of the entry-point layout template.
	// Default: "base".
	LayoutName string

	// StaticStripPrefix is the URL prefix stripped from requests before looking
	// up files in the static directory or FS. Defaults to StaticPath when empty.
	// Set this independently when the route paths differ from the FS layout.
	StaticStripPrefix string

	// StaticPaths is a slice of URL patterns for static assets (e.g. ["/css/*", "/js/*"]).
	// When set, routes are registered for each path instead of using StaticPath.
	StaticPaths []string
}

Settings represents the configuration for user-facing template and static file settings.

Directories

Path Synopsis
Package uitest provides a mock implementation of gas.UIProvider for use in tests.
Package uitest provides a mock implementation of gas.UIProvider for use in tests.

Jump to

Keyboard shortcuts

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