Live Preview

When the controller defines a Change() method, the framework auto-binds it to input/change events on form fields with a 300ms debounce — no extra attribute needed. Change reads ctx.GetString("input") and updates state.Preview only; it deliberately never writes back to state.Input, because patching the input's value mid-typing would reset the cursor. An explicit Submit commits the value.

Live Preview

When the controller has a Change() method, the framework auto-binds it to input/change events on form fields with a 300ms debounce — no lvt-on:input attribute needed. The handler reads ctx.GetString("input") and updates state.Preview only; writing back to state.Input would patch the input's value attribute mid-typing and reset the cursor.

Try: Type in the input — the preview updates ~300ms after you stop typing. Click Save to commit the value to state.Input (the field is lvt:"persist", so it survives reconnect). The cursor never jumps because Change() only updates the preview.

Template

One input bound to Change() and an <output> that mirrors the live preview.

{{define "content"}}
<article>
    <h3>Live Preview</h3>
    <p><small>When the controller has a <code>Change()</code> method, the framework auto-binds it to <code>input</code>/<code>change</code> events on form fields with a 300ms debounce — no <code>lvt-on:input</code> attribute needed. The handler reads <code>ctx.GetString("input")</code> and updates <code>state.Preview</code> only; writing back to <code>state.Input</code> would patch the input's <code>value</code> attribute mid-typing and reset the cursor.</small></p>

    <form method="POST">
        <label for="live-input">Name</label>
        <fieldset role="group">
            <input id="live-input" name="input" value="{{.Input}}" placeholder="Type your name…">
            <button name="submit">Save</button>
        </fieldset>
    </form>

    <output id="preview" aria-label="Preview">{{.Preview}}</output>

    <p><small><strong>Try:</strong> Type in the input — the preview updates ~300ms after you stop typing. Click Save to commit the value to <code>state.Input</code> (the field is <code>lvt:"persist"</code>, so it survives reconnect). The cursor never jumps because <code>Change()</code> only updates the preview.</small></p>
</article>
{{end}}

live-preview.tmpl

Handler & state

Change builds the debounced preview; Submit commits the value to the persisted Input field.

type LivePreviewController struct{}

// Change is auto-bound by the framework when the controller exposes it.
// Reads the input's current value via ctx.GetString and updates state.Preview.
// Does NOT write back to state.Input — patching the input element's value
// attribute mid-typing would reset the cursor position. (See
// examples/live-preview/main.go:26-29 for the same constraint.) An explicit
// Submit action commits state.Input on form submission.
func (c *LivePreviewController) Change(state LivePreviewState, ctx *livetemplate.Context) (LivePreviewState, error) {
	if ctx.Has("input") {
		state.Preview = "Hello, " + ctx.GetString("input") + "!"
	}
	return state, nil
}

func (c *LivePreviewController) Submit(state LivePreviewState, ctx *livetemplate.Context) (LivePreviewState, error) {
	state.Input = ctx.GetString("input")
	state.Preview = "Saved: " + state.Input
	return state, nil
}

func livePreviewHandler() http.Handler {
	tmpl := newLayoutTmpl("templates/layout.tmpl", "templates/realtime/live-preview.tmpl")
	return tmpl.Handle(&LivePreviewController{}, livetemplate.AsState(&LivePreviewState{
		Title:    "Live Preview",
		Category: "Real-Time & Multi-User",
		// Preview is intentionally empty initially — Change builds the
		// "Hello, …!" value as the user types. Mirrors live-preview/main.go's
		// initial state (preview("")  → empty until the first Change fires).
	}))
}

handlers_realtime.go:264-295

type LivePreviewState struct {
	Title    string
	Category string
	// Input is persisted so a reconnect lands on the user's last-saved value;
	// Preview is derived from Input by Change/Submit so it doesn't need to
	// persist — leaving it unpersisted means a stale derived value can't
	// briefly appear before the next render rebuilds it.
	Input   string `lvt:"persist"`
	Preview string
}

state_realtime.go:58-68

When to use

Reach for Reconnection Recovery when the typed value itself must survive a reload.

source: livetemplate/docs · path: examples/patterns/templates/realtime/live-preview.tmpl