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 ¶
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 ¶
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 ¶
Names returns the sorted list of view names currently loaded. Useful for diagnostics and tests.
func (*Engine) Render ¶
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.
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.