LiveTemplate Counter Example

A real-time counter application demonstrating LiveTemplate's reactive state management and tree-based optimization.

Features

Running the Example

  1. Start the server:

    From project root:

    go run examples/counter/main.go
    

    Or from the counter directory:

    cd examples/counter
    go run main.go
    

    With custom port:

    PORT=8081 go run main.go
    

    With environment-based configuration:

    # Development mode with connection limits
    LVT_DEV_MODE=true LVT_MAX_CONNECTIONS=100 go run main.go
    
    # Production mode with allowed origins
    LVT_ALLOWED_ORIGINS="https://example.com" LVT_LOG_LEVEL=info go run main.go
    
  2. Open your browser: Navigate to http://localhost:8080

  3. Interact with the counter:

    • Click +1 to increment the counter
    • Click -1 to decrement the counter
    • Click Reset to reset to zero
    • Watch the conditional text change based on the counter value

Configuration

This example uses LiveTemplate's environment-based configuration system. All configuration is loaded from environment variables with the LVT_ prefix:

Variable Default Description
LVT_DEV_MODE false Enable development mode (uses local client library)
LVT_MAX_CONNECTIONS 0 (unlimited) Maximum concurrent WebSocket connections
LVT_MAX_CONNECTIONS_PER_GROUP 0 (unlimited) Maximum connections per session group
LVT_ALLOWED_ORIGINS empty Comma-separated list of allowed WebSocket origins
LVT_LOG_LEVEL info Logging level (debug, info, warn, error)
LVT_METRICS_ENABLED true Enable Prometheus metrics export
LVT_SHUTDOWN_TIMEOUT 30s Graceful shutdown timeout

Example configurations:

# Development
LVT_DEV_MODE=true LVT_LOG_LEVEL=debug go run main.go

# Production with limits
LVT_MAX_CONNECTIONS=10000 LVT_ALLOWED_ORIGINS="https://example.com" go run main.go

# Disable metrics for testing
LVT_METRICS_ENABLED=false go run main.go

For more details, see CONFIGURATION.md.

How It Works

Server Side (Go)

The server is extremely simple with the new reactive API:

// Controller: singleton, holds dependencies (none in this simple example)
type CounterController struct{}

// State: pure data, cloned per session
type CounterState struct {
    Title       string `json:"title" lvt:"persist"`
    Counter     int    `json:"counter" lvt:"persist"`
    LastUpdated string `json:"last_updated" lvt:"persist"`
}

// Named action methods — routed via <button name="increment">
func (c *CounterController) Increment(state CounterState, ctx *livetemplate.Context) (CounterState, error) {
    state.Counter++
    state.LastUpdated = formatTime()
    return state, nil
}

func (c *CounterController) Decrement(state CounterState, ctx *livetemplate.Context) (CounterState, error) {
    state.Counter--
    state.LastUpdated = formatTime()
    return state, nil
}

func (c *CounterController) Reset(state CounterState, ctx *livetemplate.Context) (CounterState, error) {
    state.Counter = 0
    state.LastUpdated = formatTime()
    return state, nil
}

func main() {
    envConfig, _ := livetemplate.LoadEnvConfig()

    controller := &CounterController{}
    initialState := &CounterState{Title: "Live Counter", Counter: 0, LastUpdated: formatTime()}

    tmpl := livetemplate.Must(livetemplate.New("counter", envConfig.ToOptions()...))
    http.Handle("/", tmpl.Handle(controller, livetemplate.AsState(initialState)))
    http.ListenAndServe(":8080", nil)
}

Key concepts:

Client Side (JavaScript)

Zero-config integration - just add one script tag:

<!-- In your template — standard HTML, no special attributes needed -->
<button name="increment">+1</button>
<button name="decrement">-1</button>
<button name="reset">Reset</button>

<!-- Auto-initializing client library -->
<script src="livetemplate-client.js"></script>

That's it! No JavaScript code needed. The client library auto-initializes and handles:

Sending Actions with Data

Actions use standard HTML forms with button name routing and hidden inputs for data:

<!-- Simple action via button name -->
<button name="increment">+1</button>

<!-- Form with multiple fields -->
<form method="POST" name="add">
    <input name="title" type="text">
    <input name="priority" type="number">
    <button type="submit">Add</button>
</form>

<!-- Data via hidden inputs -->
<form method="POST">
    <input type="hidden" name="id" value="123">
    <button name="delete">Delete Item</button>
</form>

Form field values are accessed in the controller via ctx.GetString(), ctx.GetInt(), or ctx.BindAndValidate():

func (c *Controller) Delete(state State, ctx *livetemplate.Context) (State, error) {
    id := ctx.GetInt("id")
    // ...
}

Tier 2 attributes (use only when standard HTML can't express it):

LiveTemplate Integration

Architecture

Browser                    WebSocket/HTTP              Go Server
┌─────────────────┐        ┌──────────┐               ┌──────────────────┐
│ counter.tmpl    │        │          │               │ CounterState     │
│ (rendered HTML) │        │          │               │   implements     │
│                 │        │          │               │   Store          │
│ [+1] [-1] [Reset]◄──────►│  /live   │◄─────────────►│                  │
│                 │        │          │               │ Change(action,   │
│ LiveTemplate    │        │          │               │   data)          │
│ Client JS       │        │          │               │                  │
│                 │        │          │               │ Handle()         │
│ Button name     │        │ Auto-    │               │ - Clones state   │
│ routing         │        │ detects  │               │ - Generates      │
│ (Tier 1 HTML)   │        │ transport│               │   updates        │
│                 │        │          │               │ - Broadcasts     │
└─────────────────┘        └──────────┘               └──────────────────┘

Example Update Payloads

Initial State (counter = 0):

{
  "s": ["<!DOCTYPE html><html>...", "...</html>"],
  "0": "Live Counter",
  "1": "0",
  "2": "zero",
  "3": "Counter is zero",
  "4": "2025-09-30 00:20:00",
  "5": "session-1727654400"
}

After Increment (only changed values):

{
  "1": "1",
  "2": "positive",
  "3": "Counter is positive",
  "4": "2025-09-30 00:20:05"
}

This demonstrates LiveTemplate's bandwidth efficiency - subsequent updates contain only the 4 changed dynamic values instead of the full HTML document.

Template Structure

The template follows the same pattern as testdata/e2e/counter/input.tmpl:

Development Notes

Controller+State Pattern

The counter uses the Controller+State pattern introduced in v0.7.0:

// Controller: singleton, holds dependencies
controller := &CounterController{}

// State: pure data, cloned per session
initialState := &CounterState{Title: "Live Counter", Counter: 0}

tmpl := livetemplate.Must(livetemplate.New("counter"))
http.Handle("/", tmpl.Handle(controller, livetemplate.AsState(initialState)))