view

package
v0.26.0 Latest Latest
Warning

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

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package view is a thin, ergonomic layer over the standard library's html/template, modelled on Laravel's Blade view layer.

It loads templates from any fs.FS — an embed.FS for a single-binary deploy, or an on-disk directory with hot-reload during development — and adds the conveniences a real app needs on top of html/template:

  • Named views resolved by path (e.g. "pages/home").
  • Layouts: a view declares its layout and its body is injected into the layout's {{block "content"}} placeholder.
  • Partials / includes and reusable components via {{template ...}}.
  • A batteries-included FuncMap (dict, default, date/string/number helpers) plus injectable route/asset/url helpers so this package never imports the web layer — it stays a leaf.
  • Auto-escaping is inherited from html/template (contextual, on by default).
  • Per-render data merged over shared globals.

The package is deliberately a leaf: Engine.Render writes rendered bytes to any io.Writer the caller passes (an http.ResponseWriter, a bytes.Buffer for mail, etc.). It does not import the web package, so the web layer depends on view and not the other way around.

Layouts

A layout is an ordinary template that yields to its child via a block:

{{/* layouts/app.html */}}
<html><body><main>{{block "content" .}}{{end}}</main></body></html>

A view redefines that block and declares which layout wraps it through the {{layout "name"}} directive on its first line:

{{layout "layouts/app"}}
{{define "content"}}<h1>Hello {{.Name}}</h1>{{end}}

Render then executes the layout with the view's "content" definition in scope. A view without a {{layout}} directive is rendered standalone.

Partials and components

Any loaded template is addressable by name, so partials and components are just {{template "name" .}}; pass several values into a component with dict:

{{template "partials/nav" .}}
{{template "components/button" (dict "label" "Save" "kind" "primary")}}

Usage

//go:embed templates
var tplFS embed.FS

eng, err := view.New(tplFS, view.Options{Root: "templates"})
// ...
eng.Render(w, "pages/home", map[string]any{"Name": "Ada"})

Rendering mail

Because Render targets any io.Writer, the mail package can render an HTML body without view importing mail:

var buf bytes.Buffer
eng.Render(&buf, "emails/welcome", data)
msg := mail.NewMessage().To(addr).HTML(buf.String()).Build()
Example

Example renders a page that declares a layout, pulls in a reusable button component via {{template ... (dict ...)}}, and uses a custom template func ("badge") injected through Options.Funcs. The FS is an in-memory fstest.MapFS so the output is fully deterministic.

package main

import (
	"bytes"
	"fmt"
	"html/template"
	"testing/fstest"

	"github.com/devituz/lagodev/view"
)

func main() {
	files := fstest.MapFS{
		// Layout: yields to the child view through {{block "content"}}.
		"templates/layouts/app.html": &fstest.MapFile{Data: []byte(
			`<html><body><main>{{block "content" .}}{{end}}</main></body></html>`,
		)},
		// Reusable component, addressed by name and fed values via dict.
		"templates/components/button.html": &fstest.MapFile{Data: []byte(
			`<button class="{{.kind}}">{{.label}}</button>`,
		)},
		// Page: declares its layout, redefines "content", calls the component
		// and the custom "badge" func.
		"templates/pages/home.html": &fstest.MapFile{Data: []byte(
			`{{layout "layouts/app"}}` +
				`{{define "content"}}` +
				`<h1>{{upper .Name}}</h1>` +
				`{{badge .Role}}` +
				`{{template "components/button" (dict "label" "Save" "kind" "primary")}}` +
				`{{end}}`,
		)},
	}

	eng, err := view.New(files, view.Options{
		Root: "templates",
		Funcs: template.FuncMap{
			// Custom func: renders a small role badge (trusted markup).
			"badge": func(role string) template.HTML {
				return template.HTML(`<span class="badge">` + role + `</span>`)
			},
		},
	})
	if err != nil {
		panic(err)
	}

	var buf bytes.Buffer
	if err := eng.Render(&buf, "pages/home", map[string]any{
		"Name": "ada",
		"Role": "admin",
	}); err != nil {
		panic(err)
	}

	fmt.Println(buf.String())
}
Output:
<html><body><main><h1>ADA</h1><span class="badge">admin</span><button class="primary">Save</button></main></body></html>

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrTemplateNotFound = errors.New("view: template not found")

ErrTemplateNotFound is returned by Render/RenderToString when the requested view name was never loaded.

Functions

This section is empty.

Types

type Engine

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

Engine compiles and renders a set of named templates from an fs.FS.

An Engine is safe for concurrent use. In production (Dev=false) templates are parsed once at New and reads are lock-free against an immutable set; in Dev mode the set is rebuilt under a mutex on every Render.

func New

func New(fsys fs.FS, opts Options) (*Engine, error)

New constructs an Engine over fsys and parses the templates immediately so syntax errors surface at startup (in Dev mode they also re-surface on each Render). A nil fsys is an error.

func (*Engine) Names

func (e *Engine) Names() []string

Names returns the sorted list of view names currently loaded. Useful for diagnostics and tests.

func (*Engine) Render

func (e *Engine) Render(w io.Writer, name string, data any) error

Render executes the named view and writes the result to w. When the view declared a {{layout}}, the layout is executed with the view's "content" block in scope; otherwise the view is rendered standalone.

data is the per-render payload. When it is a map[string]any, the engine merges the configured Globals beneath it (per-render keys win) and also exposes the whole globals map under the "globals" key. For non-map data the value is passed through untouched and globals are reachable only via the "globals" func/key is not injected — pass a map if you need globals.

A missing view yields ErrTemplateNotFound. Auto-escaping is html/template's contextual default.

func (*Engine) RenderToString

func (e *Engine) RenderToString(name string, data any) (string, error)

RenderToString renders the view into a string. Convenient for mail bodies, tests, and anywhere an io.Writer is awkward.

type Options

type Options struct {
	// Root is the sub-directory of the FS that holds templates. The Root
	// prefix is trimmed from every view name, so a file at
	// "templates/pages/home.html" with Root "templates" is addressed as
	// "pages/home". Empty means the FS root.
	Root string

	// Ext is the template file extension to load, including the dot. When
	// empty it defaults to ".html". Files with other extensions are ignored.
	Ext string

	// Dev enables hot-reload: every Render re-reads and re-parses the
	// templates from the FS so edits are picked up without a restart. Leave
	// false in production for a parse-once, lock-free fast path. Dev relies on
	// the FS reflecting on-disk changes (os.DirFS does; embed.FS does not).
	Dev bool

	// Funcs are extra template functions merged over the built-ins. Injecting
	// route/asset/url helpers here keeps view a leaf package.
	Funcs template.FuncMap

	// Globals are values exposed to every render under the "globals" key and
	// merged into map data (see Render). Per-render data wins on conflict.
	Globals map[string]any
}

Options configures an Engine.

Jump to

Keyboard shortcuts

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