partial

package module
v0.15.1 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 23 Imported by: 0

README

go-partial

go-partial

Go Reference codecov

Documentation · Part of go-webthings.

This package provides a request-aware rendering layer for Go templates. It lets applications render full pages or targeted partials from the same registered template tree, with wrapper/content composition, connector headers, OOB output, caching, and typed template-friendly data flow. go-partial supersedes go-htmx. The rendering core stays small; connectors and optional extensions add the browser integration your application needs.

Features

  • Partial Templates: Define and render partial templates with typed dot data and functions.
  • Native Template Composition: Use {{ template "row.gohtml" . }} with SetDot so reusable sections can render inside a page or as HTMX targets.
  • Wrapper Content: Use wrapper partials with content children while keeping page data explicit.
  • Template Caching: Enable caching of parsed templates for improved performance.
  • Out-of-Band Rendering: Support for rendering out-of-band (OOB) partials.
  • File System Support: Use custom fs.FS implementations for template file access.
  • Concurrent Rendering: Configure partial trees up front, then reuse them safely across concurrent requests.
  • Checks: Check typed contracts with CheckContracts and every template file with CheckTemplates before the first request.
  • Browser Clients: Small scripts for flash messages, server-sent events and interactions, with no build step (clients).
  • Strict Content-Security-Policy: Output that works without 'unsafe-inline' and 'unsafe-eval'; see Content-Security-Policy.

Installation

To install the package, run:

go get github.com/donseba/go-partial

Template functions

Template-facing helpers and accessors are documented in TEMPLATE_FUNCTIONS.md.

Package Shape

The root package is intentionally small: rendering lifecycle, partial trees, runtime context, connectors, Root partial configuration, and core template helpers. Core does not import optional packages.

Optional packages are split by stability:

  • ext/... contains extension packages that are useful but not required by core, such as ext/errors and ext/debug.
  • exp/... contains experimental opt-in features, such as localization, CSRF, flash messages, selection, actions, pageflow, interactions, metrics, OpenTelemetry, slots, head regions, target resolvers, template helpers, and SSE.

Applications choose the pieces they want with SetFunc(...), Use(...), or package-specific setup helpers. A render stage follows the same lifecycle everywhere: Prepare can add request-scoped context, Render wraps or replaces template rendering, and Finalize observes or transforms the result.

A stage gives templates functions bound to the render with ResolveFunc: go-partial asks it, by name, only for the functions a template calls, so a page whose templates call none of them pays nothing. Templates are parsed with stand-ins for the names a stage declares in FuncNames, so Use(stage) is all a stage needs; the FuncMap() of the stages of exp/... and ext/... stays for go-clue's signatures and for templates go-partial does not execute.

partial.RenderStageHooks{
    FuncNames: []string{"tenant"},
    ResolveFunc: func(ctx *partial.RenderContext, name string) (any, bool) {
        if name != "tenant" {
            return nil, false
        }
        return func() string { return tenantFrom(ctx.Context) }, true
    },
}

RenderContext.SetFunc in Prepare still works and comes first, but makes the function for every partial the stage prepares.

Render stages may set generic response metadata through ctx.Response. partial.Write applies that status and those headers after rendering. Templates do not receive helpers for setting headers or status; templates produce HTML.

Rendering

For partials, prefer the package-level rendering functions:

html, err := partial.Render(ctx, content)
html, err = partial.RenderWithRequest(ctx, r, content)
err = partial.Write(ctx, w, r, content)

partial.Write owns HTTP response behavior: configured headers, connector response headers, render-stage response metadata, error fragments, and out-of-band regions. Partial itself does not render or write responses; pass it to the package functions.

partial.RenderSelf(ctx, r, p) renders the partial itself with the request, whatever target the request asks for. Use it for HTML that reaches the browser another way, such as a server-sent event.

partial.Write adds the request headers the connector reads to the Vary response header (see connector.VaryHeaders), so caches keep a page and a fragment of it apart. Unless a header sets it, Write sends Content-Type: text/html; charset=utf-8.

Template Cache

UseTemplateCache(true) on the root turns the template cache on for the whole tree: children, content, the partials {{ partial }} renders and those rendered under the root with SetParent use the setting of their nearest ancestor that sets one, unless they set it themselves. One switch on the root so covers development and production:

root.UseTemplateCache(!development)

What the cache learns of a file system, its parses, its scans, which files exist and what include patterns match, is kept by the root of the tree and shared by its clones, the partials rendered under it and those that name it with SetParent. Another root, also one over the same directory or map file system, such as a second application in one test process, learns it again: it sees a file added after the first root rendered, which the first root, with its cache on, does not. Give partials built per request a configured parent with SetParent rather than a root of their own, so they share its cache.

Shared Includes

SetIncludes gives every template of a partial and of the partials rendered under it the templates its patterns match, such as a theme's partials, as if each partial listed the files itself:

root := partial.NewID("shell", "layout.gohtml").
    SetFileSystem(theme).
    SetIncludes("partials/*.gohtml")

Patterns are those of fs.Glob, resolved against the file system of the partial being rendered; a file is parsed once, also when several patterns, or the partial's own templates, name it. A partial's patterns come after those of its ancestors. With the template cache a pattern is resolved once per file system; without, at every render, so new files show.

SetChildIncludes gives the patterns to the partials rendered under a partial only, not to the partial itself: a root gives the content, regions, fragments and error templates of every page the theme's partials without adding them to the parse of its layout.

root.SetChildIncludes("partials/*.gohtml")

root.Glob(pattern) returns the files a pattern matches, found as includes are: once per file system with the template cache.

Data Computed When Needed

partial.DotFuncOf(fn) is a dot computed by a function only when the partial renders, on the page or as the target of a partial request, which SetDotSource sets. A response that leaves the partial out, such as one for another panel of a dashboard, does not load its data:

stats := partial.DotFuncOf(func(ctx context.Context) (Stats, error) {
    return store.Stats(ctx)
})
panel := partial.NewID("stats", "stats.gohtml").SetDotSource(stats)

The function receives the render's context and runs once per render tree; an error fails the partial's render. The dot keeps its type, so CheckContracts checks it against the template's @dot without calling the function. A partial.Dot can travel as a value, such as in a field of a struct, before there is a partial.

partial.ModelFuncOf(fn) is a typed @model value computed only when a template calls it, once per render tree, also when the layout, the header and the sidebar each call it, and not at all for a response that leaves them out. SetModel and SetContract take it with the other models, also from a list of models an application passes around before there is a partial:

page.SetModel(user, partial.ModelFuncOf(func(ctx context.Context) (Navigation, error) {
    return loadNavigation(ctx)
}))
{{/* @model Nav github.com/acme/app.Navigation */}}
<nav>{{ range Nav.Items }}…{{ end }}</nav>

It is matched by its type, as SetModel values are, also by CheckContracts, without calling the function. SetModel ignores nil values.

Dot.Model() gives a dot as a model that shares its value: a dot of DotFuncOf is computed once per render tree when the layout renders it as its dot and the regions call it with @model, rather than once for each. A dot of an interface type, whose values' type is not known, is no model.

view := partial.DotFuncOf(loadView)
root.SetDotSource(view).SetModel(view.Model())
Choosing A Template

FirstExisting returns the first of several templates that exists in a partial's file system, such as a page's own template before the one of its type and the default:

file, err := root.FirstExisting("pages/"+slug+".gohtml", "pages/"+kind+".gohtml", "pages/page.gohtml")

With the template cache, the answer for each candidate is remembered per file system, also when it is missing. Without a match, the error wraps fs.ErrNotExist.

Over an Overlay(theme, defaults), FirstExisting finds a default specific template before the theme's general one. FirstExistingIn tries every candidate in a layer before the next, the layers of the partial's overlay or those it is given:

file, err := root.FirstExistingIn(nil, "screens/"+key+".gohtml", "screens/"+kind+".gohtml")

root.Defines("mail/welcome.gohtml", "html") reports whether a file defines a template, from the scan renders use, kept with the template cache; HasEntry() whether a partial's file defines its entry.

Rendering Text

partial.RenderText(ctx, p) renders a partial with text/template, for text that is not HTML, such as the subject and text part of an e-mail, with the file system, includes, functions, @model values, stages and template cache of an HTML render:

mail := partial.NewID("mail", "mail/welcome.gohtml").SetParent(root).SetDot(user)

subject, err := partial.RenderText(ctx, mail.SetEntry("subject"))
text, err := partial.RenderText(ctx, mail.SetEntry("text"))
html, err := partial.RenderSelf(ctx, nil, mail.SetEntry("html"))

Nothing is escaped. Stages see partial.RenderKindText. Text templates call others with {{ template }}, from the partial's files and includes; {{ partial }} and {{ content }} render HTML.

Rendering Under A Parent

SetParent renders a partial under a configured partial without adding it to that partial's children: it inherits the parent's functions, render stages, connector, file system, template cache, failure policy and @model values, and the parent is not changed. One configured root can so be the parent of every part an application renders on its own, such as a component, an e-mail or a server-sent event, without cloning it:

card := partial.NewID("card", "card.gohtml").SetDot(data).SetParent(root)
html, err := partial.RenderSelf(ctx, r, card)

Requests cannot target such a partial; use With for children that can be targeted. Parent() returns the partial a partial renders under.

Rendering A Defined Template

SetEntry(name) renders the template a file defines with {{ define name }} instead of the file itself, for files that define several parts, such as an e-mail:

html, err := partial.Render(ctx, partial.New("mail/welcome.gohtml").SetEntry("html").SetDot(user))
Failure Policy

By default a failing part of a page, such as the content child or a {{ partial }} call, renders as an error fragment in place and the rest of the page renders. Where a partly broken page is worse than an error page, fail the whole response instead:

root.SetFailurePolicy(partial.FailResponse)

With FailResponse, any failing part, and any template, child or content partial that is missing, fails the render. Render, RenderWithRequest and RenderSelf return the first failure and no HTML. Write writes nothing of the page: it writes the failure response of a registered error stage, such as ext/errors, or nothing at all, and returns the error so the handler can answer it.

When Write wrote a failure response, it returns a *partial.FailureResponseError with the status it wrote, so the handler knows not to write another one. A failure response is sent with Cache-Control: no-store. With FailResponse it keeps an error status for partial requests too, and comes without out-of-band regions; with FailInline a partial request gets the error fragment with status 200, so the client swaps it in place.

err := partial.Write(ctx, w, r, page)
var written *partial.FailureResponseError
if err != nil && !errors.As(err, &written) {
    http.Error(w, "Something went wrong", http.StatusInternalServerError)
}
Target Fallback

A partial request for a target the tree cannot resolve, such as a link in another page's layout that targets an element this page lacks, fails by default. A target fallback answers it with a partial of the tree instead, and tells the client to swap it where that partial lives, as a navigation:

root.SetContent(content).
    SetTargetFallback(partial.TargetFallback{ID: "content", PushURL: true})

The fallback renders as if the client had asked for it, with the out-of-band regions of its parents. A slot of exp/slots that is requested on its own, such as a list that loads more rows, answers without those regions with slots.OmitOOB(). Write sends Retarget (#content by default), Reswap (outerHTML by default) and, for GET requests with PushURL, the request URL through the connector, such as HX-Retarget, HX-Reswap and HX-Push-Url for htmx. Known children and targets a resolver of exp/target answers are not affected.

Each answer of the fallback emits a target.fallback event at info level, with the target asked for and the fallback ID; a request the tree cannot answer without a fallback emits a target.missing warning. See OBSERVABILITY.md.

Metrics Output

exp/metrics records render lifecycle data through a small Sink interface. Use your own sink for storage, or write JSON lines to any io.Writer:

sink := metrics.Fanout(
    inMemoryStore,
    metrics.NewWriterSink(os.Stdout),
)

root.Use(metrics.Stage(sink, metrics.WithTag("chain", "web")))

The writer sink is intentionally plain: it works with stdout, files, buffers, pipes, or app-owned adapters that forward records to SSE, queues, or databases.

metrics.Outermost() records only the outermost render of a context, such as a page's layout with everything rendered inside it, instead of every nested partial; the renders inside it then cost no more than a context lookup, which suits metrics that are always on:

root.Use(metrics.Stage(sink, metrics.Outermost()))

Metrics records distinguish request identity from flow identity. WithRequestID labels the current HTTP request. WithTraceID can group related browser work such as a page request and later async or SSE requests. WithParentRequestID can link a spawned request back to the request that caused it.

Diagnostic Events

Core and extensions can emit diagnostic events without owning logging, tracing, or queues. Attach shared consumers to the reusable root partial:

events := partial.NewAsyncEvents(
    partial.EventsConfig{Buffer: 256, DropPolicy: partial.DropNewest},
    logger.Sink(slog.Default(), logger.WithMinLevel(partial.EventWarn)),
    mySink,
)

root := partial.New("templates/shell.gohtml").
    SetEvents(events)

ext/logger adapts events to log/slog. logger.WithLevels(...) writes only the levels given, such as partial.EventWarn without the errors an application logs where it answers them, and logger.Sink(nil) writes to slog.Default() as it is when each event is written. App-owned sinks can forward the same events to DynamoDB, Kafka, OpenTelemetry, SSE debug pages, files, or any other collector. See OBSERVABILITY.md for concrete sink examples.

For request-owned collectors, attach a sink with partial.WithEventSink(r.Context(), sink) and close request-owned async dispatchers in middleware.

Example Applications

A documentation-style site built with go-partial is available in examples/docs.

go run ./examples/docs

Open http://localhost:8091.

An htmx-backed feature showcase with real template files is available in examples/showcase.

go run ./examples/showcase

Open http://localhost:8090 to see pages for typed rows, selection partials, actions, flash messages, out-of-band rendering, context helpers, localization, HTMX response headers, server-sent events, live metrics, infinite scroll with cursor-style X-Action values, and the optional error page.

Integrations

Several integrations are available, detailed information can be found in the INTEGRATIONS.md file.

  • htmx 2 (connector.NewHTMX) and htmx 4 (connector.NewHTMX4)
  • Turbo
  • Unpoly
  • Partial, for framework-neutral fetch clients and tests
  • SSE writer, for streaming rendered HTML patches
HTMX 4

Choose connector.NewHTMX4(nil) on the root partial. The connector reads HX-Request-Type and decodes HX-Target: main#content into the partial ID content. Body/select requests and history restoration render the full page. Use IDs on fragment targets; the tag name alone cannot identify a partial.

Continue to build responses with ReswapWith(connector.NewSwap()...). The response builder snapshots the options, and the selected connector serializes scroll/show targets in its own protocol. NewHTMX keeps the HTMX 2 syntax; NewHTMX4 emits separate scrollTarget/showTarget modifiers. Raw Reswap strings are sent as supplied, so write those in the selected protocol's syntax.

HTMX 4 removed HX-Trigger-After-Swap and HX-Trigger-After-Settle; its connector omits these fields. Use Trigger for server events or the browser's htmx:after:swap / htmx:after:settle events when timing matters. Stream interactions emit hx-sse:connect; load the pinned hx-sse extension and send unnamed SSE data messages for HTML swaps. Named SSE messages dispatch events.

Browser migration also requires explicit :inherited attributes and the new fetch-based event context. See the official migration guide.

HTMX 4 protocol audit

The connector's complete header/attribute output was checked against the HTMX 4 header reference, migration reference, and the swap reference.

Definitions HTMX 4 behavior
HX-Request, HX-Boosted, HX-Current-URL, HX-History-Restore-Request Names unchanged; history restores render the document
HX-Request-Type full renders the document; partial renders the target
HX-Target, HX-Source Browser sends tagName#id; target lookup decodes the URI-encoded ID
Request HX-Trigger, HX-Trigger-Name Replaced by HX-Source; legacy constants remain for HTMX 2
HX-Prompt Available with the HTMX 4 prompt extension; absent from core
HX-Location, HX-Push-Url, HX-Redirect, HX-Refresh, HX-Replace-Url, HX-Reswap, HX-Retarget, HX-Reselect, response HX-Trigger Header names unchanged; supported response fields are covered together by a protocol test
HX-Trigger-After-Swap, HX-Trigger-After-Settle Omitted by the HTMX 4 connector; retained by HTMX 2
hx-get, hx-trigger, hx-target, hx-swap Still valid on the request element; async/load, reveal, poll, refresh and custom-event helpers are covered
hx-ext, sse-connect, sse-swap Legacy only; streams emit hx-sse:connect, hx-target, hx-swap and require the HTMX 4 SSE script
Swap.FocusScroll() focusScroll in HTMX 4, focus-scroll in HTMX 2
Swap.Show() / Swap.Scroll() HTMX 4 separates direction from quoted showTarget / scrollTarget

The attribute constants cover output owned by the interaction helpers, rather than every possible template attribute. Application templates must also migrate inheritance, renamed attributes, event handlers and extension loading.

HTMX 4 also introduces built-in morph swaps and outerSync; typed swap styles are available as SwapInnerMorph, SwapOuterMorph, SwapOuterSync and SwapTextContent. These use the same NewSwap().Style(...) builder.

Other new browser features belong in application templates: hx-action and hx-method, body-based hx-query, per-element hx-config, hx-ignore, hx-validate, status-specific hx-status:CODE, explicit inheritance and multi-target <hx-partial> responses. Existing OOB output remains supported. These do not require new connector request/response headers. The rewritten SSE integration is covered; WebSockets, streaming multipart responses, reactive hx-live, download/head/pending/preload and other extensions require their own scripts and application endpoints. They are not enabled by NewHTMX4.

Error Output

partial.Write can render an HTML error page when template parsing or execution fails, but only when an error stage is registered. Register ext/errors to choose the failure markup. In detailed mode, the page includes the partial ID, template list, request URL, template location, and original error so development and failed htmx requests still return useful output. In safe mode (the default, for production) it names no partial, template or error: it shows the status text (Data.Title), the safe message (Data.Message) and the request URL, in the language of Data.Lang.

Normal requests receive a 500 full HTML page. HTMX/partial requests receive a swappable error fragment with status 200, because HTMX does not swap 500 responses by default.

The built-in page styles itself with one inline <style> element, so it looks right when the application's own assets are what failed; exterrors.StyleHash() is its hash for a Content-Security-Policy. The built-in fragment, which lands in the application's page, has classes only (go-partial-error, go-partial-error__title, go-partial-error__message, go-partial-error__details): serve exterrors.StylesheetFS(), which holds the CSS that styles them as partial-errors.css, and link that file from the layout, or style the classes in the application's own CSS. See Content-Security-Policy.

With exterrors.WithFlash(fn), a partial request that fails is answered with an error flash message instead, added out of band to the flash target with the template of the tree's exp/flash stage, as the page's own messages are, while the request's target stays as it is (HX-Reswap: none); its text is the one fn returns, or Data.Message (see WithMessage) for a nil fn or an empty text. Refusals that are not render failures use flash.Respond (see Flash Messages In The Browser).

Register the error stage on the reusable root partial:

root := partial.New("templates/shell.gohtml").
    Use(exterrors.Stage(exterrors.WithMode(exterrors.ModeDetailed)))

Or on one partial:

content.Use(exterrors.Stage())

The failure response can be a template of the application's own, such as a theme's 500 page or a toast for failed partial requests:

root.Use(exterrors.Stage(
    exterrors.WithTemplate("500.gohtml"),
    exterrors.WithFragmentTemplate("partials/error-toast.gohtml"),
))

The template is read from the file system of the partial that failed and renders as its child, with its functions, stages, includes (also the root's SetChildIncludes) and @model values; its dot is an exterrors.Data. Data.Message is exterrors.SafeMessage in the safe mode and the error in the detailed mode, so a template can show it; WithMessage(fn) sets it, such as to a translation of SafeMessage in the request's language (an empty text keeps it), for the error page, the error fragment and the error flash message alike. WithLang(fn) sets Data.Lang, the language of the error page, such as that of the page that failed: the built-in page writes it as its lang attribute (en without it), and a template can too. WithTitle(fn) sets Data.Title, the heading of the built-in page and fragment, such as a translation of the status text (http.StatusText, the default; an empty text keeps it); a detailed failed render keeps its "Template render error" heading. Data.PartialID and Data.Templates name the partial that failed in detailed mode only. Data.Error holds the error in both modes, and Data.Location its template location in detailed mode: show them only when .Detailed. When the template fails itself, the built-in markup answers and an ext.errors.template_failed warning is emitted. While an error template renders, exterrors.FromContext(ctx) returns its Data and true for the context a template function with a context.Context parameter gets, in the template and the partials it includes, so a function that writes something of the page that failed, such as its structured data, can leave it out. A stage with only a page template answers partial requests with it too, and one with only a fragment template full pages.

partial.RenderWithRequest still returns the render error directly. partial.Write asks the render stage chain for a failure response; without ext/errors, it returns the original render error.

An error that happens outside a render, such as a form post whose handler fails to save it, gets the same answer with exterrors.Respond(w, r, root, status, err): the error page, fragment or flash message of the stage registered on root's tree, with its templates, WithMessage and WithFlash, written with status and never cached. Data.Status is that status (500 for a render that failed), and Data.PartialID and Data.Templates are empty, as no partial failed:

if err := comments.Add(r.Context(), form); err != nil {
    log.Error("add comment", "error", err)
    exterrors.Respond(w, r, root, http.StatusInternalServerError, err)
    return
}

A full-page request gets the error page. A partial request gets the error flash message with WithFlash, out of band with HX-Reswap: none; without it, the error page for a 5xx status, which the interactions client's reloadOnError shows as the whole page for a form post, and the error fragment for another status, such as 429, which htmx 4 swaps into the target. A tree without the stage gets the built-in markup in the safe mode. partial.WriteError(w, r, p, status, err) is the core of it, for any error stage: it returns the error, writing nothing, when no stage answers; error stages read the status with RenderContext.FailureStatus().

Localization

Templates receive a request localizer through the localizer and locale helpers from exp/localization. The interface only requires GetLocale(). Translation behavior should come from user-provided template functions registered with Partial.SetFunc:

// tr gives tl, tn, ctl and ctn, such as a *translator.Translator of
// github.com/donseba/go-translator/v2.
root.SetFunc(localization.FuncMap(), tr.FuncMap())
root.Use(localization.Stage())
ctx := localization.WithLocalizer(r.Context(), localizer)
_ = partial.Write(ctx, w, r, content)
<p>{{ tl localizer "Hello, World!" }}</p>
<p>{{ tn localizer "You have one message." "You have %d messages." 5 5 }}</p>
<button>{{ ctl localizer "button" "save" }}</button>

For templates go-partial does not execute, such as a text/template, localization.Funcs(localization.New("nl")) returns the same locale and localizer helpers, bound to a localizer.

tl, tn, ctl, and ctn are not built into go-partial. For a fuller translation backend, github.com/donseba/go-translator/v2 (package translator) fits this pattern well: Translator.FuncMap() gives these four functions, which take the localizer as their first argument.

CSRF

exp/csrf gives templates the request's CSRF token through the csrf helper. go-partial does not make or check tokens: the application's CSRF middleware puts the token on the request context, as a csrf.Token (Token(ctx) and Key()) or as a plain string:

root.Use(csrf.Stage())

ctx := csrf.WithTokenString(r.Context(), token) // or csrf.WithToken(ctx, myToken)
_ = partial.Write(ctx, w, r, page)
<input type="hidden" name="{{ csrf.Key }}" value="{{ csrf.Token ctx.Context }}">

With WithTokenString the key is csrf.DefaultTokenKey (X-CSRF-Token). Without a token on the context, csrf gives a placeholder token that no check accepts.

HTMX Response Helpers

The configured connector turns partial response instructions into protocol-specific response headers:

content := partial.NewID("notice", "notice.gohtml")

content.Response().
    Retarget("#notice").
    ReswapWith(connector.NewSwap().Style(connector.SwapOuterHTML).Transition(true)).
    TriggerWith(connector.NewTrigger().AddEventObject("notice", map[string]any{
        "message": "Saved",
    }))

You can also set the response instructions as data:

content.SetResponse(connector.Response{
    Retarget: "#notice",
    Trigger: connector.NewTrigger().AddEventObject("notice", map[string]any{
        "message": "Saved",
    }).String(),
})

With the HTMX connector, these become HX-Retarget, HX-Reswap, and HX-Trigger headers during partial.Write.

After a form is posted, connector.Redirect sends the client on in the way its library follows: HX-Location with status 200 for a request htmx made, so htmx swaps the new page into the target without a full load, and a 303 See Other otherwise, which browsers, Turbo and Unpoly follow:

connector.Redirect(root.Connector(), w, r, connector.Location{Path: "/admin/pages", Target: "#content"})

connector.Attrs(conn, connector.Attr{Confirm: "Delete?", Boost: connector.BoostOff, Target: "#list", Swap: "outerHTML"}) returns the attributes of a link or form in the connector's syntax, such as hx-confirm and hx-boost="false" for htmx, up-confirm and up-follow="false" up-submit="false" for Unpoly and data-turbo-confirm and data-turbo="false" for Turbo; templates use clientAttrs, noBoost and confirmAttr of exp/interactions.

Attr also writes the settings of a whole page, on the body: Inherit gives them to the descendants (htmx 4's :inherited; an inherited boost carries its target and swap, quoted when they hold whitespace or a comma, such as swap:'outerHTML showTarget:body'), DisableWhileRequest names the elements disabled while a request is under way (htmx 4's hx-disable, htmx 2's hx-disabled-elt), KeepOnServerError leaves the page as it is on a 5xx response (htmx 4's hx-status:5xx="swap:none push:false"), and ReloadOnError loads a link or form whose request gets a 5xx status or no answer as a whole page (data-partial-reload-on-error with every connector, which the client of exp/interactions reads): A link is followed and a GET form submitted again by the browser itself. A request that is not safe to send twice (POST, PUT, PATCH, DELETE), such as a comment or a vote, is never sent again: if the server answered with an HTML error page, that page replaces the document, as a whole-page load would show it; without one, or without an answer, the page stays as it is and partial:reload-on-error:failed is dispatched on the form (detail.method, detail.status, 0 without an answer), so the application can react. An htmx 4 answer with a 5xx status and HX-Reswap: none leaves the page as it is, also for a POST: it has only out-of-band parts, such as the error flash message of exterrors.WithFlash(fn), which htmx 4 swaps.

connector.Attrs(conn, connector.Attr{Inherit: true, Boost: connector.BoostOn, Target: "#content", Swap: "outerHTML",
    DisableWhileRequest: "find button[type='submit']", KeepOnServerError: true})
// hx-boost:inherited="target:#content swap:outerHTML" hx-disable:inherited="find button[type='submit']"
// hx-status:5xx:inherited="swap:none push:false"

SubmitConfirm asks a question before a submit button sends its form, for a form whose buttons do different things: data-turbo-confirm with Turbo, else data-partial-confirm, which exp/interactions' browser client asks (interactions.ClientScript()). connector.MergeAttrs(conn, attrs, attr, kinds...) puts attr into a map of attributes another library built, replacing the connector's attributes of the same settings, and removing those of kinds. connector.Apply(conn, w.Header(), connector.Response{PushURL: "false"}) sets response headers in one call. connector.OOBAttrs(conn, "beforeend") returns the attributes that swap an element out of band, which templates write with {{ oobSwap "beforeend" }}.

The kinds of MergeAttrs are connector.AttrKind values, one for each setting of Attr: AttrConfirm, AttrBoost, AttrTarget, AttrSwap, AttrDisableWhileRequest, AttrKeepOnServerError, AttrSubmitConfirm and AttrReloadOnError. This asks another question, or none when question is empty:

attrs = connector.MergeAttrs(conn, attrs, connector.Attr{Confirm: question}, connector.AttrConfirm)

connector.SetRequestTarget(conn, req, "comments") makes a request ask for a target as the connector's client library would, the inverse of GetTargetValue, for tests and requests a server makes to itself.

Handlers that need to know more about a request ask the connector instead of reading protocol headers. p.Connector() returns the connector a partial uses:

conn := root.Connector()
connector.IsRequest(conn, r)   // made by the client library, also for a whole page
connector.IsBoosted(conn, r)   // a boosted link or form
connector.CurrentURL(conn, r)  // the page the client shows, such as HX-Current-URL
connector.VaryHeaders(conn)    // the request headers that change responses

Debug Helper

The debug template helper renders a styled diagnostic box using an embedded template. The box styles its classes with one <style> element of its own; under a Content-Security-Policy without 'unsafe-inline', add debug.StyleHash() to style-src in development (see Content-Security-Policy):

{{ debug runtime . }}

Register the debug helper and stage globally, per wrapper, or per partial:

root.SetFunc(debug.FuncMap())
root.Use(debug.Stage())

With debug.WithFunc(), the stage gives templates the helper bound to the render, so they write {{ debug . }}; {{ debug runtime . }} keeps working. debug.WithFunc(debug.UnlessDefined()) leaves a debug function the application registers itself in place (other than debug.Disabled()), without a warning. In a text render, such as an e-mail subject, debug writes plain text. In production, debug.Disabled() is a helper that renders nothing, so templates keep their calls:

if development {
    root.Use(debug.Stage(debug.WithFunc()))
} else {
    root.SetFunc(debug.Disabled())
}

Server-Sent Events

SSE is a writer layer, not a connector. Use it after deciding which partials changed:

events := sse.NewWriter(w)

notice := partial.NewID("notice", "notice.gohtml").
    SetDot(Notice{Message: "Saved"})

_ = events.PatchPartial(r.Context(), r, "#notice", notice)
_ = events.Signal("saved", true)
events.Flush()

PatchPartial renders the partial itself, whatever the request targets. A partial that is not in a tree renders with the functions, stages, file system and models of an application's root through events.SetParent(root), which leaves both the partial and the root unchanged, so a part of a page streams without a render helper of its own:

events := sse.NewWriter(w).SetParent(root)
viewers := partial.NewID("viewers", "viewers.gohtml").SetDot(view)
_ = events.PatchPartial(r.Context(), r, "#viewers", viewers)

Flush follows middleware wrappers that expose Unwrap() http.ResponseWriter and honors their FlushError methods.

The writer declares constants for expected headers and event names, such as HeaderContentType, ContentTypeEventStream, EventPatch, EventSignal, and EventError.

exp/sse ships a small browser client for these events, with no build step and no other library. It replaces the elements each patch's target selects with its HTML (outerHTML), dispatches partial:signal and partial:error as DOM events, and opens a stream for every element with data-partial-stream="/url" while it is in the page. Serve it from clients.FS() (see Serving The Browser Clients):

mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServerFS(clients.FS())))
<script src="/assets/partial-sse.js"></script>
<div data-partial-stream="/events"></div>

With htmx loaded, patched content is processed by htmx. The client works with every connector; {{ stream }} of exp/interactions with a connector that leaves the client to the page also opens a stream through it.

A stream follows its element: when the URL in data-partial-stream changes, the client opens a stream to the new one. With data-partial-stream-hidden="30s", it closes the stream once the browser tab has been hidden that long (partial:stream:paused) and opens it again when the tab shows, dispatching partial:stream:reconnect on the element, so the page can fetch what it missed:

<aside id="presence" data-partial-stream="/events/presence" data-partial-stream-hidden="30s"></aside>

With data-partial-stream-client-id="tab", the client adds an id of the browser tab to the stream URL as that parameter, the same for the life of the page, so the server tells the tabs of one browser apart, such as for presence; scripts read it as goPartialSSE.clientId. A page restored from the back/forward cache opens its streams again, dispatching partial:stream:reconnect.

With data-partial-stream-beacon="/presence", the client tells that address about the stream, so the server can tell a hidden tab, whose stream is closed, from one that is gone. It POSTs clientId, params (the query of the stream URL, with the client id) and reason as a form: beat every data-partial-stream-beat="15s" while the element is in the page, also while the tab is hidden (a 4xx answer stops them); hidden when the stream closes as the tab is hidden; removed when the element leaves the page and no element left reports to the same address, such as after a navigation to a page without the stream; pagehide when the page is left. The page-leaving reports go with navigator.sendBeacon. sse.ReadBeacon(r) reads one:

beacon, err := sse.ReadBeacon(r)
if err != nil {
    http.Error(w, "invalid_beacon", http.StatusBadRequest)
    return
}
if beacon.Reason.Leaving() { // removed or pagehide
    presence.Leave(beacon.Params.Get("tab"))
} else { // beat or hidden
    presence.Visit(beacon.Params.Get("tab"), beacon.Params.Get("page"))
}

beacon.Reason is one of the sse.BeaconReason constants sse.BeaconBeat, sse.BeaconHidden, sse.BeaconRemoved and sse.BeaconPagehide; Leaving() is true for the last two.

Templates write the client's attributes with {{ sseAttrs }} (sse.FuncMap()), which writes nothing without a URL; in Go, sse.ClientStream{...}.Attrs():

<div{{ sseAttrs .PresenceStream "clientId" "tab" "hidden" "30s" "beat" "15s" "beacon" "/presence" }}>
Streams Many Clients Follow

A Broker fans messages out to the streams that follow a topic, such as everyone looking at the same page. It keeps its state in memory, so it serves the streams of one process, and is safe for concurrent use:

broker := sse.NewBroker(sse.BrokerOptions{MaxSubscribers: 1000, MaxPerKey: 10, Buffer: 1})
defer broker.Close()

// When something changes:
broker.Publish("page:/about", sse.Message{Event: "changed", Data: "/about"})

BrokerOptions limit it: MaxSubscribers caps the open subscriptions and MaxPerKey those that share a key, such as the streams of one user (zero is unlimited); Subscribe then fails with sse.ErrTooManySubscribers, and after Close with sse.ErrBrokerClosed. Buffer is how many messages a subscription holds while its stream is busy (default 16). Publish never blocks and reports how many subscriptions received the message; a full subscription misses it, so a buffer of 1 coalesces "something changed" notifications into one.

A stream handler subscribes, and Writer.Serve streams the subscription's messages until the request ends, the subscription closes or a write fails, flushing after each:

func stream(w http.ResponseWriter, r *http.Request) {
    subscription, err := broker.Subscribe(userID(r), "page:"+r.URL.Query().Get("page"))
    if err != nil {
        http.Error(w, "too_many_streams", http.StatusTooManyRequests)
        return
    }
    defer subscription.Close()

    events := sse.NewWriter(w).SetParent(root)
    _ = events.Serve(r.Context(), subscription, sse.ServeOptions{
        Heartbeat:       25 * time.Second,
        BeforeHeartbeat: func() error { return checkSession(r) },
        Handle: func(stream *sse.Writer, message sse.Message) error {
            return stream.PatchPartial(r.Context(), r, "#viewers", viewersPartial(r))
        },
    })
}

ServeOptions.Heartbeat writes a comment at that interval (Writer.Heartbeat writes one by hand), which keeps proxies from closing an idle stream and notices a client that went away; BeforeHeartbeat runs first, and its error ends the stream, such as when the session ended. Handle writes a message, such as a partial rendered for this client; without it the message's ID, Event and Data are written. Serve returns nil when the subscription closed, and clears the connection's write deadline, which an http.Server's WriteTimeout would otherwise use to end every stream. Subscription.Messages() is the queue for code that streams on its own.

Serving The Browser Clients

The clients of exp/flash, exp/sse and exp/interactions are files of one file system, clients.FS() (partial-flash.js, partial-sse.js, partial-interactions.js), for applications that serve, fingerprint and version them with their other assets; each package's ClientScript() returns its client for a bundle of the application's own. clients.All() lists them with the names scripts import them by, for an import map:

for _, client := range clients.All() {
    importMap.WithFile(client.Import, client.File) // "partial-flash", "partial-flash.js", …
}
A Confirmation Dialog Of The Page

The client of exp/interactions asks the question of a submit button with "submitConfirm", and htmx's hx-confirm (htmx 2 and 4), in a <dialog data-partial-confirm-dialog> of the page when there is one, instead of with the browser's window.confirm: some embedded browsers never show that and answer no, and the page's dialog has the application's styles and translated buttons. The question goes into the element with data-partial-confirm-question, as text. A button in the dialog with a value closes it with that value, also without a <form method="dialog">; confirm is yes, and Escape or any other value is no:

<dialog data-partial-confirm-dialog aria-labelledby="confirm-question">
    <p id="confirm-question" data-partial-confirm-question></p>
    <button type="button" value="cancel" autofocus>Cancel</button>
    <button type="button" value="confirm">Confirm</button>
</dialog>

Without the dialog, nothing changes: the questions use window.confirm. The questions of Unpoly and Turbo stay theirs, and so does an htmx 4 question that starts with js:. window.goPartialInteractions.ask(question) asks a question of the application's own the same way and resolves to true for yes.

Content-Security-Policy

go-partial's output works under a strict policy, without 'unsafe-inline' for styles or scripts and without 'unsafe-eval', such as default-src 'self'; script-src 'self'; style-src 'self'. Its browser clients are files (see Serving The Browser Clients), which 'self' covers, and none of them uses eval or new Function. What it writes inline:

Output Inline Under a strict policy
ext/errors built-in error page one <style> element add exterrors.StyleHash() to style-src
ext/errors built-in error fragment nothing: classes only serve exterrors.StylesheetFS() and link its partial-errors.css, or style it with the application's CSS; unstyled it reads as a heading, a paragraph and a list
ext/debug box (development) one <style> element per box, the same each time add debug.StyleHash() to style-src in development; without it the value shows unstyled
exp/head region markers empty <template> elements nothing to do

The hashes are 'sha256-…' sources with their quotes, ready for a policy. They change only when go-partial changes the style, which RELEASE_NOTES.md then says; take them from the functions rather than copying them:

policy := "default-src 'self'; script-src 'self'; style-src 'self' " + exterrors.StyleHash()
if development {
    policy += " " + debug.StyleHash()
}

style-src covers <style> elements; a policy that sets style-src-elem needs the hash there instead. An application that answers with an error template of its own (WithTemplate, WithFragmentTemplate) without inline style needs no hash.

Things that run under the page's policy rather than the response's:

  • The interactions client's reloadOnError writes the HTML error page of a failed POST into the page that sent it (document.open/write). That page keeps the policy of the page it replaces, so every page with data-partial-reload-on-error needs the error page's hash in its own style-src: exterrors.StyleHash() for the built-in page, or the hash of the application's own error page's inline style, if it has any.
  • Partial responses and SSE patches are inserted into the page, so their style="…" attributes are blocked (style-src-attr) and an inline <style> or <script> in them applies only when the page's policy has its hash. Style swapped and streamed content with classes, and keep inline scripts out of fragments.
  • Out-of-band <script type="application/ld+json"> and other data blocks are never executed, so the policy does not affect them.

With htmx, avoid what htmx evaluates as JavaScript, which 'unsafe-eval' would have to allow:

  • hx-on* attributes, such as hx-on:click;
  • trigger filters, such as hx-trigger="click[ctrlKey]";
  • js: and javascript: values, such as in hx-vals, hx-headers and hx-confirm. Plain JSON in hx-vals and hx-headers, and a plain question in hx-confirm, are fine.

htmx 4's indicator CSS (includeIndicatorCSS) is a constructable stylesheet (adoptedStyleSheets), which CSP does not block. htmx 2 inserts a <style> for its indicators: set "includeIndicatorStyles": false in its htmx-config meta element and style .htmx-indicator and .htmx-request in the application's CSS, or give it inlineStyleNonce on pages that are never cached.

Flash Messages Across Redirects

exp/flash keeps messages for the request after a redirect in a cookie, with their level:

cookie := flash.Cookie{Key: key}

cookie.Persist(w, flash.Success("Saved"))
http.Redirect(w, r, "/items", http.StatusSeeOther)
// For every request, such as in middleware:
r = cookie.Restore(w, r)

Restore moves the messages into the request's flash store, where {{ flash }} shows them, and clears the cookie. The fields of flash.Cookie configure the cookie; its Key signs it, so only messages the application persisted are shown, and the zero Cookie is not signed.

Flash Messages In The Browser

exp/flash ships a browser client for its messages, with no build step and no other library, which clients.FS() serves (see Serving The Browser Clients):

mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServerFS(clients.FS())))
<script src="/assets/partial-flash.js"></script>

It hides each message after a time that depends on its level (success and info 5s, warn 8s, error never), which data-flash-autohide-<level>="3s" or data-flash-autohide="3s" on an ancestor, or data-flash-autohide on a message, changes ("off" keeps it). A message stays while the pointer or keyboard focus is on it, goes when an element in it with data-flash-dismiss is clicked or Escape is pressed in it, and dispatches partial:flash as it appears. With data-flash-request-error="Something went wrong" on the flash target, failed requests of htmx (2 and 4), Unpoly and Turbo, and partial:error events of exp/sse, show that text as an error; scripts call goPartialFlash.show(level, text). A response with an error status that htmx swaps into its target, such as a form re-rendered with its validation errors (422) with htmx 4, shows itself and is no failed request: with htmx 4 the client decides at htmx:finally:request, once htmx swapped the response or not, and with htmx 2 by the shouldSwap of htmx:beforeSwap.

The messages the client shows itself look like those of the server: {{ flashTemplate }} in the flash target renders the application's flash template (flash.WithTemplate) as a <template data-flash-template="error"> per level, which the client clones. More attributes on the flash target:

<div id="flash-messages"
     data-flash-request-error="The request failed. Please try again."
     data-flash-request-error-replace
     data-flash-request-error-body
     data-flash-request-error-skip-flash>
    {{ flashTemplate }}
    {{ flash }}
</div>

-replace keeps one request error at a time, the next request removing it; -body shows the text/plain body of a refused request (4xx) when it is a sentence, without htmx swapping it; -skip-flash shows nothing more for a failed response that brings its own message.

The flash stage writes these attributes itself with flash.WithRequestErrors, in {{ flashTarget }} and in {{ flashTargetAttrs }}, which also writes the target's id and takes a text of the template's own, such as a translated one. {{ flashTemplate }} writes nothing when its partial renders out of band, so a flash target that responses add their messages to needs no condition:

root.Use(flash.Stage(flash.WithRequestErrors(flash.RequestErrors{
    Text: "The request failed. Please try again.", Replace: true, Body: true, SkipFlash: true,
})))
<div {{ flashTargetAttrs (t "The request failed. Please try again.") }} {{ oobAttr "beforeend" }} class="toasts">
    {{ flash }}
    {{ flashTemplate }}
</div>

A handler refuses a request with messages through flash.Respond: a response that adds them to the flash target out of band, with the template of the flash stage, for requests of the client library, and their text as text/plain otherwise:

_ = flash.Respond(w, r, root, http.StatusForbidden, flash.Error("You may not do this."))

flash.RenderOOB(ctx, messages...) renders such an addition from a render stage.

Slots, Regions And Fragments

exp/slots places named parts of a page with {{ slot "name" }}. Slots are looked up from the partial that places them through its ancestors, so a content template can place a slot of the layout. Options decide how partial requests treat a slot:

root.Use(slots.Stage())

// A region: placed on the page, and sent out of band with every partial response.
slots.Set(root, "cart", cart, slots.OOB())

// A fragment: answered on its own when a request targets its ID and the page
// can render it, else left to the target fallback.
slots.Set(root, "comments", comments, slots.CheckContracts(), slots.OmitOOB())

CheckContracts, OmitOOB and Deferred make a slot standalone: the stage answers requests for its ID. A slot without options is registered as a child of the partial it is set on, as before.

A deferred slot loads after the page: {{ slot }} renders a placeholder that requests the slot from the page's address through the connector, such as hx-get with hx-trigger="load", and the request renders the slot alone, with its lazy dot. slots.Placeholder(p) renders a placeholder of the application's own with a slots.PlaceholderData dot, and slots.Data(v) gives that dot data of the slot's own as .Data, such as a panel's heading, so one placeholder template serves several slots:

slots.Set(root, "recent", recent.SetDotSource(partial.DotFuncOf(loadRecent)), slots.Deferred(), slots.OmitOOB())
slots.Set(root, "attention", attention, slots.Placeholder(panel), slots.Data(Heading{Title: "Needs attention"}), slots.OmitOOB())

The template that places a slot can defer it too, whatever its options, with {{ deferredSlot "name" }}, or {{ deferredSlot "name" "reveal" }} to load it when it scrolls into view, such as one fragment of a directory that slots.Glob set alike:

<aside>{{ deferredSlot "popular" "reveal" }}</aside>

In Go, slots.DeferredSlot(name, ctx, "reveal") does what {{ deferredSlot }} does from a render context, such as in a template function of the application's own.

A region can send only the elements of it that a request changes: with SetOOBElements(ids...) on its partial, a partial response sends the elements of its output with those IDs, each out of band on its own with the connector's attribute, instead of the whole region; SetOOBElementsFunc(fn) chooses them per request, such as the menu items whose state changes from the page the visitor comes from. See Using Out-of-Band (OOB) Partials. A region placed in <head>, such as the page's canonical and alternate links, is made one with head.Region(p) of exp/head; see A Region In The Head.

slots.Glob(root, "regions/*.gohtml", opts...) sets a slot for every file of a directory, named after the file (menu for regions/menu.gohtml), found once per file system with the template cache; slots.Setup(fn) configures each slot's partial, such as its dot. {{ requiredSlot "name" }} fails the template for a slot that is not set; {{ hasSlot }} checks optional ones.

Basic Usage

Here's a simple typed example. The template owns the contract, the handler supplies the model.

1. Create A Root Blueprint

The root partial holds shared render configuration and is cloned per request.

root := partial.NewID("shell", "templates/shell.html").
    SetConnector(connector.NewHTMX(nil)).
    SetFileSystem(os.DirFS("web")).
    UseTemplateCache(true).
    SetEvents(events).
    SetFunc(template.FuncMap{
        "money": formatMoney,
    })

2. Compose The Root Partial Per Request

A wrapper partial can render one configured content child through {{ content }}. Create the tree inside the handler when the content, wrapper, dot data, or registered children are request-specific. Clone the configured root blueprint per request; per-request wiring should stay on the clone.

3. Define Partials

Create Partial instances for the content and any other components.

type ContentPage struct {
    PageTitle string
    Message string
}

type ShellPage struct {
    AppName string
}

func handler(w http.ResponseWriter, r *http.Request) {
    // Create the main content partial
    content := partial.NewID("content", "templates/content.html").
        SetDot(ContentPage{
            PageTitle: "Home Page",
            Message:   "Welcome to our website!",
        })
    
    // Clone the shared shell and attach request-specific content.
    page := root.Clone().
        SetDot(ShellPage{AppName: "My Application"})
    page.SetContent(content)
    
    output, err := partial.RenderWithRequest(r.Context(), r, page)
    if err != nil {
        http.Error(w, "An error occurred while rendering the page.", http.StatusInternalServerError)
        return
    }
    w.Write([]byte(output))
}

Template Files

templates/shell.html

<!DOCTYPE html>
<html>
<head>
    <title>{{.AppName}}</title>
</head>
<body>
    {{ content }}
</body>
</html>

templates/content.html

<h1>{{ .PageTitle }}</h1>
<p>{{ .Message }}</p>

Note: When a wrapper partial wraps content, it renders the configured route partial by calling {{ content }}.

Accessing Data in Templates

Use SetDot for application data:

{{ .PageTitle }}
{{ .Message }}

For shared application values, put them on a typed model and declare them with go-clue @model:

{{/*
@model App github.com/example/app.AppInfo
*/}}

{{ App.AppName }}
wrapper.SetModel(appInfo)

When SetDot is used, request-specific values are available through helpers instead of fields on dot (locale with the stage of exp/localization, csrf with that of exp/csrf):

{{ ctx.URL.Path }}
{{ url.Path }}
{{ request.Method }}
{{ locale }}
{{ csrf.Key }}
{{ basePath }}

Typed Contracts

go-partial can register go-clue typed root declarations before parsing templates. The declaration owns the template name, while the controller supplies the matching Go value:

{{/*
@model Page github.com/example/app.DashboardPage
*/}}

<h1>{{ Page.Title }}</h1>
content := partial.NewID("content", "templates/dashboard.gohtml").
    SetModel(page)

SetModel appends values to the typed roots already inherited through the partial tree. Use it when a partial adds local models on top of parent models. SetContract(annotation, values...) does the same thing for custom annotation names.

When more than one root has the same Go type, bind by name:

{{/*
@interaction LikesPoll github.com/donseba/go-partial/exp/interactions.Interaction
@interaction LikeButton github.com/donseba/go-partial/exp/interactions.Interaction
*/}}

{{ poll runtime LikesPoll }}
{{ refresh runtime LikeButton }}
content.SetContract("interaction",
    interactions.NewPoll("/posts/42/likes").As("LikesPoll").Every(5*time.Second),
    interactions.NewRefresh("/posts/42/likes").As("LikeButton").Target("#likes"),
)

Interaction roots are normal go-clue contracts registered with SetContract("interaction", values...). Use As(name) when the endpoint does not naturally produce the contract name; otherwise the last endpoint segment is capitalized, so /stats becomes Stats.

Contract names cannot collide with go-partial's own helpers, such as partial, content, ctx, or url (see Template Functions): a template that declares such a root fails to render.

Checking Contracts

CheckContracts tells whether a partial can render with the values it has, without rendering it: every typed root its templates declare finds one value on the partial or its parents, and its dot has the @dot type of its first template.

if err := fragment.CheckContracts(); err != nil {
    // this page cannot answer for the fragment
}

runtime.CheckContracts(p) checks a partial that is not attached yet, as a child of the partial being rendered; exp/slots uses it with slots.CheckContracts(). partial.ParseDeclarations(sources...) returns the @model, other typed and @dot declarations of template sources, for tools and templates rendered outside go-partial.

Checking Templates

CheckTemplates parses every template file of a partial's file system, in every folder, with the functions its renders have: those of SetFunc on the partial and its ancestors, go-partial's own helpers, those its render stages give (Use) and the typed roots the files declare. It reports every problem, not only the first: a file that does not parse, every call of a function the partial does not have, and every {{ template }} that names a template no file defines. Run it when the application starts, so a template that would fail its renders is known before the first request:

problems, err := root.CheckTemplates()
if err != nil {
    return err // reading the file system failed
}

for _, problem := range problems {
    log.Print(problem)
}
layout.gohtml:2:4: function "newsleterForm" not defined; did you mean "newsletterForm"?
layout.gohtml:3:13: no such template "footer"
regions/menu.gohtml:1: unexpected EOF: an {{if}}, {{range}}, {{with}}, {{block}} or {{define}} lacks its {{end}}

Each partial.TemplateProblem has the File, Line and Column (0 when the parser does not say), the Func or Template it names, a Suggestion with the closest defined name when one is close, and the Message; Position() gives file:line:col, and Error() the line above. For an Overlay, Layer is the index of the layer the file comes from, the first layer 0. Without patterns every *.gohtml file is checked; CheckTemplates("*.tmpl") chooses files by their base name, as path.Match does. A template named in any checked file counts as defined, as templates render with the templates of other files, such as those of SetIncludes. The problems are not errors: the error is only that of reading the file system.

go-partial keeps interaction template helpers in github.com/donseba/go-partial/exp/interactions. They are opt-in:

import "github.com/donseba/go-partial/exp/interactions"

root.SetFunc(interactions.FuncMap())

interactions.AttrsFuncMap() holds only the helpers that write attributes on an element of the template's own (revealAttrs, submitAttrs, clientAttrs, noBoost and the like), for applications that leave generic names such as poll, on or async to functions of their own.

Those functions include go-clue signature metadata for the overloads that normal Go function declarations cannot express. The repository .go-doc/config.json wires them into editor tooling for this repo; go-clue, formerly go-doc, still reads that file and the go-doc: comments. Helper calls such as {{ async runtime "/stats" }} and {{ async runtime Stats }} can both be completed and validated. The runtime argument is a per-render value injected by go-partial; it exposes request context, the active connector, partial tree, and diagnostic stages to helpers without package globals.

Use the inline endpoint form when the endpoint is naturally local to the template, especially inside loops:

{{ range .Rows }}
    {{ async runtime "/async/row/:row" "row" .ID }}
{{ end }}

Use a named interaction when Go should own stable IDs, targets, intervals, events, placeholders, or reuse:

{{/*
@interaction LikesPoll github.com/donseba/go-partial/exp/interactions.Interaction
*/}}
{{ poll runtime LikesPoll }}
content.SetContract("interaction",
    interactions.NewPoll("/posts/42/likes").As("LikesPoll").Every(5*time.Second),
)

To put an interaction on an element of the template's own, such as one that only polls while a job runs, use the attribute variants asyncAttrs, revealAttrs, pollAttrs, onAttrs, streamAttrs and refreshAttrs; submitAttrs and liveAttrs make a form post and an input request as the user types (see TEMPLATE_FUNCTIONS.md). Without a target or id they target the element itself:

<ul id="jobs"{{ if .Running }}{{ pollAttrs runtime "/jobs" "3s" }}{{ end }}>…</ul>

"select" picks the part of the response to swap (hx-select for htmx), and options whose names start with hx-, up-, data- or aria- are written on the element as given, replacing the connector's attribute of the same name:

<div {{ pollAttrs runtime "/inbox" "every" "10s" "select" "#messages" "hx-indicator" "#spinner" }}></div>

Template Composition

For repeated sections that should be understood by go-clue and also render as HTMX targets, prefer native templates plus SetDot. The parent owns the loop, and the nested template receives the row as normal dot data:

{{/*
@dot github.com/example/app.TablePage
*/}}
{{ range .Rows }}
    {{ template "row.html" . }}
    {{/* or: {{ template "/templates/row.html" . }} */}}
{{ end }}
<tr id="row-{{ .ID }}">
    <td>{{ .Name }}</td>
</tr>

Register the row partial on the parent so the parent parse can see the row template and so an HTMX request can still target a row by ID; the resolver answers through the stage of exp/target:

rowPartial := partial.NewID("row", "templates/row.html")
table.With(rowPartial).Use(target.Stage())
target.WithResolver(table, func(ctx context.Context, r *http.Request, target string) (*partial.Partial, bool) {
    row := findRowForTarget(target)
    return partial.NewID(target, "templates/row.html").SetDot(row), true
})

For wrapper partials, render the route content through content:

<main>{{ content }}</main>

Avoid using partial as a general row-composition helper in new code. Native template calls keep the template idiomatic and give go-clue the strongest type information. Use partial when you intentionally want to render a template path through go-partial's render path:

{{ partial runtime "templates/notice.gohtml" .Notice }}

Keep registered partials for HTMX targets, OOB output, selection/action rendering, and places where the browser can request a stable partial ID.

Using Out-of-Band (OOB) Partials

Out-of-Band partials allow you to update parts of the page without reloading:

Defining an OOB Partial
type Footer struct {
    Text string
}

// Create the OOB partial
footer := partial.New("templates/footer.html").ID("footer")
footer.SetDot(Footer{
    Text: "This is the footer",
})

// Add the OOB partial
p.WithOOB(footer)

A partial response sends the out-of-band partials in a fixed order, so the same page gives the same bytes on every render, which keeps ETags and response caches stable: those of the partial nearest the target first, then those of each ancestor up to the root, and each partial's own in the order WithOOB registered them. Registering an ID again keeps its place. slots.Set with slots.OOB() registers in the order it is called, and slots.Glob in the order of the file names, such as head, menu, title for a regions directory.

A region goes with partial requests for the partials below the one it is registered on, such as the content of a layout. With SetAlwaysSwapOOB(true), which slots.OOB() sets, it also goes with a request for that partial itself, such as a notice a page registers for a toast container of the layout: out of band after the partial's output, unless the output renders the region partial already, as a layout does with {{ slot }}. Markup of the region written another way, such as with {{ template }}, is not seen, so the region then comes twice.

page.WithOOB(partial.NewID("notice", "templates/notice.gohtml").SetAlwaysSwapOOB(true))
Using OOB Partials in Templates

In your templates, you can use the oobAttr function to conditionally render OOB attributes: the connector's, hx-swap-oob for htmx and up-hungry for Unpoly (see connector.OOBAttrs).

templates/footer.html

<div{{ oobAttr }} id="footer">{{ .Text }}</div>
Sending Only Some Elements Of A Region

SetOOBElements(ids...) makes a region that goes out of band send only the elements of its output with those IDs, each carrying the connector's out-of-band attribute, rather than its whole output, such as the items of a menu that mark the page being shown:

menu := partial.NewID("menu", "templates/menu.gohtml").SetOOBElements("menu-about", "menu-contact")
p.WithOOB(menu)

An element inside another one it sends goes along with that one. The region's own element, the one with {{ oobAttr }}, is not sent unless its ID is one of them. When none of the IDs is in the output, or the end of an element cannot be told, such as an <li> without its </li>, the whole region goes, as without IDs. On a full page the region renders whole.

SetOOBElementsFunc(fn) chooses the IDs per request. fn is called only when the region goes out of band, once per response; the request headers it names in Vary go to the response's Vary header then, and only then, so a full page or a fragment request does not vary with them. connector.CurrentURLHeader(conn) names the header in which the client library sends the URL of the page it shows, HX-Current-URL for htmx, which connector.CurrentURL(conn, r) reads:

menu.SetOOBElementsFunc(func(r *http.Request) partial.OOBElements {
    return partial.OOBElements{
        IDs:  changedItems(connector.CurrentURL(conn, r), r.URL),
        Vary: []string{connector.CurrentURLHeader(conn)},
    }
})

The two replace each other. partial.AddVary(w.Header(), names...) adds request headers to a response's Vary header, unless it lists them already.

A connector of your own whose client library sends such a header implements connector.CurrentURLHeaderNamer, CurrentURLHeader() string, as the htmx connectors do; connector.CurrentURLHeader returns "" for a connector without it, or a nil one.

A Region In The Head

A region of a layout placed in <head>, with the page's canonical link, description, alternate and feed links or structured data, cannot go out of band as other regions do: <head> holds no element to wrap it, and the number of its elements changes from page to page. head.Region(p) of exp/head makes a partial such a region. On a full page its output sits between two empty <template> markers (data-partial-head and data-partial-head-end, with the partial's ID); out of band it replaces whatever is between them, as a group, so the elements of the page left are removed rather than kept next to the new ones:

region := head.Region(partial.NewID("head", "regions/head.gohtml").SetDot(page))
slots.Set(root, "head", region, slots.OOB())
<head>
    <meta charset="utf-8">
    {{ slot "head" }}
    <link rel="stylesheet" href="/site.css">
</head>

With htmx 2 and 4 the region goes as two out-of-band swaps that need no extension: hx-swap-oob="delete:…" of the elements between the markers, and hx-swap-oob="beforebegin:…" of a <div> with the new elements before the end marker. A <title> in the region's output goes on its own, before the markers, where htmx sets the document's title from it. The region needs no ID and no {{ oobAttr }}; partials it renders are not affected. With a connector whose out-of-band attribute is not hx-swap-oob, such as Unpoly's, the region's output goes as it is.

A back or forward navigation that htmx restores from its history replaces the body and the title only: the head keeps the region's elements of the page left. With htmx 2, "historyCacheSize": 0 and "refreshOnHistoryMiss": true in its htmx-config meta element make such a navigation load the whole page instead.

Template Functions

You can add custom functions to be used within your templates:

import "strings"

// Define custom functions
funcs := template.FuncMap{
    "upper": strings.ToUpper,
}

// Merge the functions into this partial tree
p.SetFunc(funcs)

SetFunc registers helpers in the current scope. Function names inherited from the Root partial or parent partial remain available, and protected go-partial helper names cannot be overwritten.

Functions whose first parameter is a context.Context receive the context of the render; templates call them without it:

root.SetFunc(template.FuncMap{
    "posts": func(ctx context.Context, section string, limit int) ([]Post, error) {
        return store.Latest(ctx, section, limit)
    },
})
{{ range posts "news" 3 }}<li>{{ .Title }}</li>{{ end }}

The context is the one passed to Render, RenderWithRequest or Write, with what render stages added. Each function is bound once per render tree, however many partials call it. For templates go-partial does not execute, such as the text part of an e-mail, partial.BindContext(ctx, funcs...) binds such functions the same way.

partial.RuntimeFrom(ctx) gives such a function the runtime of the partial whose template calls it, so it can render a component as a true child of that partial, with its models, functions, stages and failure policy:

"card": func(ctx context.Context, card Card) (template.HTML, error) {
    runtime, ok := partial.RuntimeFrom(ctx)
    if !ok {
        return "", errors.New("card: not rendering")
    }
    return runtime.RenderPartial(partial.NewID("card", "card.gohtml").SetDot(card))
},

partial.ReservedFuncNames(stages...) lists the names SetFunc cannot give templates: go-partial's helpers and the functions the given stages give templates, such as locale for localization.Stage(). partial.IsReservedFuncName(name, stages...) also reports names starting with an underscore.

SetFunc warns with a func.protected event when it is given a function templates never call: one named like a go-partial helper, which it ignores, or like a function a stage of the partial or its ancestors gives templates; Use warns about registered functions a new stage replaces. A stage's own stand-ins, such as slots.FuncMap() for slots.Stage(), raise no warning: they come from the package the stage names in RenderStageHooks.Package, which a package that offers stand-ins for its stage's functions sets, such as to reflect.TypeFor[T]().PkgPath() for a type T of its own. Set the event sink before the functions to receive these warnings.

go-partial always reserves the helpers it injects for rendering and request state: runtime, partial, content, ctx, request, url, basePath, the URL helpers urlIs, urlStarts, urlContains, urlPath and joinPath, and the OOB helpers oob, oobAttr and oobSwap. The helpers of a stage, such as locale (exp/localization), csrf (exp/csrf), targetIs, selectionIs and actionIs (exp/target, exp/selection, exp/actions), slot or flash, are reserved while the stage is in use. Generic helpers such as dict, string helpers, and date helpers are ordinary template functions and may be replaced.

Optional helper providers live under exp/:

import (
    "github.com/donseba/go-partial/exp/flash"
    "github.com/donseba/go-partial/exp/interactions"
    "github.com/donseba/go-partial/exp/templatehelpers"
)

root.SetFunc(
    flash.FuncMap(),
    interactions.FuncMap(),
    templatehelpers.StringFuncMap(),
    templatehelpers.CollectionFuncMap(),
)
root.Use(flash.Stage())

The helpers of interactions and templatehelpers work with SetFunc alone. Those of flash.FuncMap() render nothing without flash.Stage(), which gives templates the real ones; the map gives go-clue their signatures.

For go-clue, point provider discovery at the same packages:

{
  "providers": [
    "github.com/donseba/go-partial/exp/flash",
    "github.com/donseba/go-partial/exp/interactions",
    "github.com/donseba/go-partial/exp/templatehelpers"
  ]
}
Usage in Template:
{{ upper .Message }}
Using a Custom File System

If your templates are stored in a custom file system, set it with SetFileSystem:

import (
    "embed"
)

//go:embed templates/*
var templatesFS embed.FS

p.SetFileSystem(templatesFS)

If you do not use a custom file system, the package will use the default file system and look for templates relative to the current working directory.

Templates From Memory And Overlays

partial.Sources is a file system of template sources held in memory, for markup a package ships, and partial.Overlay looks files up in each layer in turn. Together they let applications replace a default template with a file of their own:

var defaults = partial.Sources{
    "site/share.gohtml": `<nav class="share">{{ range .Links }}<a href="{{ .URL }}">{{ .Label }}</a>{{ end }}</nav>`,
}

share := partial.NewID("share", "site/share.gohtml").
    SetFileSystem(partial.Overlay(themeFiles, defaults)).
    SetDot(bar)

Partials, {{ partial }}, {{ template }}, typed models and the template cache work as with any file system. Directories list the files of every layer. The cache tells file systems apart by identity: create Sources once rather than per render. Overlays are identified by their layers, so an overlay may be built per render.

partial.Layers(fsys) returns the layers of an overlay, in the order it looks files up in, and any other file system alone. Use it to add a layer below an overlay without nesting one overlay in another:

fsys := partial.Overlay(append(partial.Layers(theme), defaults)...)

To render a template a file defines, rather than the file itself, such as the html block of an e-mail template, add a source that calls it and list the file after it:

entry := partial.Sources{"mail-html.gohtml": `{{ template "html" . }}`}
html := partial.NewID("mail", "mail-html.gohtml", "mail/confirmation.gohtml").
    SetFileSystem(partial.Overlay(entry, themeFiles)).
    SetDot(data)

Rendering Tables and Dynamic Content

For tables and repeated fragments, prefer native Go templates plus SetDot. The parent receives a typed page model, ranges over rows, and calls the row template with {{ template "row.html" . }}. That keeps the template readable for go-clue while go-partial still knows the row partial ID for HTMX target requests.

Example: Rendering a Table with Dynamic Rows

templates/table.html

<table>
    {{ range .Rows }}
        {{ template "row.html" . }}
    {{ end }}
</table>

templates/row.html

<tr id="row-{{ .ID }}">
    <td>{{ .Name }}</td>
</tr>

Go Code:

type Row struct {
    ID   int
    Name string
}

type TablePage struct {
    Rows []Row
}

rowPartial := partial.NewID("row", "templates/row.html")
tablePartial := partial.NewID("table", "templates/table.html").
    SetDot(TablePage{Rows: rows})
tablePartial.With(rowPartial).Use(target.Stage())

target.WithResolver(tablePartial, func(ctx context.Context, r *http.Request, target string) (*partial.Partial, bool) {
    row, ok := findRowForTarget(target)
    if !ok {
        return nil, false
    }
    return partial.NewID(target, "templates/row.html").SetDot(row), true
})

out, err := partial.RenderWithRequest(r.Context(), r, tablePartial)

If the child does not need custom configuration, use WithTemplate as shorthand:

tablePartial := partial.NewID("table", "templates/table.html").
    WithTemplate("templates/row.html").
    SetDot(TablePage{Rows: rows})

This is equivalent to tablePartial.With(partial.NewID("row", "templates/row.html")).

Template Data

In your templates, prefer this model:

  • {{.}}: Your app model when the partial uses SetDot.
  • Typed roots: Additional typed values registered with SetModel or SetContract.
  • {{ctx}}, {{request}}, {{url}}, {{locale}}, {{csrf}}, {{basePath}}: request-aware helpers that stay available when SetDot changes . (locale and csrf with the stages of exp/localization and exp/csrf).

go-partial does not wrap your model in .Data, .App, .Shell, or .Global. Shared application values should be explicit typed roots, for example SetModel(appInfo) with a matching go-clue declaration. Request-scoped values live behind helper functions so changing dot never hides them.

Concurrency and Template Caching

Configure reusable root partials, functions, render stages, headers, and filesystems before serving requests. Clone before adding request-specific content or dot data. After configuration, partial.RenderWithRequest and partial.Write can be called concurrently on cloned partial trees. Request-specific values such as request, url, ctx, runtime, stage values, selected targets, and template helper bindings are scoped to the active render and are not stored on the reusable partial configuration.

In HTTP handlers, pass the request context, normally r.Context() or a context derived from it. That lets cancellation, deadlines, localization, CSRF state, event sinks, metrics IDs, and other middleware values follow the render. partial.Render(ctx, p) without a request is for tests, offline rendering, and small utilities.

Do not call configuration methods such as SetFunc, Use, With, SetDot, SetFileSystem, SetConnector, or SetResponseHeaders while the same tree is being rendered. Build or clone a separate tree when configuration needs to change at runtime.

Mutable builder and stream/session helpers are intentionally single-owner values. Do not share connector.ResponseBuilder, connector.Trigger, connector.Swap, sse.Writer, or pageflow.SessionData across goroutines without your own synchronization.

The package also includes concurrency safety measures for template caching:

  • Parsed templates are cached by the configured partial tree.
  • Template files are scanned once per file system and tree root, so partials built per request under a configured parent (SetParent) read no files after the first.
  • Clones share the registered functions until either registers more.
  • Mutexes prevent duplicate parsing for the same Root partial/template/function shape.
  • Cached templates are rebound with request-specific functions per render.
  • Rendered HTML is not cached.
  • Set UseTemplateCache to true to enable parsed template caching; the partials rendered under a partial inherit its setting.
root.UseTemplateCache(true)

Handling Partial Rendering via HTTP Headers

You can render specific partials based on the target header the connector reads: X-Target with the default connector (connector.NewPartial), HX-Target with htmx, which also sends HX-Request: true.

Example:

func handler(w http.ResponseWriter, r *http.Request) {
    page := root.Clone().SetContent(content)
    output, err := partial.RenderWithRequest(r.Context(), r, page)
    if err != nil {
        http.Error(w, "An error occurred while rendering the page.", http.StatusInternalServerError)
        return
    }
    w.Write([]byte(output))
}

To request a specific partial:

curl -H "X-Target: sidebar" http://localhost:8080

# With connector.NewHTMX:
curl -H "HX-Request: true" -H "HX-Target: sidebar" http://localhost:8080

Useless benchmark results

with caching enabled

goos: darwin
goarch: arm64
pkg: github.com/donseba/go-partial
cpu: Apple M2 Pro
BenchmarkRenderWithRequest
BenchmarkRenderWithRequest-12    	  526102	      2254 ns/op
PASS

with caching disabled

goos: darwin
goarch: arm64
pkg: github.com/donseba/go-partial
cpu: Apple M2 Pro
BenchmarkRenderWithRequest
BenchmarkRenderWithRequest-12    	   57529	     19891 ns/op
PASS

which would mean that caching is roughly 9-10 times faster than without caching

Upgrading

From v0.14.0

Compile breaks

  • slots.Set and debug.Stage take options as a last, variadic parameter (slots.Option, debug.Option). Calls compile as before; only a function value of the old type, such as var stage func() partial.RenderStage = debug.Stage, does not.
  • New fields in partial.RenderStageHooks (ResolveFunc, FuncNames, FallbackFuncNames), connector.Interaction (Select, Attrs), flash.Data (RequestErrorAttrs) and exterrors.Data (Title, Lang and Status, after Message): struct literals of these types that list the fields without their names no longer compile. Name the fields.

Responses of Write

  • Write adds the request headers the connector reads to the Vary header of every response: for htmx HX-Request, HX-Boosted, HX-History-Restore-Request, HX-Target, X-Select and X-Action, for htmx 4 also HX-Request-Type; before, it set no Vary. Caches and CDNs so keep a page and its fragments apart; check caching rules that ignore or rewrite Vary.
  • Write sends Content-Type: text/html; charset=utf-8 unless a header already sets it; before, only failure responses had it, and net/http guessed the type of the others from their first bytes.
  • When an error stage, such as ext/errors, answers a failed render, Write sends the failure response with Cache-Control: no-store and returns a *partial.FailureResponseError that wraps the render error; before, it returned the render error itself. A comparison such as err == renderErr no longer matches: use errors.Is, and errors.As to tell that the response has been written.

Targets and out-of-band regions

  • Out-of-band partials go in the order they were registered with WithOOB (slots.Glob: the order of the file names), nearest partial first; before, the regions of one partial came in a different order from one render to the next. A request that targets an ID found in more than one branch of the tree now always finds the same partial, searching the children in the order of their IDs.
  • A target that a render stage resolves, such as a partial an exp/target resolver returns, also gets the out-of-band regions of the partial that resolved it, as a registered child does; before, only those of that partial's ancestors were added. Such responses can carry more regions.
  • SetAlwaysSwapOOB(true), which slots.OOB() sets, takes effect: such a region also goes out of band with a partial request for the partial it is registered on, after that partial's output, unless the output places it already, as a layout does with {{ slot }}. Before, the setting did nothing, and a region went only with requests for the partials below the one it is registered on. Responses to requests for a layout or page with such regions that it does not place now carry them; register a region without the setting where it should not go.
  • {{ oobAttr }} writes the connector's out-of-band attribute, as oobSwap and SetOOBElements do: up-hungry="" for Unpoly, where it wrote hx-swap-oob. htmx, Turbo and connectors without an attribute of their own keep hx-swap-oob; the swap value given is now HTML-escaped.
  • oobSwap is a go-partial helper now: SetFunc ignores a function of that name, and a template that declares a typed root named oobSwap fails to render. Rename them.

Template cache

  • UseTemplateCache applies to the partials rendered under a partial that do not set it themselves, such as its children, its content and the partials of exp/flash; before, each partial used its own setting, so with only the root cached the others rendered without the cache. Edits to their template files no longer show until the root is built again: turn the cache off in development, for example with root.UseTemplateCache(!development).
  • With the cache on, a file that {{ partial "path" }} does not find is remembered as missing, as a file it found already was; a file added later is not seen by that tree.
  • A file system the cache cannot tell apart from others of its type, such as a struct that holds a map, no longer shares cached parses: its templates are parsed on every render, so renders over it are slower. Pass a pointer to such a struct instead.

Template functions

  • A function registered with SetFunc whose first parameter is a context.Context receives the render's context: templates call it without that argument. A template that passed a context itself now gives one argument too many and fails; drop the argument.
  • SetFunc and Use emit the func.protected warning, which was declared but never sent: for a function named like a go-partial helper, which SetFunc ignores as before, or like a function a stage gives templates, and for a registered function a new stage replaces. Expect new warnings; rename the functions they name.
  • The stages of exp/target, exp/slots, exp/localization, exp/flash, exp/csrf, exp/selection, exp/actions and ext/logger give their helpers through RenderStageHooks.ResolveFunc, only to the templates that call them; they are no longer in RenderContext.Funcs. A stage of the application's own that read them there gets them with RenderContext.ResolveFunc(name) in its Render hook.
  • While their stage is in use, flashTemplate and flashTargetAttrs (flash.Stage), and requiredSlot and deferredSlot (slots.Stage), are reserved names: the stage gives templates these functions, so a function the application registers under such a name is no longer called.
  • {{ partial }} runs the render stages of the calling partial once for the template it renders; before, they ran twice. Stages that count or record renders see one render per call.

Per package

  • exp/slots: {{ slot }} and {{ hasSlot }} look a slot up through every ancestor of the partial that places it, the nearest first; before, only on the nearest partial that had slots, so a slot set on the layout rendered "" in a content partial with slots of its own. slots.Set on a partial that inherits slots, or on a clone, no longer changes the slots of its ancestor or of the original.
  • exp/sse: Writer.PatchPartial renders the partial itself (partial.RenderSelf); before, it used RenderWithRequest, which for a partial request, such as one of htmx, looked up the target the request names and added out-of-band regions.
  • exp/interactions: in the endpoint form, "select" becomes hx-select with the htmx connectors and data-select with connector.NewPartial, and options whose names start with hx-, up-, data- or aria- are written on the element as given, after the connector's own attributes, which they replace; before, both were ignored. With htmx 2 and 4, the options include, pushURL and replaceURL of an interaction become hx-include, hx-push-url and hx-replace-url; with Unpoly, the target this is written as :origin.
  • interactions.FuncMap() holds 11 more helpers (asyncAttrs, revealAttrs, pollAttrs, onAttrs, streamAttrs, refreshAttrs, submitAttrs, liveAttrs, clientAttrs, noBoost, confirmAttr), which replace functions of those names registered before it. Register such a function after FuncMap().
  • flash.FuncMap() holds flashTemplate and flashTargetAttrs, and slots.FuncMap() holds requiredSlot and deferredSlot, which replace functions of those names registered before them, as above.
  • exp/target: target.Stage() leaves a target that an earlier stage resolved alone; before, its resolver replaced it.
  • ext/errors in ModeSafe, the default: the built-in error page and fragment of a failed render show the status text (Data.Title), also as the page's <title>, and exterrors.SafeMessage; before, they showed "Template render error" with the partial ID and template names. BuildData in ModeSafe sets Message to SafeMessage and leaves Location, PartialID and Templates empty, so markup of the application's own that prints them no longer shows the error; Data.Error still holds it. ModeDetailed is unchanged.
  • ext/errors: the built-in error fragment has classes instead of style attributes, which a Content-Security-Policy without 'unsafe-inline' blocks: go-partial-error (as before), and the new go-partial-error__title (h1), go-partial-error__message (p) and go-partial-error__details (dl). It no longer styles itself: serve and link the stylesheet of exterrors.StylesheetFS() (partial-errors.css) or style the classes in the application's CSS; unstyled it reads as a heading, a paragraph and a list. The built-in error page keeps its inline <style>, whose hash is exterrors.StyleHash().
  • ext/debug: the debug box has classes (go-partial-debug__header, __label, __partial, __value) and one <style> element of its own instead of style attributes; it looks the same. Under a strict policy, add debug.StyleHash() to style-src.
  • ext/logger: logger.Sink(nil) writes to slog.Default() as it is when each event is written; before, to the default logger of the moment Sink was called, so a later slog.SetDefault now takes effect.
  • exp/otel: go-partial requires OpenTelemetry Go v1.45.0, which fixes GHSA-8wmf-6v46-5gfg (exporter endpoints in verbose internal logs). A module that requires an older version moves up to it.

Contributing

Contributions are welcome! Please open an issue or submit a pull request with your improvements.

License

This project is licensed under the MIT License.

Documentation

Overview

Package partial provides request-aware rendering for Go templates.

Partial trees, Stages, connectors, and template caches are safe to reuse across concurrent renders after configuration is complete. Request-scoped state is carried by RenderContext and is not stored on the reusable Partial configuration.

Configuration methods such as SetFunc, Use, With, SetDot, SetFileSystem, SetConnector, SetResponseHeaders, and extension setup helpers mutate the partial they are called on. Call them before serving requests, or synchronize access externally when changing configuration at runtime.

Index

Constants

View Source
const (
	// EventRenderStart is emitted before a render chain runs.
	EventRenderStart = "render.start"
	// EventRenderFinish is emitted after a render chain succeeds.
	EventRenderFinish = "render.finish"
	// EventRenderError is emitted after a render chain fails.
	EventRenderError = "render.error"
	// EventRenderWriteError is emitted when writing a render response fails.
	EventRenderWriteError = "render.write_error"
	// EventRenderOOBError is emitted when an out-of-band child render fails.
	EventRenderOOBError = "render.oob_error"
	// EventTemplateMissing is emitted when a helper references a missing template.
	EventTemplateMissing = "template.missing"
	// EventTemplateParseError is emitted when parsing or loading templates fails.
	EventTemplateParseError = "template.parse_error"
	// EventTemplateExecuteError is emitted when template execution fails.
	EventTemplateExecuteError = "template.execute_error"
	// EventFuncProtected is emitted, as a warning with the name in the
	// "function" field, when SetFunc is given a function templates never
	// call: one named like a go-partial helper, or like a function a render
	// stage of the partial or its ancestors gives templates; and when Use
	// adds a stage that replaces a registered function.
	EventFuncProtected = "func.protected"
	// EventContentMissing is emitted when content is called without a content child.
	EventContentMissing = "content.missing"
	// EventTargetMissing is emitted when a requested target cannot be resolved.
	EventTargetMissing = "target.missing"
	// EventTargetFallback is emitted when the target fallback answers a
	// request for a target that cannot be resolved; see SetTargetFallback.
	EventTargetFallback = "target.fallback"
	// EventContractInvalid is emitted when contract data or helper arguments are invalid.
	EventContractInvalid = "contract.invalid"
)

Variables

This section is empty.

Functions

func AddVary added in v0.15.0

func AddVary(header http.Header, names ...string)

AddVary adds names to the Vary header, unless it lists them, or "*", already, for a response that depends on those request headers. Names that are empty are left out.

partial.AddVary(w.Header(), connector.CurrentURLHeader(conn))

func BindContext added in v0.15.0

func BindContext(ctx context.Context, funcs ...template.FuncMap) template.FuncMap

BindContext returns funcs for templates that go-partial does not execute, such as the text part of an e-mail: functions whose first parameter is a context.Context are given ctx, the others are returned as they are. Later maps replace names of earlier ones. Inside a render, functions the render already bound are reused.

func IsReservedFuncName added in v0.15.0

func IsReservedFuncName(name string, stages ...RenderStage) bool

IsReservedFuncName reports whether templates cannot be given a function named name: one of ReservedFuncNames(stages...), or a name that starts with an underscore.

func Layers added in v0.15.0

func Layers(fsys fs.FS) []fs.FS

Layers returns the layers of fsys when it is an Overlay, in the order it looks files up in, and fsys alone otherwise; nil for a nil fsys. Use it to add a layer below an overlay without nesting one overlay in another, which FirstExistingIn would try as one layer:

partial.Overlay(append(partial.Layers(theme), defaults)...)

func Overlay added in v0.15.0

func Overlay(layers ...fs.FS) fs.FS

Overlay returns a file system that looks files up in each layer in turn and uses the first that has them; directories list the files of every layer. Use it to let applications replace templates a package ships, or to add in-memory Sources to a directory of templates:

partial.Overlay(os.DirFS("theme"), defaults)

Overlays of the same layers share their entries in the template cache, so an overlay may be built for every render, as long as its layers are not; see Sources.

func Render added in v0.9.0

func Render(ctx context.Context, p *Partial) (template.HTML, error)

Render renders a partial without an http.Request.

Use Render for tests, offline rendering, and jobs that do not have request data. HTTP handlers should usually use Write, or RenderWithRequest when the caller needs the HTML string instead of a written response.

func RenderSelf added in v0.15.0

func RenderSelf(ctx context.Context, r *http.Request, p *Partial) (template.HTML, error)

RenderSelf renders the partial itself with request data, whatever target the request asks for: for a part of a page that reaches the browser another way, such as a server-sent event, or a page rendered for a request that was made for another one. Templates see the request as they do with RenderWithRequest; no out-of-band regions are added.

func RenderText added in v0.15.0

func RenderText(ctx context.Context, p *Partial) (string, error)

RenderText renders the partial as plain text with text/template, for text that is not HTML, such as the subject and the text part of an e-mail: nothing is escaped. Everything else is as for Render: the partial's file system, includes, entry, dot, @model values, functions (those that take a context.Context receive ctx) and template cache, and the render stages of the partial and its ancestors, which give templates their functions and see RenderKindText. A file can hold the text and the HTML of a message side by side:

mail := partial.NewID("mail", "mail/welcome.gohtml").SetParent(root).SetDot(data)
subject, err := partial.RenderText(ctx, mail.SetEntry("subject"))
text, err := partial.RenderText(ctx, mail.SetEntry("text"))
html, err := partial.RenderSelf(ctx, nil, mail.SetEntry("html"))

Text templates call other templates with {{ template }}, from the files of the partial or of its includes (see SetIncludes); go-partial's {{ partial }} and {{ content }} render HTML.

func RenderWithRequest added in v0.9.0

func RenderWithRequest(ctx context.Context, r *http.Request, p *Partial) (template.HTML, error)

RenderWithRequest renders a partial using request-aware connector behavior.

When the connector identifies the request as a partial request, this renders the requested target and appends eligible out-of-band regions. For normal requests it renders the partial itself.

func ReservedFuncNames added in v0.15.0

func ReservedFuncNames(stages ...RenderStage) []string

ReservedFuncNames returns, sorted, the template function names that SetFunc cannot give templates: go-partial's own helpers, such as partial, content, ctx and url, and the names the given render stages give templates, which replace registered functions of the same name. Names that start with an underscore are reserved as well; see IsReservedFuncName.

func WithEventSink added in v0.9.0

func WithEventSink(ctx context.Context, sink EventSink) context.Context

WithEventSink attaches a request-scoped event sink to ctx.

Request-scoped sinks are fanned out with partial tree sinks configured with Partial.SetEvents. If the sink owns goroutines, the caller still owns closing it, usually with defer in middleware.

If ctx already has a request-scoped sink, the new sink is appended with fanout.

func Write added in v0.9.0

Write renders a partial and writes the HTTP response.

Write owns the response side of rendering: configured response headers, connector response headers, render-stage response metadata, error fragments, and out-of-band regions are applied here. The response varies with the request headers the connector reads, which Write adds to the Vary header. Unless a header sets it, the content type is text/html; charset=utf-8.

The page is rendered before anything is written. With the FailResponse policy (see SetFailurePolicy), a failing part of the page fails the whole response. When the render fails and an error stage, such as ext/errors, renders a failure response, Write writes it, never cached, and returns a *FailureResponseError; otherwise it writes nothing and returns the error.

func WriteError added in v0.15.0

func WriteError(w http.ResponseWriter, r *http.Request, p *Partial, status int, err error) error

WriteError answers r with the failure response of the error stage of p's tree, such as ext/errors, for err, an error that happened outside a render, such as a form post whose handler failed to save it: the error page, or for a partial request the error fragment or flash message, as Write answers a render that failed, written with status. The error stage renders with p's functions, includes and models, so an error template looks as it does for a render that failed. A status of 0 or less is 500, and a nil err the text of the status.

if err := save(r); err != nil {
	if werr := partial.WriteError(w, r, root, http.StatusInternalServerError, err); werr != nil {
		http.Error(w, "Something went wrong", http.StatusInternalServerError)
	}
}

The response is never cached, and varies with the request headers the connector reads. WriteError returns nil when it answered the request. Otherwise it wrote nothing and returns why: err itself when the tree has no error stage that answers it, so the caller answers it another way. A failed write of the answer, as to a client that has gone, is emitted as an EventRenderWriteError event. ext/errors' Respond answers with its built-in markup when no error stage does. The error stage renders with the context of r.

Types

type AsyncEvents added in v0.9.0

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

AsyncEvents is a non-blocking event dispatcher backed by a bounded queue.

func NewAsyncEvents added in v0.9.0

func NewAsyncEvents(cfg EventsConfig, sinks ...EventSink) *AsyncEvents

NewAsyncEvents returns a non-blocking dispatcher for diagnostic events.

func (*AsyncEvents) Close added in v0.9.0

func (events *AsyncEvents) Close(ctx context.Context) error

Close stops workers after delivering queued events, or returns when ctx ends.

func (*AsyncEvents) Dropped added in v0.9.0

func (events *AsyncEvents) Dropped() uint64

Dropped returns the number of events dropped by this dispatcher.

func (*AsyncEvents) Emit added in v0.9.0

func (events *AsyncEvents) Emit(ctx *RenderContext, event Event)

Emit enqueues event without blocking unless configured with Block.

type Declaration added in v0.15.0

type Declaration struct {
	Annotation string
	Name       string
	// Type is the declared Go type as package path and name, also when it
	// was written as path/Name.
	Type string
}

Declaration is one typed declaration in a template comment, such as "@model Page github.com/acme/app.Page" or "@dot github.com/acme/app.Row". A @dot declaration has the annotation "dot" and no name.

func ParseDeclarations added in v0.15.0

func ParseDeclarations(sources ...string) ([]Declaration, error)

ParseDeclarations returns the typed declarations of template sources, in the order they appear: the typed roots go-partial binds with SetModel and SetContract, and the @dot types. A root declared twice with one type is listed once; a root declared with two types is an error, as it is when the templates render.

type Dot added in v0.15.0

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

Dot is the dot of a partial as a value, for code that passes it around before a partial exists, such as in a field of a struct: computed when the partial renders (DotFuncOf), keeping its type for CheckContracts. The zero Dot is no dot. Give it to a partial with SetDotSource.

type Panel struct {
	ID   string
	Dot  partial.Dot
}

panel := Panel{ID: "stats", Dot: partial.DotFuncOf(loadStats)}
partial.NewID(panel.ID, "stats.gohtml").SetDotSource(panel.Dot)

func DotFuncOf added in v0.15.0

func DotFuncOf[T any](fn func(ctx context.Context) (T, error)) Dot

DotFuncOf returns a Dot computed by fn only when the partial renders: on the page, or on its own as the target of a partial request. A part of a page whose data is expensive, such as a panel of a dashboard, then costs nothing in responses that leave it out. fn receives the context of the render and is called once per render tree, also when several partials inherit the dot; an error fails the render of the partial. A nil fn is no dot.

As T is known before the dot is computed, CheckContracts checks it against the @dot declaration of the partial's template without calling fn, so it tells whether the partial suits a request without computing its data.

stats := partial.DotFuncOf(func(ctx context.Context) (Stats, error) {
	return loadStats(ctx)
})
panel := partial.NewID("stats", "stats.gohtml").SetDotSource(stats)

func (Dot) IsZero added in v0.15.0

func (d Dot) IsZero() bool

IsZero reports whether the Dot is no dot.

func (Dot) Model added in v0.15.0

func (d Dot) Model() ModelFunc

Model returns the dot as a model, sharing its value: it is computed once per render tree, also when a partial renders it as its dot and a template calls it as a model, such as a layout's dot and the regions that declare it with @model. A Dot of DotFuncOf for an interface type, whose values' type is not known before they are computed, and the zero Dot are no model, the zero ModelFunc, which SetModel ignores.

filled := partial.DotFuncOf(fillView)
page.SetDotSource(filled).SetModel(filled.Model())

type DropPolicy added in v0.9.0

type DropPolicy string

DropPolicy decides what AsyncEvents does when its buffer is full.

const (
	// DropNewest drops the incoming event when the queue is full.
	DropNewest DropPolicy = "drop_newest"
	// DropOldest drops one queued event to make room for the incoming event.
	DropOldest DropPolicy = "drop_oldest"
	// Block waits for queue space or Close. Use only when event loss is worse than render latency.
	Block DropPolicy = "block"
)

type Event added in v0.9.0

type Event struct {
	Time         time.Time
	Kind         string
	Level        EventLevel
	Message      string
	Error        error
	Fields       map[string]any
	TraceID      string
	SpanID       string
	ParentSpanID string
	PartialID    string
	ParentID     string
	Name         string
}

Event describes something observable that happened during rendering.

type EventLevel added in v0.9.0

type EventLevel string

EventLevel describes the severity of a diagnostic event.

const (
	// EventDebug is useful for local tracing and lifecycle views.
	EventDebug EventLevel = "debug"
	// EventInfo describes normal diagnostic breadcrumbs.
	EventInfo EventLevel = "info"
	// EventWarn describes recoverable problems or ignored configuration.
	EventWarn EventLevel = "warn"
	// EventError describes render failures or other errors.
	EventError EventLevel = "error"
)

type EventSink added in v0.9.0

type EventSink interface {
	Emit(*RenderContext, Event)
}

EventSink receives diagnostic events. Sinks should be defensive and cheap.

func EventSinkFromContext added in v0.9.0

func EventSinkFromContext(ctx context.Context) EventSink

EventSinkFromContext returns the request-scoped event sink attached to ctx.

type EventSinkFunc added in v0.9.0

type EventSinkFunc func(*RenderContext, Event)

EventSinkFunc adapts a function to EventSink.

func (EventSinkFunc) Emit added in v0.9.0

func (f EventSinkFunc) Emit(ctx *RenderContext, event Event)

Emit sends event to the wrapped function.

type EventsConfig added in v0.9.0

type EventsConfig struct {
	Buffer     int
	DropPolicy DropPolicy
	Workers    int
}

EventsConfig configures AsyncEvents.

type Factory added in v0.10.0

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

Factory creates request-scoped partials from a configured prototype. The returned values are ordinary *Partial instances and can use the complete native API directly.

func NewFactory added in v0.10.0

func NewFactory(prototype *Partial) *Factory

NewFactory creates a factory from prototype. Later changes to prototype do not affect the factory.

func (*Factory) New added in v0.10.0

func (f *Factory) New(templates ...string) *Partial

New creates a partial with the factory configuration and template paths.

func (*Factory) NewID added in v0.10.0

func (f *Factory) NewID(id string, templates ...string) *Partial

NewID creates a named partial with the factory configuration and template paths.

type FailurePolicy added in v0.15.0

type FailurePolicy int

FailurePolicy decides what a failing part of a render does to the whole render; see Partial.SetFailurePolicy.

const (
	// FailInline renders a failing part of a page, such as the content
	// child, a {{ partial }} call or a component rendered by a stage, as the
	// error fragment of the registered error stage, or as an error message,
	// in place; the rest of the page renders. This is the default.
	FailInline FailurePolicy = iota

	// FailResponse fails the whole render when any part of it fails, and
	// also when a template, child or content partial it asks for is
	// missing. Render, RenderWithRequest and RenderSelf return the first
	// failure as their error and no HTML. Write writes no part of the page:
	// it writes the failure response of a registered error stage, such as
	// ext/errors, with an error status also for partial requests and
	// without out-of-band regions, and returns a *FailureResponseError; or
	// it writes nothing at all and returns the error, so the caller can
	// answer it. Use it where showing a partly broken page is worse than an
	// error page, such as on a public website.
	FailResponse
)

type FailureResponseError added in v0.15.0

type FailureResponseError struct {
	Status int
	Err    error
}

FailureResponseError is the error Write returns when a render failed and Write answered the request with the failure response of an error stage, such as ext/errors: the response is written, and the caller must not write another. Without an error stage, Write writes nothing and returns the render error itself. Err is the render error and Status the status written.

err := partial.Write(ctx, w, r, page)
var written *partial.FailureResponseError
if err != nil && !errors.As(err, &written) {
	http.Error(w, "Something went wrong", http.StatusInternalServerError)
}

func (*FailureResponseError) Error added in v0.15.0

func (e *FailureResponseError) Error() string

func (*FailureResponseError) Unwrap added in v0.15.0

func (e *FailureResponseError) Unwrap() error

type FanoutEventSink added in v0.9.0

type FanoutEventSink []EventSink

FanoutEventSink forwards events to multiple sinks.

func FanoutEvents added in v0.9.0

func FanoutEvents(sinks ...EventSink) FanoutEventSink

FanoutEvents returns a sink that forwards events to each non-nil sink.

func (FanoutEventSink) Emit added in v0.9.0

func (sinks FanoutEventSink) Emit(ctx *RenderContext, event Event)

Emit sends event to each configured sink.

type ModelFunc added in v0.15.0

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

ModelFunc is a typed model computed only when a template calls it, as a value: SetModel and SetContract take it with other models, such as from a list of models an application passes around before a partial exists. Make one with ModelFuncOf; the zero ModelFunc is no model, which SetModel ignores.

func ModelFuncOf added in v0.15.0

func ModelFuncOf[T any](fn func(ctx context.Context) (T, error)) ModelFunc

ModelFuncOf returns a model of the type T computed by fn only when a template of the partial or of a partial rendered under it calls the model, such as the navigation that a layout, its header and its sidebar each declare with @model Nav github.com/acme/app.Navigation: fn is called once per render tree, also when several partials call the model, and not at all for a response that leaves the templates that declare it out. An error fails the template that calls the model. It is matched by T, as SetModel matches values, without calling fn, also by CheckContracts; T must be a concrete type. A nil fn is no model.

models := []any{user, partial.ModelFuncOf(func(ctx context.Context) (SearchBox, error) {
	return searchBox(ctx)
})}
page.SetModel(models...)

func (ModelFunc) IsZero added in v0.15.0

func (m ModelFunc) IsZero() bool

IsZero reports whether the ModelFunc is no model.

type NamedContract added in v0.9.0

type NamedContract interface {
	ContractName() string
}

NamedContract lets values choose their go-doc contract name.

type OOBElements added in v0.15.0

type OOBElements struct {
	// IDs are the IDs of the elements that go out of band, as
	// SetOOBElements takes them; none sends the whole output.
	IDs []string
	// Vary names the request headers the choice depends on, such as the
	// header in which the client library sends the URL of the page it
	// shows (connector.CurrentURLHeader). Write adds them to the Vary
	// header of the response, so caches keep the responses apart; names
	// that are empty are left out.
	Vary []string
}

OOBElements is what the function of SetOOBElementsFunc chooses for a request: the elements of a region that go out of band, and the request headers the choice depends on.

type Partial

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

Partial stores reusable template, data, and child-tree configuration.

func New

func New(templates ...string) *Partial

New creates a root partial with the default ID "root".

func NewID

func NewID(id string, templates ...string) *Partial

NewID creates a partial with the provided ID.

func (*Partial) CheckContracts added in v0.15.0

func (p *Partial) CheckContracts() error

CheckContracts reports whether the partial can render with the values it has, without rendering it: every typed root its templates declare, such as @model Page, has exactly one matching value among those set on the partial and its parents, and its dot is of the type the first template declares with @dot. A @dot of any or interface{} accepts every dot. It returns the first problem found, or nil.

Use it to decide whether a partial suits a request before committing to it, for example in a target resolver; see Runtime.CheckContracts for a partial that is not attached to the tree yet.

func (*Partial) CheckTemplates added in v0.15.0

func (p *Partial) CheckTemplates(patterns ...string) ([]TemplateProblem, error)

CheckTemplates parses every template file of the partial's file system, in every folder, with the functions its renders have: those registered with SetFunc on the partial and its ancestors, go-partial's own helpers, those the render stages of the partial and its ancestors give (see Use) and the typed roots the files declare. It reports every problem, not only the first: a file that does not parse, every call of a function the partial does not have, and every {{template}} that names a template no file defines, each with its file, line and column, and for an Overlay the layer the file comes from. Run it when the application starts, so a template that would fail its renders is known before the first request:

problems, err := root.CheckTemplates()
for _, problem := range problems {
	log.Print(problem)
}

patterns choose the files by their base name, as path.Match does, such as "*.tmpl"; without, every file named *.gohtml is a template. Templates render with the templates of other files, such as those of SetIncludes, so a template named in any checked file counts as defined. The error is that of reading the file system; the problems are not errors.

func (*Partial) ClearDot added in v0.9.0

func (p *Partial) ClearDot() *Partial

ClearDot removes the explicit root value.

func (*Partial) Clone added in v0.2.0

func (p *Partial) Clone() *Partial

Clone returns an independent copy of the partial configuration and child tree.

func (*Partial) Connector added in v0.15.0

func (p *Partial) Connector() connector.Connector

Connector returns the connector the partial uses: its own, its nearest parent's, or connector.NewPartial(nil).

func (*Partial) Defines added in v0.15.0

func (p *Partial) Defines(file string, name string) (bool, error)

Defines reports whether the template file defines a template called name with {{ define }}, such as the "html" part of an e-mail that a partial renders with SetEntry, without parsing it for a render: the scan of the file is the one renders use, kept with the template cache (see UseTemplateCache) and made again without. The file is read from the partial's file system; a missing file or one that does not parse is an error.

if ok, _ := root.Defines("mail/welcome.gohtml", "html"); ok {
	html, err = partial.RenderSelf(ctx, nil, mail.SetEntry("html"))
}

func (*Partial) Extension added in v0.9.0

func (p *Partial) Extension(key any) (any, bool)

Extension returns extension-owned data from this partial or its parents.

func (*Partial) FirstExisting added in v0.15.0

func (p *Partial) FirstExisting(candidates ...string) (string, error)

FirstExisting returns the first of candidates that is a file in the partial's file system, such as the template of a page's own type before the default one:

file, err := root.FirstExisting("pages/"+key+".gohtml", "pages/"+kind+".gohtml", "pages/page.gohtml")

With the template cache (see UseTemplateCache) the answer for each candidate is remembered per file system, also when it is missing; without, the file system is asked every time. It returns an error that wraps fs.ErrNotExist when no candidate exists, or the first error other than a missing file.

func (*Partial) FirstExistingIn added in v0.15.0

func (p *Partial) FirstExistingIn(layers []fs.FS, candidates ...string) (string, error)

FirstExistingIn returns the first of candidates found in layers, trying every candidate in a layer before the next layer: over a theme and a package's defaults, the theme's general template wins over the default specific one, where FirstExisting on Overlay(theme, defaults) would pick the latter.

file, err := root.FirstExistingIn(nil, "pages/"+key+".gohtml", "pages/"+kind+".gohtml")

Without layers, those of the partial's file system are used when it is an Overlay, and the file system itself otherwise; nil layers are skipped. The answers are remembered as FirstExisting remembers them. The name returned is that of the file in the layer, which the overlay serves.

func (*Partial) Func added in v0.15.0

func (p *Partial) Func(name string) (any, bool)

Func returns the function registered as name with SetFunc on the partial or, failing that, on its nearest ancestor, as SetFunc was given it.

func (*Partial) GetBasePath added in v0.9.0

func (p *Partial) GetBasePath() string

GetBasePath returns the configured base path, falling back to parents.

func (*Partial) Glob added in v0.15.0

func (p *Partial) Glob(pattern string) ([]string, error)

Glob returns the template files pattern matches in the partial's file system, as SetIncludes resolves its patterns: with the template cache (see UseTemplateCache) the matches are remembered per file system; without, the file system is asked every time, so new files show. Directories are left out. The slice may be shared: do not change it.

files, err := root.Glob("regions/*.gohtml")

func (*Partial) HasEntry added in v0.15.0

func (p *Partial) HasEntry() (bool, error)

HasEntry reports whether the partial's first template defines its entry (see SetEntry); without an entry, whether it has a template.

func (*Partial) ID

func (p *Partial) ID(id string) *Partial

ID sets the stable ID used for target lookup, child registration, and diagnostics.

func (*Partial) IsOOB added in v0.9.0

func (p *Partial) IsOOB() bool

IsOOB reports whether the partial is currently being rendered out-of-band.

func (*Partial) Parent added in v0.15.0

func (p *Partial) Parent() *Partial

Parent returns the partial p renders under: the partial it was added to with With, SetContent or WithOOB, or the one set with SetParent; nil for a root.

func (*Partial) ParentID added in v0.9.0

func (p *Partial) ParentID() string

ParentID returns the ID of the parent partial, if one is attached.

func (*Partial) PartialID added in v0.9.0

func (p *Partial) PartialID() string

PartialID returns the stable render ID for this partial.

func (*Partial) Response added in v0.9.0

func (p *Partial) Response() *connector.ResponseBuilder

Response returns a builder for connector-specific response instructions.

func (*Partial) SetAlwaysSwapOOB added in v0.9.0

func (p *Partial) SetAlwaysSwapOOB(alwaysSwapOOB bool) *Partial

SetAlwaysSwapOOB makes this out-of-band partial render on every partial request. A region registered with WithOOB goes out of band with partial requests for the partials below the one it is registered on; one that always swaps also goes with requests for that partial itself, such as a notice a page registers for a toast container of the layout. It then goes after the partial's output, unless the output places it already, such as a layout does with {{ slot }} of exp/slots.

func (*Partial) SetBasePath added in v0.9.0

func (p *Partial) SetBasePath(basePath string) *Partial

SetBasePath sets the base path for the partial.

func (*Partial) SetChildIncludes added in v0.15.0

func (p *Partial) SetChildIncludes(patterns ...string) *Partial

SetChildIncludes is SetIncludes for the partials rendered under the partial only, not for the partial itself: a root can give the content, regions and fragments of every page their shared templates, such as a theme's "partials/*.gohtml", without adding them to the parse of its layout.

root := partial.New("layout.gohtml").
	SetFileSystem(theme).
	SetChildIncludes("partials/*.gohtml")

The patterns of descendants come after those of SetIncludes on the same partial. SetChildIncludes replaces the partial's own child patterns; no patterns removes them.

func (*Partial) SetConnector added in v0.8.0

func (p *Partial) SetConnector(c connector.Connector) *Partial

SetConnector configures the request connector used by this partial.

func (*Partial) SetContent added in v0.9.0

func (p *Partial) SetContent(child *Partial) *Partial

SetContent registers the primary content child rendered by the content helper.

func (*Partial) SetContract added in v0.9.0

func (p *Partial) SetContract(annotation string, values ...any) *Partial

SetContract registers typed values for go-doc root declarations. Values are matched by type unless they implement NamedContract. A ModelFunc (see ModelFuncOf) is computed only when a template calls it, and matched by the type it computes.

func (*Partial) SetDot added in v0.9.0

func (p *Partial) SetDot(value any) *Partial

SetDot sets the root value passed to html/template Execute. Templates receive this value as "." and can still use core render helpers such as ctx, request, url, basePath, runtime, partial, and content. Extension helpers such as debug, locale, and csrf remain available when their FuncMaps and stages are used.

func (*Partial) SetDotSource added in v0.15.0

func (p *Partial) SetDotSource(d Dot) *Partial

SetDotSource sets the dot of the partial to d, computed when the partial renders. The zero Dot removes the dot, as ClearDot does. It replaces SetDot, and SetDot replaces it.

func (*Partial) SetEntry added in v0.15.0

func (p *Partial) SetEntry(name string) *Partial

SetEntry makes the partial render the template defined as name, such as {{ define "html" }} in one of its files, instead of its first file. Use it for a file that defines several parts, such as an e-mail with its subject, text and HTML. The @dot declaration of the first file still types the dot. An empty name renders the first file again.

func (*Partial) SetEvents added in v0.9.0

func (p *Partial) SetEvents(events EventSink) *Partial

SetEvents configures the diagnostic event sink inherited by this partial tree.

func (*Partial) SetExtension added in v0.9.0

func (p *Partial) SetExtension(key any, value any) *Partial

SetExtension stores extension-owned data on this partial.

func (*Partial) SetFailurePolicy added in v0.15.0

func (p *Partial) SetFailurePolicy(policy FailurePolicy) *Partial

SetFailurePolicy sets what a failing part of a render does to the whole: FailInline (the default) or FailResponse. Children inherit the policy; the policy of the partial passed to Render, RenderWithRequest, RenderSelf or Write decides for its whole render.

func (*Partial) SetFileSystem added in v0.2.0

func (p *Partial) SetFileSystem(fs fs.FS) *Partial

SetFileSystem sets the file system for the partial.

func (*Partial) SetFunc added in v0.9.0

func (p *Partial) SetFunc(funcMaps ...template.FuncMap) *Partial

SetFunc registers template functions in the Partial scope. Functions named like a go-partial helper are ignored, and those named like a function a render stage of the partial or its ancestors gives templates are never called (see ReservedFuncNames): for each, the partial's event sink receives an EventFuncProtected warning.

func (*Partial) SetIncludes added in v0.15.0

func (p *Partial) SetIncludes(patterns ...string) *Partial

SetIncludes adds the templates that match patterns, such as "partials/*.gohtml", to every render of the partial and of the partials rendered under it: its children and content, the partials {{ partial }} renders and those that name it with SetParent. Their templates can call the templates these files define, as if each partial listed the files itself. Patterns are those of fs.Glob, resolved against the file system of the partial being rendered; a partial's patterns come after those of its ancestors, and a file is parsed once, also when several patterns or the partial's own templates name it. With the template cache (see UseTemplateCache) a pattern is resolved once per file system; without, at every render, so new files show.

root := partial.New("layout.gohtml").
	SetFileSystem(theme).
	SetIncludes("partials/*.gohtml")

SetIncludes replaces the partial's own patterns; no patterns removes them.

func (*Partial) SetModel added in v0.9.0

func (p *Partial) SetModel(values ...any) *Partial

SetModel registers values for typed model declarations. Nil values are ignored, so optional models can be passed as they are. A ModelFunc (see ModelFuncOf) is computed only when a template calls the model.

func (*Partial) SetOOBElements added in v0.15.0

func (p *Partial) SetOOBElements(ids ...string) *Partial

SetOOBElements makes the partial, when it goes out of band with a partial response (see WithOOB and SetAlwaysSwapOOB), send only the elements of its output with these IDs, each swapped out of band on its own with the connector's attributes (see connector.OOBAttrs), rather than its whole output. It suits a region of which a request changes a few elements the application can name, such as the items of a menu that mark the page being shown:

menu.SetOOBElements("menu-about", "menu-contact")

An element inside another one it sends goes along with that one, and an ID that is not the ID of an element in the output is left out. When none of them is, or when the end of an element cannot be told, such as an <li> without its </li>, the whole output goes, as it does without IDs: a region that does not give its elements those IDs keeps working. The region's own element, such as the one that carries {{oobAttr}}, is not sent unless its ID is one of them. Rendered on a full page, the partial is not affected. It replaces SetOOBElementsFunc, and SetOOBElementsFunc replaces it.

func (*Partial) SetOOBElementsFunc added in v0.15.0

func (p *Partial) SetOOBElementsFunc(fn func(r *http.Request) OOBElements) *Partial

SetOOBElementsFunc is SetOOBElements with the IDs chosen per request by fn, such as the menu items whose state changes from the page the visitor comes from to the page requested. fn is called only when the partial goes out of band, once per response, and not for a full page or a response that leaves the partial out. The headers it names in Vary go to the Vary header of the response then, and only then:

menu.SetOOBElementsFunc(func(r *http.Request) partial.OOBElements {
	return partial.OOBElements{
		IDs:  changedItems(connector.CurrentURL(conn, r), r.URL),
		Vary: []string{connector.CurrentURLHeader(conn)},
	}
})

A nil fn sends the whole output again. It replaces SetOOBElements, and SetOOBElements replaces it.

func (*Partial) SetParent added in v0.2.0

func (p *Partial) SetParent(parent *Partial) *Partial

SetParent renders the partial under parent without making it one of parent's children: the partial inherits what parent configures, such as its functions, render stages, connector, file system, template cache, failure policy and values for @model declarations, and parent is not changed, so one configured partial can be the parent of many partials rendered at the same time. Partial requests cannot target the partial and parent's templates do not include it; use With for that. A nil parent detaches the partial; a parent that would make the partial its own ancestor is ignored.

card := partial.NewID("card", "card.gohtml").SetDot(data).SetParent(root)
html, err := partial.RenderSelf(ctx, r, card)

func (*Partial) SetResponse added in v0.9.0

func (p *Partial) SetResponse(response connector.Response) *Partial

SetResponse configures connector-specific response instructions.

func (*Partial) SetResponseHeaders added in v0.8.0

func (p *Partial) SetResponseHeaders(headers map[string]string) *Partial

SetResponseHeaders configures plain HTTP headers written by Write.

func (*Partial) SetStatus added in v0.10.0

func (p *Partial) SetStatus(status int) *Partial

SetStatus configures the HTTP status written by Write. A zero status clears the local value and falls back to the parent partial, then to net/http's default status.

func (*Partial) SetTargetFallback added in v0.15.0

func (p *Partial) SetTargetFallback(fallback TargetFallback) *Partial

SetTargetFallback answers partial requests for unknown targets with the fallback partial rather than an error; see TargetFallback. Children inherit the fallback. The response instructions are serialized by the connector, such as HX-Retarget, HX-Reswap and HX-Push-Url for htmx, and written by Write; RenderWithRequest returns the fallback's HTML.

root.SetContent(content).
    SetTargetFallback(partial.TargetFallback{ID: "content", PushURL: true})

func (*Partial) SetTemplates added in v0.10.0

func (p *Partial) SetTemplates(templates ...string) *Partial

SetTemplates replaces the template paths while preserving the partial's configured filesystem, functions, stages, connector, and cache. It is useful when cloning a configured partial as a request-scoped blueprint.

func (*Partial) TemplatePaths added in v0.9.0

func (p *Partial) TemplatePaths() []string

TemplatePaths returns the template paths configured for this partial.

func (*Partial) Use added in v0.9.0

func (p *Partial) Use(stages ...RenderStage) *Partial

Use appends stages to this partial's render chain.

The functions a stage gives templates (see TemplateFuncNamer) need no stand-ins registered with SetFunc: templates are parsed with stand-ins for them, which the stage replaces when they execute. A function registered on the partial or its ancestors under such a name is never called: the partial's event sink receives an EventFuncProtected warning.

func (*Partial) UseTemplateCache added in v0.9.0

func (p *Partial) UseTemplateCache(useCache bool) *Partial

UseTemplateCache turns the template cache on or off for the partial and the partials rendered under it: its children, its content, the partials {{ partial }} renders and those that name it with SetParent, unless they set it themselves. A partial that sets nothing uses the setting of its nearest ancestor that does; without one, the cache is off. With the cache, each template is read and parsed once; without, on every render, so edits to template files show. Set it on the root once, for example root.UseTemplateCache(!development).

What the cache learns of a file system, its parses, its scans, which files exist (see FirstExisting) and what include patterns match (see Glob), is kept by the root of the partial tree: its clones, the partials rendered under it and those that name it with SetParent share it. Another root, also one over the same directory or the same map file system, such as a second application in one test process, learns it again: a file added after the first root rendered is seen by a root made later, but not by the first one while its cache is on. Give the partials built per request a configured parent with SetParent, rather than a root of their own, so they share its cache.

func (*Partial) With

func (p *Partial) With(child *Partial) *Partial

With registers a child partial on the partial tree.

Registered children are addressable by ID for partial requests. During a full render, go-partial also includes child templates that are referenced by native Go template calls, such as {{ template "row.gohtml" . }}.

func (*Partial) WithOOB

func (p *Partial) WithOOB(child *Partial) *Partial

WithOOB registers an out-of-band child partial on the partial tree. A partial response sends the out-of-band children of a partial in the order WithOOB registered them, so it gives the same bytes on every render; registering an ID again keeps its place.

func (*Partial) WithTemplate added in v0.9.0

func (p *Partial) WithTemplate(templatePath string) *Partial

WithTemplate creates a child partial from a template path and registers it on the partial tree. The child ID is inferred from the file name without its extension: "templates/sidebar.gohtml" becomes "sidebar".

type RenderContext added in v0.9.0

type RenderContext struct {
	Context  context.Context
	Request  *http.Request
	URL      *url.URL
	BasePath string
	Partial  *Partial
	Runtime  *Runtime
	Kind     RenderKind
	Name     string
	Data     any
	Values   RenderValues
	Error    error
	Response *RenderResponse
	Funcs    template.FuncMap
	Events   EventSink
	// contains filtered or unexported fields
}

RenderContext contains request-scoped values exposed by the ctx template helper.

func (*RenderContext) Emit added in v0.9.0

func (ctx *RenderContext) Emit(event Event)

Emit sends a diagnostic event through the active render context.

func (*RenderContext) EmitForPartial added in v0.9.0

func (ctx *RenderContext) EmitForPartial(p *Partial, event Event)

EmitForPartial sends an event for a specific partial while preserving the active render context, including request-scoped event sinks.

func (*RenderContext) FailureResponse added in v0.15.0

func (ctx *RenderContext) FailureResponse() bool

FailureResponse reports, in an error render (see ext/errors), whether the failure response answers the request as a whole, as Write writes it, rather than taking the place of a failing part of a page.

func (*RenderContext) FailureStatus added in v0.15.0

func (ctx *RenderContext) FailureStatus() int

FailureStatus is, in the error render of WriteError, which answers an error that happened outside a render, the status the response is written with, such as 500 or 429; it is 0 for a render that failed.

func (*RenderContext) OmitOOB added in v0.15.0

func (ctx *RenderContext) OmitOOB()

OmitOOB answers the partial request being rendered with the target alone, without the out-of-band regions of its ancestors. Render stages that resolve the target of a request, such as exp/slots for a standalone slot, call it in Prepare for targets that are requested on their own.

func (*RenderContext) ResolveFunc added in v0.15.0

func (ctx *RenderContext) ResolveFunc(name string) (any, bool)

ResolveFunc returns the function name as the render gives it to templates through functions set with SetFunc and SetFuncResolver and through its render stages (see TemplateFuncResolver), such as the function of another stage, from a Render hook: the stages of the render are known once every stage has prepared it. Functions registered on the partial with Partial.SetFunc are not looked up.

func (*RenderContext) SetFunc added in v0.9.0

func (ctx *RenderContext) SetFunc(name string, fn any)

func (*RenderContext) SetFuncResolver added in v0.14.0

func (ctx *RenderContext) SetFuncResolver(resolve func(name string) (fn any, ok bool))

SetFuncResolver gives templates functions that are looked up by name, only for the functions a template calls. It suits stages that offer many functions, of which a template calls a few. Functions set with SetFunc come first; a later resolver is asked before an earlier one. As with SetFunc, the names must be known when templates are parsed: register stand-ins for them with Partial.SetFunc.

func (*RenderContext) TargetResolved added in v0.15.0

func (ctx *RenderContext) TargetResolved() bool

TargetResolved reports whether a render stage resolved the target of the partial request being rendered, replacing Partial in Prepare: a later stage that resolves targets leaves it as it is.

type RenderKind added in v0.9.0

type RenderKind string

RenderKind identifies the current render task for render stages.

const (
	// RenderKindPartial is the normal server-side partial render path.
	RenderKindPartial RenderKind = "partial"
	// RenderKindTarget is the core target render path selected by a connector.
	RenderKindTarget RenderKind = "target"
)
const RenderKindText RenderKind = "text"

RenderKindText is the render kind of RenderText.

type RenderNext added in v0.9.0

type RenderNext func(*RenderContext) (template.HTML, error)

RenderNext calls the next render stage in the chain.

type RenderResponse added in v0.9.0

type RenderResponse struct {
	Headers map[string]string
	Status  int
}

RenderResponse stores generic response metadata set by render stages.

type RenderStage added in v0.9.0

type RenderStage interface {
	Prepare(*RenderContext) (*RenderContext, error)
	Render(*RenderContext, RenderNext) (template.HTML, error)
	Finalize(*RenderContext, template.HTML, error) (template.HTML, error)
}

RenderStage observes or changes a render lifecycle.

Prepare runs before the terminal template render and is the right place to add request-scoped template funcs, resolve context values, or start timing. Render wraps the terminal render and can replace or decorate the produced HTML. Finalize runs after Render, even when Render returned an error, and is the right place for cleanup, metrics, or error response shaping.

type RenderStageHooks added in v0.9.0

type RenderStageHooks struct {
	PrepareFunc  func(*RenderContext) (*RenderContext, error)
	RenderFunc   func(*RenderContext, RenderNext) (template.HTML, error)
	FinalizeFunc func(*RenderContext, template.HTML, error) (template.HTML, error)
	// ResolveFunc gives templates the stage's functions by name, only
	// those a template calls; see TemplateFuncResolver.
	ResolveFunc func(ctx *RenderContext, name string) (fn any, ok bool)
	// FuncNames lists the template functions the stage sets with
	// RenderContext.SetFunc or gives with ResolveFunc, for
	// ReservedFuncNames; templates are parsed with stand-ins for them.
	FuncNames []string
	// FallbackFuncNames lists template functions the stage gives with
	// ResolveFunc only where the application registered no function
	// of that name with SetFunc: templates are parsed with stand-ins
	// for them, a registered function is called instead of the
	// stage's, and the names are not reserved, so registering one
	// raises no warning. Functions of the stage's own package, such as
	// a stand-in it offers for production, do not count as registered;
	// see Package. See TemplateFuncFallback.
	FallbackFuncNames []string
	// Package is the import path of the package that made the stage,
	// such as github.com/acme/app/tenant. Functions of that package,
	// such as the stand-ins its FuncMap offers for the names the stage
	// gives templates, are the stage's own: registering one raises no
	// func.protected warning and does not count as registered for
	// FallbackFuncNames. When Package is empty, the package is that of
	// the hook functions, which is the caller's where the compiler
	// inlines the function that returns the stage, so a package that
	// offers stand-ins sets it, for example to
	// reflect.TypeFor[T]().PkgPath() for a type T of its own.
	Package string
}

RenderStageHooks adapts individual lifecycle functions to RenderStage.

func (RenderStageHooks) FallbackTemplateFuncNames added in v0.15.0

func (h RenderStageHooks) FallbackTemplateFuncNames() []string

FallbackTemplateFuncNames returns FallbackFuncNames; see TemplateFuncFallback.

func (RenderStageHooks) Finalize added in v0.9.0

func (h RenderStageHooks) Finalize(ctx *RenderContext, out template.HTML, renderErr error) (template.HTML, error)

func (RenderStageHooks) Prepare added in v0.9.0

func (h RenderStageHooks) Prepare(ctx *RenderContext) (*RenderContext, error)

func (RenderStageHooks) Render added in v0.9.0

func (h RenderStageHooks) Render(ctx *RenderContext, next RenderNext) (template.HTML, error)

func (RenderStageHooks) ResolveTemplateFunc added in v0.15.0

func (h RenderStageHooks) ResolveTemplateFunc(ctx *RenderContext, name string) (any, bool)

ResolveTemplateFunc returns the function ResolveFunc gives templates as name; see TemplateFuncResolver.

func (RenderStageHooks) TemplateFuncNames added in v0.15.0

func (h RenderStageHooks) TemplateFuncNames() []string

TemplateFuncNames returns the template functions the stage sets; see FuncNames and FallbackFuncNames.

type RenderValues added in v0.9.0

type RenderValues map[any]any

RenderValues stores request-scoped values shared by render stages and helpers.

func (RenderValues) Clone added in v0.9.0

func (v RenderValues) Clone() RenderValues

func (RenderValues) Get added in v0.9.0

func (v RenderValues) Get(key any) any

func (RenderValues) Set added in v0.9.0

func (v RenderValues) Set(key any, value any)

type Runtime added in v0.9.0

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

Runtime is the per-render handle passed explicitly to helpers that need request-aware behavior. It keeps application data typed through SetDot while still giving advanced helpers access to the active request, connector, and partial render path.

func RuntimeFrom added in v0.15.0

func RuntimeFrom(ctx context.Context) (*Runtime, bool)

RuntimeFrom returns the runtime of the partial whose template is executing in the render of ctx, for functions registered with SetFunc whose first parameter is a context.Context. Such a function can render a partial as a true child of the partial that calls it, with its models, functions, stages and failure policy:

func card(ctx context.Context, data Card) (template.HTML, error) {
	runtime, ok := partial.RuntimeFrom(ctx)
	if !ok {
		return "", errors.New("card: not rendering")
	}
	return runtime.RenderPartial(partial.NewID("card", "card.gohtml").SetDot(data))
}

It reports false outside a template go-partial executes. The partials of one render execute their templates one after another; a function that renders with ctx on other goroutines passes them the runtime instead.

func (*Runtime) BasePath added in v0.9.0

func (r *Runtime) BasePath() string

BasePath returns the active render base path.

func (*Runtime) CheckContracts added in v0.15.0

func (r *Runtime) CheckContracts(p *Partial) error

CheckContracts reports whether p could render here, as a child of the active partial: with the models and contract values of the tree it would render in. See Partial.CheckContracts.

func (*Runtime) Connector added in v0.9.0

func (r *Runtime) Connector() connector.Connector

Connector returns the connector for the active partial.

func (*Runtime) Context added in v0.9.0

func (r *Runtime) Context() context.Context

Context returns the context for the active render.

func (*Runtime) Partial added in v0.9.0

func (r *Runtime) Partial(path string, args ...any) template.HTML

Partial renders a template path through the current partial tree.

func (*Runtime) RenderContext added in v0.9.0

func (r *Runtime) RenderContext() *RenderContext

RenderContext returns the active render context.

func (*Runtime) RenderPartial added in v0.9.0

func (r *Runtime) RenderPartial(p *Partial) (template.HTML, error)

RenderPartial renders a child partial through the current render stage chain.

func (*Runtime) RenderPartialWithFallback added in v0.9.0

func (r *Runtime) RenderPartialWithFallback(p *Partial) (template.HTML, error)

RenderPartialWithFallback renders a partial through the current render stage chain and returns the configured error fragment when rendering fails.

func (*Runtime) RenderWith added in v0.9.0

func (r *Runtime) RenderWith(kind RenderKind, name string, data any, terminal RenderNext) (template.HTML, error)

RenderWith renders a non-template task through the active render stage chain.

func (*Runtime) Request added in v0.9.0

func (r *Runtime) Request() *http.Request

Request returns the request for the active render, if any.

func (*Runtime) SetValue added in v0.9.0

func (r *Runtime) SetValue(key any, value any)

SetValue stores a value in the active render context.

func (*Runtime) URL added in v0.9.0

func (r *Runtime) URL() *url.URL

URL returns the request URL for the active render, if any.

func (*Runtime) Value added in v0.9.0

func (r *Runtime) Value(key any) any

Value returns a value from the active render context.

type Sources added in v0.15.0

type Sources map[string]string

Sources is a file system of template sources held in memory, by path, such as the default markup a package ships for a template that applications may replace:

defaults := partial.Sources{
    "share.gohtml": `<nav class="share">{{ range .Links }}…{{ end }}</nav>`,
}
p := partial.NewID("share", "share.gohtml").
    SetFileSystem(partial.Overlay(themeFiles, defaults))

Paths use forward slashes without a leading one, as io/fs paths do; directories are implied by the paths of their files.

The template cache tells file systems apart by identity: create Sources once, such as when the application starts, rather than for every render. A Sources must not change while it is used.

func (Sources) Open added in v0.15.0

func (s Sources) Open(name string) (fs.File, error)

Open opens the source or the directory name.

func (Sources) ReadDir added in v0.15.0

func (s Sources) ReadDir(name string) ([]fs.DirEntry, error)

ReadDir lists the directory name.

func (Sources) ReadFile added in v0.15.0

func (s Sources) ReadFile(name string) ([]byte, error)

ReadFile returns the source name.

type TargetFallback added in v0.15.0

type TargetFallback struct {
	// ID is the partial rendered instead: the partial that sets the
	// fallback, or a child registered on its tree, such as the content
	// child.
	ID string
	// Retarget is the selector the client swaps the response into; "#" + ID
	// when empty.
	Retarget string
	// Swap is how the client swaps it; outerHTML when nil.
	Swap *connector.Swap
	// PushURL pushes the URL of a GET request to the browser history, so
	// the address follows the navigation.
	PushURL bool
}

TargetFallback answers partial requests for a target the partial tree cannot resolve, such as a link or form in another page's layout that targets an element this page does not have. Instead of failing, the request is answered as one for the fallback partial, usually the page's content, and the response tells the client to swap it where that partial lives, as a navigation. See Partial.SetTargetFallback.

type TemplateFuncFallback added in v0.15.0

type TemplateFuncFallback interface {
	FallbackTemplateFuncNames() []string
}

TemplateFuncFallback is implemented by render stages that give templates some functions only as a fallback for functions the application did not register; see RenderStageHooks.FallbackFuncNames.

type TemplateFuncNamer added in v0.15.0

type TemplateFuncNamer interface {
	TemplateFuncNames() []string
}

TemplateFuncNamer is implemented by render stages that give templates functions, with RenderContext.SetFunc or as a TemplateFuncResolver, so ReservedFuncNames can list them. RenderStageHooks implements it with its FuncNames.

type TemplateFuncResolver added in v0.15.0

type TemplateFuncResolver interface {
	ResolveTemplateFunc(ctx *RenderContext, name string) (fn any, ok bool)
}

TemplateFuncResolver is implemented by render stages that give templates functions bound to the render, such as one that reads the request. go-partial asks the stages of a render for the functions a template calls, by name, when the template is executed: a render whose templates call none of them costs nothing, unlike functions set with RenderContext.SetFunc in Prepare, which are made for every partial. Functions set with SetFunc or SetFuncResolver come first; a later stage is asked before an earlier one. The names must be known when templates are parsed: templates get stand-ins for those the stage declares with TemplateFuncNamer, as RenderStageHooks does with its FuncNames; register stand-ins for others with Partial.SetFunc. RenderStageHooks implements it with its ResolveFunc.

type TemplateProblem added in v0.15.0

type TemplateProblem struct {
	// File is the template's path in the partial's file system, such as
	// "regions/menu.gohtml".
	File string
	// Layer is, for a file system made with Overlay, the index of the layer
	// the file comes from, the first layer 0, as the overlay serves it;
	// 0 for any other file system.
	Layer int
	// Line and Column are where the problem is in the file, counted from 1;
	// Column is 0 when the parser does not say.
	Line   int
	Column int
	// Func is the function the template calls that the partial does not
	// have, such as "newsletterForm"; empty for other problems.
	Func string
	// Template is the template a {{template}} names that no file defines;
	// empty for other problems.
	Template string
	// Suggestion is the defined function or template whose name is closest
	// to Func or Template, such as "newsletterForm" for "newsleterForm";
	// empty when none is close.
	Suggestion string
	// Message says what is wrong, without the position, such as
	// `function "newsletterForm" not defined`.
	Message string
}

TemplateProblem is a problem CheckTemplates finds in a template file: one a render of the file would fail with.

func (TemplateProblem) Error added in v0.15.0

func (p TemplateProblem) Error() string

Error is the position and the message, with the suggestion when there is one: `layout.gohtml:69:5: function "newsleterForm" not defined; did you mean "newsletterForm"?`.

func (TemplateProblem) Position added in v0.15.0

func (p TemplateProblem) Position() string

Position is the problem's position: the file, its line and, when known, its column, such as "layout.gohtml:69:5".

Directories

Path Synopsis
Package clients holds go-partial's browser clients in one file system, for applications that serve, fingerprint and version them with their other assets, such as through an import map:
Package clients holds go-partial's browser clients in one file system, for applications that serve, fingerprint and version them with their other assets, such as through an import map:
Package connector tells go-partial how a client library makes its requests and reads its responses: htmx 2 (NewHTMX), htmx 4 (NewHTMX4), Unpoly (NewUnpoly), Turbo (NewTurbo), or plain fetch and tests (NewPartial).
Package connector tells go-partial how a client library makes its requests and reads its responses: htmx 2 (NewHTMX), htmx 4 (NewHTMX4), Unpoly (NewUnpoly), Turbo (NewTurbo), or plain fetch and tests (NewPartial).
examples
docs command
showcase command
exp
actions
Package actions provides experimental render-time action hooks for partials.
Package actions provides experimental render-time action hooks for partials.
csrf
Package csrf provides experimental CSRF token helpers for templates.
Package csrf provides experimental CSRF token helpers for templates.
flash
Package flash provides experimental request-scoped flash message helpers.
Package flash provides experimental request-scoped flash message helpers.
head
Package head keeps the elements a page writes into the document's <head>, such as <link rel="canonical">, <meta name="description">, alternate and feed links and structured data, right when a partial response swaps only a part of the page.
Package head keeps the elements a page writes into the document's <head>, such as <link rel="canonical">, <meta name="description">, alternate and feed links and structured data, right when a partial response swaps only a part of the page.
interactions
Package interactions contains optional client-interaction template helpers.
Package interactions contains optional client-interaction template helpers.
localization
Package localization provides experimental locale helpers for templates.
Package localization provides experimental locale helpers for templates.
metrics
Package metrics provides an experimental renderer for passive render metrics.
Package metrics provides an experimental renderer for passive render metrics.
otel
Package otel adds go-partial's diagnostic events to the active OpenTelemetry span of the render's context, through the event sink of Sink.
Package otel adds go-partial's diagnostic events to the active OpenTelemetry span of the render's context, through the event sink of Sink.
pageflow
Package pageflow contains experimental helpers for multi-step page flows.
Package pageflow contains experimental helpers for multi-step page flows.
selection
Package selection provides experimental selected-partial rendering helpers.
Package selection provides experimental selected-partial rendering helpers.
slots
Package slots provides experimental named regions backed by child partials.
Package slots provides experimental named regions backed by child partials.
sse
Package sse provides experimental server-sent event helpers for streaming rendered partials.
Package sse provides experimental server-sent event helpers for streaming rendered partials.
target
Package target provides experimental dynamic target resolution.
Package target provides experimental dynamic target resolution.
templatehelpers
Package templatehelpers exposes optional convenience helpers for templates.
Package templatehelpers exposes optional convenience helpers for templates.
ext
debug
Package debug provides an optional renderer for debug output.
Package debug provides an optional renderer for debug output.
errors
Package errors provides an optional renderer for render failures.
Package errors provides an optional renderer for render failures.
logger
Package logger adapts go-partial diagnostic events to log/slog.
Package logger adapts go-partial diagnostic events to log/slog.
internal
asset
Package asset packages the files go-partial's packages embed, such as their browser clients and stylesheets.
Package asset packages the files go-partial's packages embed, such as their browser clients and stylesheets.
csp
Package csp computes the Content-Security-Policy hash sources of the inline elements go-partial writes.
Package csp computes the Content-Security-Policy hash sources of the inline elements go-partial writes.

Jump to

Keyboard shortcuts

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