Loading States

While an action is in flight the framework automatically marks the submitting form aria-busy="true" and disables its <fieldset> — no directives required. When you want more, lvt-form:disable-with swaps the button's text for the pending duration, and lvt-el:setAttr:on:pending / :on:done toggle any attribute reactively across the action lifecycle. All three tiers here call the same 2-second slowSave action.

Loading States

Three layers of feedback during a slow action. All three forms call the same slowSave action (sleeps 2s server-side) so you can see the different presentations side by side.

Tier 1 — Automatic

The framework adds aria-busy="true" to the form and disabled to its <fieldset> while the action is in flight. No directives needed.

Tier 2 — lvt-form:disable-with

Custom button text during pending. Restored when the action completes.

Tier 2 — lvt-el:setAttr reactive attribute

Toggle aria-busy via :on:pending / :on:done. Multi-action pages can scope this to a specific action (e.g. :on:slowSave:pending); leaving the action name out matches every action's lifecycle, which is fine here.

Template

Three forms, one action — each demonstrates a different feedback tier, from zero-directive automatic busy state up to a reactive attribute toggle.

{{define "content"}}
<article>
    <h3>Loading States</h3>
    <p><small>Three layers of feedback during a slow action. All three forms call the same <code>slowSave</code> action (sleeps 2s server-side) so you can see the different presentations side by side.</small></p>

    <section>
        <h4>Tier 1 — Automatic</h4>
        <p><small>The framework adds <code>aria-busy="true"</code> to the form and <code>disabled</code> to its <code>&lt;fieldset&gt;</code> while the action is in flight. No directives needed.</small></p>
        <form method="POST">
            <fieldset role="group">
                <input name="data" placeholder="Type something…" aria-label="Tier 1 input">
                <button name="slowSave">Save</button>
            </fieldset>
        </form>
    </section>

    <section>
        <h4>Tier 2 — <code>lvt-form:disable-with</code></h4>
        <p><small>Custom button text during pending. Restored when the action completes.</small></p>
        <form method="POST">
            <fieldset role="group">
                <input name="data" placeholder="Type something…" aria-label="Tier 2a input">
                <button name="slowSave" lvt-form:disable-with="Saving…">Save</button>
            </fieldset>
        </form>
    </section>

    <section>
        <h4>Tier 2 — <code>lvt-el:setAttr</code> reactive attribute</h4>
        <p><small>Toggle <code>aria-busy</code> via <code>:on:pending</code> / <code>:on:done</code>. Multi-action pages can scope this to a specific action (e.g. <code>:on:slowSave:pending</code>); leaving the action name out matches every action's lifecycle, which is fine here.</small></p>
        <form method="POST">
            <fieldset role="group">
                <input name="data" placeholder="Type something…" aria-label="Tier 2b input">
                <button name="slowSave"
                    lvt-el:setAttr:on:pending="aria-busy:true"
                    lvt-el:setAttr:on:done="aria-busy:false">Save</button>
            </fieldset>
        </form>
    </section>

    {{/* All three sections call the same `slowSave` action, so this single
         indicator updates regardless of which form was submitted. */}}
    {{if .LastSave}}
    <p><small>Last save: <strong>{{.LastSave}}</strong></small></p>
    {{end}}
</article>
{{end}}

loading-states.tmpl

Handler & state

SlowSave sleeps two seconds to make the pending window visible, then stamps the time.

type LoadingStatesController struct{}

const slowSaveDelay = 2 * time.Second

func (c *LoadingStatesController) SlowSave(state LoadingStatesState, ctx *livetemplate.Context) (LoadingStatesState, error) {
	// Real handlers should honor ctx.Context().Done(); plain Sleep is fine for a 2s demo.
	time.Sleep(slowSaveDelay)
	state.LastSave = time.Now().Format("15:04:05")
	return state, nil
}

func loadingStatesHandler() http.Handler {
	tmpl := newLayoutTmpl("templates/layout.tmpl", "templates/feedback/loading-states.tmpl")
	return tmpl.Handle(&LoadingStatesController{}, livetemplate.AsState(&LoadingStatesState{
		Title:    "Loading States",
		Category: "Visual Feedback",
	}))
}

handlers_feedback.go:46-64

type LoadingStatesState struct {
	Title    string
	Category string
	LastSave string
}

state_feedback.go:21-26

When to use

To run that slow work without blocking the rest of the page, see Async Operations.

source: livetemplate/docs · path: examples/patterns/templates/feedback/loading-states.tmpl