Help us shape the future of Go! Take the 2026 Go Developer Survey

view

package
v0.27.0 Latest Latest
Warning

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

Go to latest
Published: Sep 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