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}}
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).
}))
}
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
}