Flash Messages

Set a message server-side with ctx.SetFlash("key", text) and render it with {{.lvt.FlashTag "key"}}, which emits an accessible <output> tag. Over a WebSocket connection a flash persists across renders until you ctx.ClearFlash(key) it or it auto-prunes via FlashExpiry(d); over plain HTTP it is one-shot. This example shows a required-field error that stays put, a success toast that expires after 5s, and a notice that lingers until dismissed.

Flash Messages / Toasts

Server-set messages that render via {{.lvt.FlashTag "key"}} as <output role="…" data-flash="…">. On WebSocket connections flash persists across renders until cleared with ctx.ClearFlash(key) or auto-pruned via FlashExpiry(d). On HTTP it's inherently one-shot.

Try: Submit empty → persistent error. Type a name + Save → success that auto-dismisses after 5s (a server-side goroutine fires session.TriggerAction("refresh", nil) at the deadline; the resulting render's getMessages prunes the expired flash). Click Notify → persistent info until you click Dismiss.

Template

Two forms plus three FlashTag slots — one each for the success, error, and info keys the handler sets.

{{define "content"}}
<article>
    <h3>Flash Messages / Toasts</h3>
    <p><small>Server-set messages that render via <code>{{"{{.lvt.FlashTag \"key\"}}"}}</code> as <code>&lt;output role="…" data-flash="…"&gt;</code>. On WebSocket connections flash <strong>persists across renders</strong> until cleared with <code>ctx.ClearFlash(key)</code> or auto-pruned via <code>FlashExpiry(d)</code>. On HTTP it's inherently one-shot.</small></p>

    <form method="POST">
        <fieldset role="group">
            <input name="name" placeholder="Enter name…" aria-label="Name">
            <button name="save">Save</button>
        </fieldset>
    </form>

    <form method="POST">
        <fieldset role="group">
            <button name="notify" type="submit">Notify</button>
            <button name="dismissNotify" type="submit" class="compact secondary">Dismiss</button>
        </fieldset>
    </form>

    {{.lvt.FlashTag "success"}}
    {{.lvt.FlashTag "error"}}
    {{.lvt.FlashTag "info"}}

    <p><small><strong>Try:</strong> Submit empty → persistent error. Type a name + Save → success that auto-dismisses after 5s (a server-side goroutine fires <code>session.TriggerAction("refresh", nil)</code> at the deadline; the resulting render's <code>getMessages</code> prunes the expired flash). Click <strong>Notify</strong> → persistent info until you click <strong>Dismiss</strong>.</small></p>
</article>
{{end}}

flash-messages.tmpl

Handler & state

Save sets either an error or a 5s-expiring success; Notify / DismissNotify set and clear a persistent info flash. Because FlashExpiry is render-driven, a small goroutine nudges a re-render at the deadline so the expired toast actually leaves the DOM.

type FlashMessagesController struct{}

func (c *FlashMessagesController) Save(state FlashMessagesState, ctx *livetemplate.Context) (FlashMessagesState, error) {
	name := strings.TrimSpace(ctx.GetString("name"))
	if name == "" {
		ctx.ClearFlash("success")
		ctx.SetFlash("error", "Name is required")
		return state, nil
	}
	ctx.ClearFlash("error")
	ctx.SetFlash("success", "Saved: "+name, livetemplate.FlashExpiry(flashSuccessExpiry))
	nudgeFlashExpiry(ctx, flashSuccessExpiry)
	return state, nil
}

// Refresh is a no-op action whose only purpose is to trigger a re-render
// (and therefore a getMessages snapshot, which prunes expired flash).
// Invoked by the goroutine in Save after FlashExpiry elapses.
func (c *FlashMessagesController) Refresh(state FlashMessagesState, ctx *livetemplate.Context) (FlashMessagesState, error) {
	return state, nil
}

func (c *FlashMessagesController) Notify(state FlashMessagesState, ctx *livetemplate.Context) (FlashMessagesState, error) {
	ctx.SetFlash("info", "Heads up — this stays until you dismiss it")
	return state, nil
}

func (c *FlashMessagesController) DismissNotify(state FlashMessagesState, ctx *livetemplate.Context) (FlashMessagesState, error) {
	ctx.ClearFlash("info")
	return state, nil
}

func flashMessagesHandler() http.Handler {
	tmpl := newLayoutTmpl("templates/layout.tmpl", "templates/feedback/flash-messages.tmpl")
	return tmpl.Handle(&FlashMessagesController{}, livetemplate.AsState(&FlashMessagesState{
		Title:    "Flash Messages",
		Category: "Visual Feedback",
	}))
}

handlers_feedback.go:115-154

type FlashMessagesState struct {
	Title    string
	Category string
}

state_feedback.go:39-43

When to use

For the same feedback during a slow action rather than after it, see Loading States.

source: livetemplate/docs · path: examples/patterns/templates/feedback/flash-messages.tmpl