Standard HTML Reactivity

LiveTemplate makes standard HTML reactive by default. A plain <form method="POST"> with <button name="add"> is interactive at every transport level — no framework-specific attributes required. This guide explains how it works, how it compares to other frameworks, and the tradeoffs involved.

Recent reinforcement: As of client v0.8.38, the TypeScript client and the generated templates went through a deliberate "attribute reduction" pass that removed lvt-* attributes from anything HTML can already express. Tier 1 standard HTML is now the default everywhere; Tier 2 attributes are reserved for behaviors HTML genuinely cannot express (timing, keyboard shortcuts, reactive DOM).


Why Standard HTML?

Every interactive feature in a traditional web app requires the same ceremony: design a REST endpoint, write a serializer, manage client-side state, update the DOM, and wire it all together. That overhead discourages interactivity — teams leave things static not because they should be, but because the plumbing isn't worth it. As Chris McCord put it when explaining why he built Phoenix LiveView: conventional frameworks make you "fetch the world, munge it into some format, and shoot it over the wire... then throw all that state away" on every request.

LiveView's answer was to keep all state on the server and push rendered updates over a persistent connection. LiveTemplate brings that approach to Go, with one major difference: it works equally well over standard HTTP. And it goes a step further — the HTML itself needs no framework-specific attributes for core interactions.

How It Works

Button Name = Action Routing

The name attribute on a button routes to a Go method:

<button name="add">Add</button>       <!-- routes to Add() -->
<button name="delete">Delete</button>  <!-- routes to Delete() -->

This uses standard HTML semantics — the button name is included in form data on submit. LiveTemplate reads it and dispatches to the matching method. No custom attributes needed.

Form Auto-Interception

All <form> elements inside a LiveTemplate handler are automatically intercepted:

The same HTML works identically across all three modes.

Validation

For production form validation, use ctx.BindAndValidate() with Go struct tags:

// validate is a *validator.Validate from github.com/go-playground/validator/v10,
// typically initialized once and stored on the controller.
var input struct {
    Email string `validate:"required,email,min=5"`
}
if err := ctx.BindAndValidate(&input, c.validate); err != nil {
    return state, err // field errors sent to template automatically
}

For HTML-attribute-based validation (required, pattern, min, max), see the Error Handling reference for the ValidateForm + WithFormSchema pattern.


Multi-User Broadcast

When one user's action should be visible to other WebSocket-connected tabs, use BroadcastAction. Note: BroadcastAction must be called AFTER all state mutations and ctx.With*() calls, because With*() creates shallow copies and broadcasts queued before the copy won't propagate.

func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
    state.Items = append(state.Items, Todo{Title: ctx.GetString("title")})
    // BroadcastAction after all state changes — pushes to other WS-connected tabs
    ctx.BroadcastAction("Refresh", nil)
    return state, nil
}

func (c *TodoController) Refresh(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
    state.Items = c.loadItems()
    return state, nil
}

Broadcast is scoped to the session group. For multi-instance deployments, add Redis pub/sub:

tmpl, _ := livetemplate.New("app",
    livetemplate.WithPubSubBroadcaster(redisBroadcaster),
)

See PubSub Reference for details.


Comparison with Other Frameworks

Every major reactive framework requires custom attributes on HTML elements. LiveTemplate is unique in making standard HTML reactive without modification.

Framework Form markup required Custom attributes
htmx <form hx-post="/todos" hx-target="#list"> hx-post, hx-target, hx-swap, hx-trigger
Laravel Livewire <form wire:submit="add"> wire:submit, wire:model, wire:click
Phoenix LiveView <form phx-submit="add"> phx-submit, phx-click, phx-change
LiveTemplate <form method="POST"> None for standard interactions

htmx

htmx extends HTML with hx-* attributes for AJAX interactions. A form without hx-post submits normally (full page reload). Every interactive element needs explicit hx-* attributes.

Laravel Livewire

Livewire uses wire:* directives in PHP/Blade templates. wire:submit captures form submissions, wire:model enables two-way binding. State is serialized into HTML attributes.

Phoenix LiveView

LiveView uses phx-* attributes and requires a persistent WebSocket connection. Forms need phx-submit to route actions. The initial page renders as static HTML, then upgrades to WebSocket.

LiveTemplate

Standard HTML forms work reactively without any framework attributes. The button name routes to a Go method, form data is available via ctx.GetString(), and the response is a minimal tree diff. WebSocket is optional — only needed for server-initiated broadcasts.


Feature Gap vs Phoenix LiveView

LiveTemplate is inspired by Phoenix LiveView but does not yet cover its full feature set. Tracked gaps as of v0.8.23:

Feature LiveView LiveTemplate Notes
Live Navigation push_navigate, push_patch Partial — __navigate__ action covers same-handler query-string navigation (no reconnect). Different-handler nav still falls back to fetch or full page load. See Navigate Action.
Stateful Components LiveComponent with own lifecycle Stateless templates only {{template}} invocations work but have no component-level state or event handling.
Streams stream/3 for large lists Not yet LiveView streams handle large/infinite lists without keeping all items in server memory. Streaming-range rendering (PRs #366/#368/#369/#370) is the latest step toward this.
JS Commands JS.push, JS.toggle, JS.show Partial lvt-* reactive attributes cover common cases (disable, add/remove class, set attribute) but aren't as composable as LiveView's server-defined JS chains.
Client Hooks phx-hook lifecycle callbacks Proposed lvt-hook proposal covers third-party JS library integration; not yet shipped.
Presence Phoenix.Presence Not built-in Can be built on LiveTemplate's session stores; requires manual implementation.
Testing Helpers live/2, render_click/3 Minimal AssertPureState exists; no view-level test DSL. Browser tests use chromedp.
Form Recovery Automatic on reconnect Partial — lvt-form:preserve retains specific fields across re-renders Full automatic recovery on WS reconnection is not yet built in.

For day-to-day workarounds, see Current Limitations.


Progressive Complexity

LiveTemplate follows a two-tier model:

Tier What you write When to use
Tier 1: Standard HTML <form>, <button name="add">, <dialog>, <a href> Forms, actions, modals, navigation
Tier 2: lvt-* attributes lvt-on:, lvt-mod:debounce, lvt-el:, lvt-fx: Timing, keyboard shortcuts, reactive DOM

Tier 2 is only for behaviors standard HTML cannot express. For example, debounced search requires lvt-mod:debounce because HTML has no timing mechanism:

<input name="Query" value="{{.Query}}"
    lvt-on:input="search" lvt-mod:debounce="300"
    placeholder="Search...">

See the Progressive Complexity Guide for the complete walkthrough.


Tradeoffs

Approach Philosophy Clarity Flexibility
Custom attributes (htmx, Livewire, LiveView) Explicit is better than implicit High — clear what's reactive High — opt-in reactivity
Standard HTML (LiveTemplate) Make the common case simple Lower — everything is reactive Lower — opt-out via lvt-form:no-intercept / lvt-nav:no-intercept

Advantages of LiveTemplate's approach:

Disadvantages:


See Also