Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions content/contributing/livetemplate.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "Contributing to LiveTemplate Core Library"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "CONTRIBUTING.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# Contributing to LiveTemplate Core Library
Expand Down
4 changes: 2 additions & 2 deletions content/guides/ephemeral-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "Ephemeral Components Guide"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/guides/ephemeral-components.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# Ephemeral Components Guide
Expand Down
4 changes: 2 additions & 2 deletions content/guides/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "LiveTemplate Observability Guide"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/guides/OBSERVABILITY.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# LiveTemplate Observability Guide
Expand Down
4 changes: 2 additions & 2 deletions content/guides/progressive-complexity.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "Progressive Complexity Guide"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/guides/progressive-complexity.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# Progressive Complexity Guide
Expand Down
4 changes: 2 additions & 2 deletions content/guides/scaling.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "LiveTemplate Scaling Guide"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/guides/SCALING.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# LiveTemplate Scaling Guide
Expand Down
4 changes: 2 additions & 2 deletions content/guides/standard-html-reactivity.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "Standard HTML Reactivity"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/guides/standard-html-reactivity.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# Standard HTML Reactivity
Expand Down
13 changes: 8 additions & 5 deletions content/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "Go Library API Reference"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/references/api-reference.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# Go Library API Reference
Expand Down Expand Up @@ -111,7 +111,9 @@ handler := tmpl.Handle(&TodoController{DB: db}, livetemplate.AsState(&TodoState{

| Option | Description |
|--------|-------------|
| `WithStore(store SessionStore)` | Override the session store for this handler |
| `WithEphemeralSweepTTL(ttl time.Duration)` | How long an idle ephemeral HTTP cache entry survives before eviction (default 30m; no effect in persistent mode) |

> The session store is set at construction via `WithSessionStore` (a `New` option), not per-handler.

---

Expand Down Expand Up @@ -423,11 +425,11 @@ Options passed to `New()`:

| Option | Signature | Description |
|--------|-----------|-------------|
| `WithDevMode` | `(enabled bool)` | Enable development mode |
| `WithDevMode` | `(enabled bool)` | Enable dev mode: allows all WS origins, verbose logging, `{{.lvt.DevMode}}` |
| `WithSessionStore` | `(store SessionStore)` | Set session store |
| `WithAuthenticator` | `(auth Authenticator)` | Set authenticator |
| `WithAllowedOrigins` | `(origins []string)` | Allowed WebSocket origins |
| `WithPermissiveOriginCheck` | `()` | Bypass origin check (dev only) |
| `WithPermissiveOriginCheck` | `()` | Bypass origin check without dev mode (`WithDevMode` already relaxes origins) |
| `WithMaxConnections` | `(max int64)` | Max WebSocket connections |
| `WithMaxConnectionsPerGroup` | `(max int64)` | Max connections per group |
| `WithWebSocketDisabled` | `()` | HTTP-only mode |
Expand All @@ -437,6 +439,7 @@ Options passed to `New()`:
| `WithCookieMaxAge` | `(maxAge time.Duration)` | Session cookie max age |
| `WithUpgrader` | `(upgrader *websocket.Upgrader)` | Custom WebSocket upgrader |
| `WithParseFiles` | `(files ...string)` | Explicit template files |
| `WithParseFS` | `(fsys fs.FS, patterns ...string)` | Templates from an `fs.FS` (e.g. `embed.FS`); precedence over `WithParseFiles` |
| `WithTemplateBaseDir` | `(dir string)` | Template discovery base dir |
| `WithIgnoreTemplateDirs` | `(dirs ...string)` | Skip directories during discovery |
| `WithUpload` | `(name string, config UploadConfig)` | Configure upload field |
Expand Down
4 changes: 2 additions & 2 deletions content/reference/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "Authentication Reference"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/references/authentication.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# Authentication Reference
Expand Down
4 changes: 2 additions & 2 deletions content/reference/client-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "Client Attributes Reference"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/references/client-attributes.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# Client Attributes Reference
Expand Down
10 changes: 5 additions & 5 deletions content/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "LiveTemplate Configuration Guide"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/references/CONFIGURATION.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# LiveTemplate Configuration Guide
Expand Down Expand Up @@ -140,9 +140,9 @@ Enable development mode.
- **Accepted values**: `true`, `false`, `1`, `0`, `yes`, `no`, `on`, `off` (case-insensitive)

**Features when enabled**:
- Uses local client library instead of CDN
- Allows **all** WebSocket origins — disables the same-origin/CSRF check, so `WithPermissiveOriginCheck()` is redundant in dev (**never enable in production**)
- More verbose logging
- Less strict origin checking
- Exposes `{{.lvt.DevMode}}` to templates

**Use case**: Local development and debugging.

Expand Down Expand Up @@ -345,7 +345,7 @@ tmpl := livetemplate.New("app",
| `WithPubSubBroadcaster(broadcaster)` | Redis pub/sub for distributed deployments |
| `WithComponentTemplates(sets...)` | Register component template sets |
| `WithIgnoreTemplateDirs(dirs...)` | Skip directories during template discovery |
| `WithPermissiveOriginCheck()` | Bypass origin check (dev only) |
| `WithPermissiveOriginCheck()` | Bypass origin check without dev mode (`WithDevMode` already relaxes origins) |
| `WithProgressiveEnhancement(enabled)` | Non-JS form submission support (default: true) |
| `WithCookieMaxAge(duration)` | Session cookie max age (default: 365 days) |

Expand Down
134 changes: 130 additions & 4 deletions content/reference/controller-pattern.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "Controller+State Pattern Reference"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/references/controller-pattern.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# Controller+State Pattern Reference
Expand Down Expand Up @@ -213,10 +213,100 @@ func (c *TodoController) OnDisconnect() {
- Cancel background jobs
- Unsubscribe from data feeds

## State methods in templates

Exported zero-arg methods on your State type are callable from templates just like
fields — `{{.ActiveCount}}` resolves to `ActiveCount()`'s return value. Methods
returning `(T, error)` are omitted (with a warning) when the error is non-nil.

Because LiveTemplate converts State to a map before rendering, these methods are
*precomputed* — but only for methods your templates actually reference. A method that
appears nowhere in any template is never called, so an expensive or side-effecting
helper you keep on State but don't render costs nothing.

Two caveats:

- **Conditional references still run.** The scoping is by name, not by reachability:
`{{if .Show}}{{.Expensive}}{{end}}` still calls `Expensive()` on every render even
when `.Show` is false, because the name appears in the template text. Keep genuinely
expensive work in an action method, not a render-time State method.
- **Prefer methods without side effects.** Precompute timing is an implementation
detail; a State method should compute a view of the state, not mutate anything or
perform I/O.

### Methods that take arguments (view helpers)

Sometimes you want a method that takes a key and returns a per-item view — e.g. a
CSS class for a given row, or a formatted label for a given id. **Put it on a struct
field, not on the top-level State**, and call it through that field:

```go
type RowViews struct{ selected map[string]bool }

func (v RowViews) Class(id string) string {
if v.selected[id] {
return "row selected"
}
return "row"
}

type BoardState struct {
Rows []Row
Views RowViews // a "view helper" sub-struct
}
```

```html
{{range .Rows}}
<div class="{{$.Views.Class .ID}}">{{.Label}}</div>
{{end}}
```

This works in both the initial HTTP render and WebSocket updates, and replaces the
common workaround of precomputing a `map[string]string` in an action and looking it
up by key in the template. It is the same shape as the framework's built-in
`{{.lvt.AriaInvalid "field"}}`.

**Why the field matters — top-level arg-methods are not supported.** LiveTemplate
converts State to a map before rendering (to inject the `{{.lvt}}` namespace), and
Go's `html/template` cannot call an argument-accepting method once its receiver is a
map key — `{{.Class .ID}}` directly on State fails with *"Class is not a method but
has arguments"*. A struct **field** is stored in the map as a struct value, so both
renderers call its methods natively. Zero-arg State methods are unaffected (they are
precomputed, as above); only *argument-accepting* methods need the field.

## Context API

For the complete Context API (data extraction, HTTP operations, struct binding), see [API Reference — Context](api-reference.md#context).

### Request context

`*livetemplate.Context` **embeds `context.Context`** — it carries the request/connection
context (cancellation, deadline, request-scoped values). So `ctx` *is* a `context.Context`:
pass it directly to any context-aware call (database queries, outbound HTTP, tracing) instead
of `context.Background()`.

```go
func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
// ✅ ctx is a context.Context — propagates cancellation when the request/connection ends
if err := c.DB.InsertTodo(ctx, todo); err != nil {
return state, err
}
return state, nil
}
```

Reaching for `context.Background()` inside an action instead silently discards that
cancellation and any deadline or trace attached to the request:

```go
// ❌ throws away request-scoped cancellation, deadline, and tracing
if err := c.DB.InsertTodo(context.Background(), todo); err != nil { ... }
```

Use `context.Background()` only for work that must deliberately **outlive** the request (e.g.
a fire-and-forget goroutine you start from the action) — not for the action's own calls.

## Error Handling

For validation errors, field errors, and template error display, see [Error Handling Reference](error-handling.md).
Expand Down Expand Up @@ -266,7 +356,8 @@ type TodoController struct {
}

func (c *TodoController) Mount(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
items, err := c.DB.GetTodos()
// Pass ctx (a context.Context) so the query is cancelled if the request ends.
items, err := c.DB.GetTodos(ctx)
if err != nil {
return state, fmt.Errorf("failed to load todos: %w", err)
}
Expand All @@ -285,7 +376,7 @@ func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoSt
Title: title,
}

if err := c.DB.InsertTodo(todo); err != nil {
if err := c.DB.InsertTodo(ctx, todo); err != nil {
return state, fmt.Errorf("database error")
}

Expand Down Expand Up @@ -390,6 +481,22 @@ func (c *ChatController) RefreshMessages(state ChatState, ctx *livetemplate.Cont
}
```

**The published action reloads data only — it is not a re-Mount.** Notice `RefreshMessages` re-reads the messages and nothing else. Reach for the tempting shortcut and it bites you:

```go
// ❌ Don't do this. A fan-out tick is neither a page load nor a connect.
func (c *ChatController) RefreshMessages(state ChatState, ctx *livetemplate.Context) (ChatState, error) {
return c.Mount(state, ctx)
}
```

`Mount` does **connect-time** work on top of loading: it `Subscribe`s to topics and runs any `IsInitialMount()`/`IsReconnect()`/`IsNewConnect()`-guarded setup (analytics, background goroutines, presence). A fan-out dispatch is an *action* — its connect-kind is `ConnectKindAction`, so all three of those helpers return `false`. Delegating the action to `Mount` therefore means any connect-kind-guarded setup **silently never runs**, and the unguarded `Subscribe` re-runs on every single broadcast — a cheap no-op for `ctx.SelfTopic()` (ACL-exempt and idempotent, as here), but a real per-tick ACL re-check for a developer topic like `Subscribe("room/" + id)`. Keep the two jobs separate:

- **`Mount`** — subscribe + load. Runs on HTTP GET/POST, WS connect, WS reconnect.
- **The published action** (`RefreshMessages`) — load only. Runs on every fan-out tick.

The shortcut *appears* to work when `Mount` is a pure data-load with no `Subscribe` or guarded setup (its fields just get re-set to the same values) — which is exactly why it's a trap: it breaks silently the day `Mount` grows a connect-time concern. So split them unconditionally, even while `Mount` is still a plain load; don't wait for the break. The runnable [`live-dashboard`](https://github.com/livetemplate/docs/tree/main/examples/live-dashboard) example follows the split from the start: `Mount` subscribes then snapshots; its `Refresh` action snapshots only.

**Ordering.** `Publish` queues onto a per-action drain. Call it **after** every `ctx.With*()` shallow-copy mutation; publishes queued before a `With*()` are stranded on the pre-copy Context and won't propagate.

**Cap.** A single action can enqueue at most `MaxPublishesPerAction` (declared in `topic_context.go`) `Publish` calls before subsequent calls become hard errors.
Expand Down Expand Up @@ -447,6 +554,25 @@ handler := tmpl.Handle(controller, livetemplate.AsState(initialState))
http.Handle("/", handler)
```

### Serving static assets alongside the app

The handler returned by `Handle` is mounted at `/`, where Go's `ServeMux` treats it as a **catch-all**: every request that doesn't match a more specific pattern falls through to it (including the WebSocket upgrade). To serve static files — images, CSS, downloads — next to your app, register them under a **more specific prefix** on the same mux. Longest-prefix-match routes those requests to the file server and everything else to the app:

```go
handler := tmpl.Handle(controller, livetemplate.AsState(initialState))

// Files under /assets/… are served from ./assets. http.Dir already rejects
// "../" traversal, so http.FileServer needs no extra guard.
http.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets"))))

// The app owns everything else.
http.Handle("/", handler)
```

The prefix just has to be distinct from your app's own routes — `ServeMux` does the rest. This composition is plain `net/http`; the framework adds nothing here on purpose.

**When a prefix isn't enough.** If your app renders content that references assets at **arbitrary top-level paths** — e.g. user-authored HTML with `<img src="diagram.png">` resolving to `/diagram.png` — those paths can collide with app routes, and `ServeMux` can't disambiguate them by prefix. That case needs a fall-through wrapper: *serve the file if it exists under a safe root, otherwise defer to the app handler.* Keep it a small wrapper in your own `main` rather than reaching for a framework primitive — the security-relevant policy (which extensions to serve, embedded vs. disk, symlink resolution) is app-specific, and `http.Dir` / `http.FileServer` already supply the traversal-safe file access it builds on.

## Upload Access

For file upload configuration and handling, see [Upload Reference](uploads.md).
Expand Down
4 changes: 2 additions & 2 deletions content/reference/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "Error Handling Reference"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/references/error-handling.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# Error Handling Reference
Expand Down
4 changes: 2 additions & 2 deletions content/reference/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "Current Limitations"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/references/current-limitations.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# Current Limitations
Expand Down
4 changes: 2 additions & 2 deletions content/reference/navigate.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "Navigate Action Reference"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/references/navigate.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# Navigate Action Reference
Expand Down
4 changes: 2 additions & 2 deletions content/reference/progressive-complexity.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "Progressive Complexity Reference"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/references/progressive-complexity-reference.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# Progressive Complexity Reference
Expand Down
4 changes: 2 additions & 2 deletions content/reference/pubsub.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
title: "PubSub Reference"
source_repo: "https://github.com/livetemplate/livetemplate"
source_path: "docs/references/pubsub.md"
source_ref: "v0.16.0"
source_commit: "f4f9147c7066382d821c022caa48683d0886ad9a"
source_ref: "v0.19.0"
source_commit: "62f903a3b235b047bd3a612fb7da90daa1ccad04"
---

# PubSub Reference
Expand Down
Loading
Loading