view

package
v0.27.0 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package view is the view layer: kyse for markup, HTMX for interaction, Alpine for ephemeral client state, Tailwind for style. It is a binary and it is never Node.

A project that uses it still runs with `git clone && aru dev`: no node_modules, no package.json, no lockfile of JavaScript, nothing installed beyond Go and the standalone binaries the CLI fetches. Having a build step is allowed; being Node is not.

It lives in the framework and the views do not, and that split is deliberate: resources/views/ belongs to the project, because it is edited; the rendering machinery belongs here, because it is not.

The error page deliberately does not use this package. It has to render when the rest is broken, including when the view build failed, so it stays as html/template inline in observability/errorpage.

This package is a bridge. It is removed in v1.0.0; import github.com/arandu-io/hesape/view directly.

The components moved to github.com/arandu-io/hesape, under new names, and this package is now the old names pointing at them. Everything the view runtime is made of answers to one hesape package:

hesape/view  the compiled-view registry, the runtime generated code calls,
             the served assets, and the development reload script

The death date above is what keeps this from being a second way to import one type. Bridging the registries is not only a matter of tidiness: Register, RegisterLayout and RegisterAsset write into package-level tables, and a framework that kept its own would have generated views landing in one table while the Renderer the kernel installed read the other.

What is NOT bridged, and why

Two files here still hold an implementation, because the hesape design diverged in a way no envelope can absorb without breaking a caller:

Page, Layout, New  hesape/view renamed the four error accessors --
                   FieldError became First, FieldErrors became Get,
                   HasErrors became Any and ErrorSummary became All, and
                   the Layout interface followed. github.com/arandu-io/kyse
                   declares a two-method interface asking for FieldError,
                   in a separate module, so an alias compiles here and
                   breaks the component library in silence. An envelope cannot
                   stand in either: Page is written as a composite literal
                   across the skeleton and the published screens, and
                   promoted fields are not addressable in one. hesape's New
                   also takes a *hesape/http.Context and its Errors field
                   is a hesape/validation.Errors, neither of which this
                   module's http and validation reach yet.
Module             hesape/view.Module takes a *hesape/routing.Router and
                   answers a hesape/http.Renderer, and it deliberately
                   drops the compile-time assertion against the module
                   contract while foundation is still being built. This one
                   keeps kernel.Module and *http.Router, and hands over
                   the bridged Renderer -- which satisfies http.Renderer
                   because the two interfaces declare the same method.

Both are reported as gaps rather than reimplemented anywhere.

Index

Constants

View Source
const AssetPath = hview.AssetPath

AssetPath is where assets are served from. The hash is in the path, so the response can be cached forever and a new build simply has a new URL.

View Source
const Stylesheet = hview.Stylesheet

Stylesheet is the name of the one stylesheet, and there is only one.

The collection embeds a default under this name and RegisterStylesheet replaces it. Not a second file, not a second URL, not a cascade order: one name, one URL, one set of bytes.

Variables

This section is empty.

Functions

func AssetHash added in v0.20.0

func AssetHash(body []byte) string

AssetHash is the path segment an asset is served under: the first twelve hex characters of the SHA-256 of its bytes.

It is exported because one thing outside this repository has to produce the same string. `aru font:add` writes an absolute src: into the stylesheet it generates, and it has to name the font's own hash -- a URL carrying any other hash is served without caching, by design.

func CSRF

func CSRF(w io.Writer, data any) error

CSRF writes the hidden input a form needs.

The token comes from the data, through an interface the page data satisfies. It is not read from a global: a template that reaches for request state outside the data it was given is how a form ends up with another session's token under load.

func Handler

func Handler(w http.ResponseWriter, r *http.Request)

Handler serves the embedded assets.

Anything whose path carries the right hash is immutable and cached for a year; a wrong hash is served without caching, so a stale reference degrades into a slow page rather than a broken one.

func Include

func Include(w io.Writer, name string, data any) error

Include renders a partial with the same data as the page.

A partial shares the page's data. That data is one typed struct, so the partial receives exactly it -- and a partial that wants something else is a partial that takes different data, which is what a component is for.

func Register

func Register(name string, f Func)

Register records a compiled view under the name a controller renders it by.

view.Register("invoices/index", renderInvoicesIndex)

Generated code calls it from init(), so importing the views package is what makes them reachable -- the same shape as a database/sql driver.

Registering twice panics rather than replacing. Two views for one name is a build artifact that outlived its source, and finding out at boot beats finding out from a page that renders the wrong thing.

func RegisterAsset added in v0.21.0

func RegisterAsset(name, contentType string, body []byte)

RegisterAsset adds one file to the served assets.

It is the transport primitive and it knows nothing about what it carries: a name, a content type and the bytes. Unlike RegisterStylesheet it ADDS rather than replaces, and registering one name twice panics.

func RegisterLayout

func RegisterLayout(name string, f LayoutFunc)

RegisterLayout records a compiled layout. Generated code calls it from init() when the view contains a @yield.

func RegisterStylesheet added in v0.12.0

func RegisterStylesheet(css []byte)

RegisterStylesheet replaces the embedded stylesheet with the application's.

`aru view:build` compiles resources/css/app.css into assets/app.css, and the skeleton hands those bytes over from init(), the same shape as Register:

//go:embed assets/app.css
var appCSS []byte

func init() { view.RegisterStylesheet(appCSS) }

It replaces rather than adds, and a second registration panics.

func Registered

func Registered() []string

Registered returns the known view names, sorted. `aru doctor` reads it to check that every ctx.View("x") has a view named x.

func ReloadTag added in v0.24.0

func ReloadTag(stream string) string

ReloadTag registers the reload script and returns the tag that runs it.

stream is where the script listens, and it comes from the caller because the route belongs to the kernel: two constants for one address is how a client and a server come to disagree about it.

Registering happens once. A second call returns the same tag rather than panicking on a duplicate asset, so a test that boots two kernels is not a crash.

Why it is a file rather than an inline <script>: the CSP is script-src 'self', and an inline tag is refused by it -- silently, which would read as the feature simply not working -- so this is registered like every other asset and referenced by its content-addressed URL.

func RenderInto

func RenderInto(w io.Writer, layout string, data any, sections map[string]func(io.Writer) error) error

RenderInto renders a layout, handing it the sections of the child view.

This is what `@extends` compiles to: the child does not write markup, it renders the layout and passes what goes in the holes.

func Text

func Text(v any) string

Text renders a value as a string, for interpolation.

It handles the types a view actually interpolates, and formats anything else with %v. It is not reflection over a struct: the field access already happened in generated Go, and this only turns the result into characters.

It panics on a method value, which is {{ .Name }} written where {{ .Name() }} was meant. That behaviour is hesape's now, and unchanged.

func URL

func URL(name string) string

URL returns the versioned path of an asset: /_arandu/assets/<hash>/htmx.min.js

The hash comes from the content, so upgrading HTMX changes the URL and no browser serves a stale script -- without anyone remembering to bump a version.

func Version

func Version() string

Version reports the served version of each asset, for `aru doctor` and for the debug page.

It reports what is served rather than what is embedded, so a stylesheet that never reached the browser shows up here as the collection's hash next to a project that thought it had built its own.

func WrongData

func WrongData(view, want string, got any) error

WrongData is what a generated view returns when the data is not the struct it declared.

Generated code calls it, so the message is the same everywhere:

d, ok := data.(HomeData)
if !ok { return view.WrongData("home", "HomeData", data) }

The alternative -- rendering the zero value -- is a blank page with a 200, which is the failure this framework exists to make impossible.

func Yield

func Yield(w io.Writer, sections map[string]func(io.Writer) error, name string) error

Yield renders the section a child view declared, or nothing.

A layout yields sections that a given child may not have, and the answer is the empty string. A missing section is a page without a sidebar, not an error.

Types

type Asset added in v0.21.0

type Asset = hview.Asset

Asset is one served file, as Assets reports it.

func Assets added in v0.21.0

func Assets() []Asset

Assets reports the name, content type and URL of everything served, sorted by name.

It exists so a caller can build markup about the assets it registered -- the <link rel=preload> for a font, for instance -- without this package having to know what a font is.

type Func

type Func = hview.Func

Func is what a compiled view is: a function that writes HTML.

`aru view:build` emits one per `.kyse.go` and registers it by name. The data arrives as `any` and the generated function asserts it back to the struct the view declared -- which is why a wrong type is an error naming both sides rather than a blank page.

type Layout added in v0.14.0

type Layout interface {
	// PageTitle is the document title, and what HTMX swaps on navigation.
	PageTitle() string
	// PageDescription is the meta description. Empty writes no tag, because a
	// missing description outranks an empty one.
	PageDescription() string
	// CanonicalURL is the absolute address of this page, or empty for none.
	CanonicalURL() string
	// IsCurrent says whether a navigation target is this page, so the header
	// can stop linking to where you already are.
	IsCurrent(href string) bool

	// BrandName is the application name in the navigation bar.
	BrandName() string
	// CSRFToken is what @csrf reads, and what <body> carries into every HTMX
	// request.
	CSRFToken() string

	// SignedIn decides which half of the navigation is drawn, and SignedInName
	// is who it greets.
	SignedIn() bool
	SignedInName() string

	// The navigation targets. An empty one draws no link, which is how a route
	// the application never registered stays out of the markup instead of
	// becoming a 404 the layout put there.
	HomeLink() string
	LoginLink() string
	LogoutLink() string
	RegisterLink() string

	// PanelLink is the signed-in person's own area, and AdminLink is the one
	// only some of them may open. Both follow the same rule as the four above:
	// empty draws nothing.
	//
	// AdminLink is empty for anybody who would be refused, which is what keeps
	// the header from ever offering an address that answers 403. The decision is
	// the controller's -- it holds the subject and the policy -- and the markup
	// only ever sees a string. A layout that asked about roles would be a second
	// place where authorization is decided, and the second place is the one that
	// gets it wrong.
	PanelLink() string
	AdminLink() string

	// HasErrors says whether the attempt that landed here was rejected, and
	// ErrorSummary is one line per failed field for the banner that says so.
	//
	// The banner is the layout's, which is why these two are here and the
	// per-field message is not: a page body draws "must be at least 12
	// characters" under the box it belongs to, and the layout draws the summary
	// once, above everything. FieldError and OldValue are promoted methods on
	// Page for the body to call, deliberately outside this interface -- Layout
	// is what the LAYOUT asks for.
	HasErrors() bool
	ErrorSummary() []string
}

Layout is what a layout asks of the data every screen hands it.

It lives here rather than in the application because every Arandu application declared the same ninety lines of it, and ninety lines nobody wrote are ninety lines nobody reads. A project that needs different chrome declares its own interface in the layout's @go block; this is the one the delivered layout uses.

It is an interface rather than a struct so that pages with unrelated data share one frame: the layout asks for behaviour, and Page below is the implementation a page embeds to get it.

type LayoutFunc

type LayoutFunc = hview.LayoutFunc

LayoutFunc is a view that receives sections, which is what a layout is.

type Module

type Module struct{}

Module serves the embedded assets and wires the renderer.

It is a kernel.Module and not a plain Mount function an application calls: a function has to be remembered, and a screen that emits three tags against a server nobody mounted gets three 404s -- no stylesheet, no HTMX, no Alpine.

A module appears in the Register call next to events, jobs and the scheduler, which is where somebody reading main.go already looks to learn what an application is made of.

func NewModule

func NewModule() *Module

NewModule returns the module.

k.Register(view.NewModule(), auth.New(...), events.NewModule())

func (*Module) Name

func (*Module) Name() string

Name is the module identifier.

func (*Module) ReloadTag added in v0.24.0

func (*Module) ReloadTag(stream string) string

ReloadTag supplies the development live-reload tag to the kernel.

kernel.ReloadTagger, an optional interface, asked for only in development. The kernel gives the address of the stream it serves; this package owns the script and the asset it is served as.

func (*Module) Renderer

func (*Module) Renderer() http.Renderer

Renderer supplies the view renderer to the kernel.

It is kernel.RendererProvider, an optional interface: the kernel asks every registered module whether it brings one, before any route is registered. That is what makes ctx.View work without the application calling a wiring function that somebody eventually forgets.

func (*Module) Routes

func (*Module) Routes(r *http.Router)

Routes registers the content-addressed asset route.

One route, one handler. The hash in the path is what makes the response cacheable forever, and what makes a deploy invalidate it without anybody clearing anything.

type Page added in v0.14.0

type Page struct {
	// Title is the document title.
	Title string
	// Description is the meta description, and the og:description. Leave it
	// empty on a page that has nothing specific to say.
	Description string
	// Canonical is the absolute URL of this page. It is what stops the same post
	// counting twice when it answers on more than one address.
	Canonical string

	// AppName is the brand in the navigation bar.
	AppName string

	// Token is the CSRF token issued for this session. It reaches the markup
	// twice: as the hidden field @csrf writes, and as the hx-headers attribute
	// on <body> that makes every HTMX request carry it.
	Token string

	// Authenticated decides which half of the navigation bar is drawn, and
	// UserName is the signed-in person's display name.
	Authenticated bool
	UserName      string

	// Where the navigation points. They come from the router, through the
	// controller. RegisterURL is empty when registration is not open.
	HomeURL     string
	LoginURL    string
	LogoutURL   string
	RegisterURL string

	// PanelURL and AdminURL are drawn only when signed in, and AdminURL only
	// for somebody the policy would let in. Filled by the controller, from the
	// subject it already holds -- see the Layout interface.
	PanelURL string
	AdminURL string

	// Path is the address this page was served at, so the navigation can say
	// where you are.
	//
	// A header that offers "Sign in" on the sign-in page is a header with a link
	// to the page you are reading -- and the one control that would help, the
	// way out to registering, is the one it does not show. The layout reads this
	// through IsCurrent; nothing else needs it.
	Path string

	// Errors is what validation rejected on the attempt that was sent back
	// here, keyed by the name of the form input -- the same name
	// components.FieldProps.Name carries.
	//
	// It is filled by New, from the flash, and by nothing else. No handler
	// assigns it: a handler that had to would be a handler that can forget to,
	// and forgetting is invisible -- the form comes back with no messages on it,
	// which is the failure this field exists to end.
	//
	// Empty on every page nobody was rejected on, which is nearly all of them.
	Errors validation.Errors

	// Old is what was typed on that attempt, so the boxes come back filled in
	// rather than blank.
	//
	// It never carries a password. See security.Flash for the list and for why
	// the MESSAGE for a password survives when the value does not: an empty
	// password box that does not say why it was rejected is the original bug.
	Old url.Values
}

Page is the chrome every screen hands the layout, embedded rather than repeated:

type PostsIndexData struct {
	view.Page
	Posts []PostRow
}

A page declares a struct of its own -- which is what turns a typo in a field name into a compile error -- and takes the frame from here.

Nothing on it is a helper a view reaches for by itself. There is no config(), no route() and no auth(): the controller fills these in, so a name that drifts is a compile error rather than a blank link, and a form can never end up carrying another session's token under load.

func New added in v0.25.4

func New(ctx *http.Context, title string) Page

New returns the page chrome for this request, with the messages and the typed input of a rejected attempt already on it.

Page: view.New(ctx, "New post").WithToken(token),

It replaces the view.Page{Title: ..., Token: ...} literal a controller used to write, and the difference is the whole point of this file: nothing in that line mentions errors, and the errors are on the page. There is no argument to pass and therefore none to forget, which matters because forgetting produces a form that comes back blank -- correct-looking, and wrong.

It fills only what the request itself knows: the title it was given, the address being served, and what the flash left behind. The application name, the navigation and the signed-in person are the controller's, because they are decisions -- see the Layout interface on why the layout is never allowed to go and fetch them.

func (p Page) AdminLink() string

AdminLink is the administration area, or empty for anybody who would be refused it.

func (Page) BrandName added in v0.14.0

func (p Page) BrandName() string

BrandName is the application name, shown in the navigation bar.

func (Page) CSRFToken added in v0.14.0

func (p Page) CSRFToken() string

CSRFToken is what @csrf reads to write the hidden field.

It is a method rather than the field itself because the field is also interpolated into hx-headers, and one name cannot be both.

func (Page) CanonicalURL added in v0.14.0

func (p Page) CanonicalURL() string

CanonicalURL is the address search engines should treat as this page's own.

func (Page) ErrorSummary added in v0.25.4

func (p Page) ErrorSummary() []string

ErrorSummary is one line per failed field, for the banner, in sorted field order.

Password must be at least 12 characters

The field name is prepended HERE and not baked into the message. A message written as ":attribute must be at least :min characters" carries the name everywhere, including under the labelled box where the label has just said it. So a message reads bare where it is drawn in context, and named where it is drawn out of context, and there is one message either way.

Sorted rather than in map order, so the banner does not reshuffle between two renders of the same failure.

func (Page) FieldError added in v0.25.4

func (p Page) FieldError(name string) string

FieldError is the first message for a field, or empty.

One message and not all of them, because that is what a form draws: the box has room for one line, and the first is the one that names what to change. It is what components.FieldProps.Error takes:

Error: .FieldError("email")

Empty for a field that was accepted, and empty for every field on a page nobody was rejected on -- so a screen asks unconditionally and draws nothing when there is nothing. There is no @error directive and none is needed.

func (Page) FieldErrors added in v0.25.4

func (p Page) FieldErrors(name string) []string

FieldErrors is every message for a field, for the rare screen that lists them.

func (Page) HasErrors added in v0.25.4

func (p Page) HasErrors() bool

HasErrors reports whether the attempt that landed here was rejected.

It is what a layout asks before drawing the banner. A page with no errors must not draw an empty one: a box that is always there and usually blank is a box people stop reading, which is how a real message goes unseen.

func (p Page) HomeLink() string

HomeLink is where the brand points.

func (Page) IsCurrent added in v0.23.0

func (p Page) IsCurrent(href string) bool

IsCurrent reports whether a navigation target is the page being read.

It compares paths and ignores the query, because "?resent=1" is still the same page -- a navigation that changes when a flash message is added is one nobody can predict.

An empty href is never current: an empty URL draws no control, and a control that is not drawn cannot be the one you are on.

func (p Page) LoginLink() string

LoginLink is the sign-in screen.

func (p Page) LogoutLink() string

LogoutLink is what the sign-out form posts to.

func (Page) OldOr added in v0.25.4

func (p Page) OldOr(name, fallback string) string

OldOr is OldValue, falling back to what the field already held.

It is what an edit form needs and a create form does not: the box starts at the stored value, and comes back carrying the rejected edit rather than reverting to what is in the database -- which would quietly undo the change somebody is in the middle of making.

It answers the fallback when nothing was flashed, and the flashed value even when that value is empty: a field somebody deliberately cleared must not fill itself back in.

func (Page) OldValue added in v0.25.4

func (p Page) OldValue(name string) string

OldValue is what was typed in a field on the attempt that was rejected.

Value: .OldValue("email")

Always empty for a password, by construction rather than by the screen remembering to leave it out. See security.Flash.

func (Page) PageDescription added in v0.14.0

func (p Page) PageDescription() string

PageDescription is the meta description of this page.

func (Page) PageTitle added in v0.14.0

func (p Page) PageTitle() string

PageTitle is what the browser tab shows.

func (p Page) PanelLink() string

PanelLink is the signed-in person's own area, or empty.

func (p Page) RegisterLink() string

RegisterLink is the sign-up screen, or empty when registration is closed.

func (Page) SignedIn added in v0.14.0

func (p Page) SignedIn() bool

SignedIn reports whether there is a session behind this render.

func (Page) SignedInName added in v0.14.0

func (p Page) SignedInName() string

SignedInName is who the navigation bar greets.

func (Page) WithToken added in v0.25.4

func (p Page) WithToken(token string) Page

WithToken sets the CSRF token, which the controller issues.

A method rather than a field in the literal, so that New reads as one expression at the call site. It returns a copy: Page is a value everywhere else, and a builder that mutated in place would be the one method on it that does.

type Renderer

type Renderer = hview.Renderer

Renderer draws a view. It is the concrete side of http.Renderer.

It carries no state -- the registry above is package-level -- which is why the type is an empty struct on both sides of this bridge and the alias is exact.

func NewRenderer

func NewRenderer() *Renderer

NewRenderer returns the renderer. The kernel hands it to the router at boot.

Jump to

Keyboard shortcuts

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