Skip to content
Merged
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
16 changes: 15 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ When a form is submitted, the framework resolves the action in this order:
- `live-preview/` — Tier 1 with `Change()` method for live updates
- `chat/` — Tier 1+2 (uses `lvt-scroll` for auto-scroll)

## Framework Documentation

Before writing code, always consult the LiveTemplate reference docs and guides:

- **References:** `https://github.com/livetemplate/livetemplate/tree/main/docs/references/` — client attributes, server API, action routing
- **Guides:** `https://github.com/livetemplate/livetemplate/tree/main/docs/guides/` — progressive complexity, patterns, best practices

Use framework-native solutions instead of custom JavaScript. Common patterns:
- `input type="search"` has a browser-native clear button; the framework handles the `search` event automatically (no custom JS needed)
- The `Change()` method auto-wires input/change/search events on named form fields with 300ms debounce
- Use `hidden` HTML attribute for visibility toggling (not `style="display:none"`)

## CSS

All examples use [Pico CSS](https://picocss.com/docs) exclusively:
Expand All @@ -76,11 +88,13 @@ All examples use [Pico CSS](https://picocss.com/docs) exclusively:
- Use semantic HTML — Pico auto-styles: `<article>` (cards), `<dialog>` (modals), `<details>` (accordions), `<table>`, `<nav>`, `<progress>`
- Use Pico classes sparingly: `.container`, `.grid`, `.secondary`, `.contrast`, `.outline`
- Use `aria-invalid="true"` for form validation errors, `<small>` for helper/error text
- Use `<ins>` for success messages, `<del>` for error messages (with `style="display:block;text-decoration:none"`)
- Use `<ins>` for success messages, `<del>` for error messages, using the standardized inline style `style="display:block;text-decoration:none"` when rendering them as block-level alerts
- Use `<s>` for strikethrough text (e.g., completed todos), `<del>` for removed/error content
- Use `<mark>` for highlighted/badge text
- Use `<progress>` for progress bars
- Use `<hgroup>` for title + subtitle groupings
- Use `<fieldset role="group">` for inline input+button groups
- Use `<blockquote>` for callout/info boxes
- Do NOT write inline `style` attributes, except for the standardized `<ins>`/`<del>` block-level pattern above. Use Pico semantic elements instead (e.g., `<s>` not `style="text-decoration:line-through"`, `<nav>` not `style="display:flex"`, `hidden` not `style="display:none"`)
- Do NOT write custom CSS. If Pico cannot express a style, ask before adding custom CSS.
- Pico CSS variables (`--pico-*`) may be used for theming when semantic markup is insufficient
275 changes: 63 additions & 212 deletions todos/README.md
Original file line number Diff line number Diff line change
@@ -1,261 +1,112 @@
# LiveTemplate Todo App Example
# LiveTemplate Todo App

A real-time todo application demonstrating LiveTemplate's reactive state management with [Pico CSS](https://picocss.com/) for semantic, class-less styling.
A real-time todo application demonstrating LiveTemplate's controller pattern with SQLite persistence, basic authentication, search, sorting, and pagination. Styled with [Pico CSS](https://picocss.com/).

## Features

- **Add todos** - Create new tasks via form submission
- **Basic authentication** - Per-user todo lists (alice/password, bob/password)
- **Add todos** - Create new tasks via form submission with validation
- **Toggle completion** - Mark tasks as done/undone with checkboxes
- **Delete todos** - Remove individual tasks
- **Clear completed** - Bulk remove all completed tasks
- **Search** - Filter todos by text
- **Sort** - Newest first, oldest first, alphabetical (A-Z / Z-A)
- **Pagination** - 3 items per page with navigation controls
- **Live statistics** - Real-time total, completed, and remaining counts
- **Reactive updates** - Changes automatically broadcast to all connected clients
- **Semantic CSS** - Beautiful UI using Pico CSS without custom classes
- **Transport-agnostic** - Works over WebSocket or plain HTTP/AJAX
- **Reactive updates** - Changes broadcast to all connected clients
- **SQLite persistence** - Todos survive server restarts

## Running the Example
## Quick Start

1. **Start the server:**

From project root:
```bash
go run examples/todos/main.go
```

Or from the todos directory:
```bash
cd examples/todos
go run main.go
```
```bash
cd todos
go run .
```

With custom port:
```bash
PORT=8081 go run main.go
```
Open <http://localhost:8080> and log in with `alice` / `password`.

2. **Open your browser:**
Navigate to `http://localhost:8080`
With a custom port:

3. **Interact with todos:**
- Type a task in the input field and click "Add"
- Check/uncheck boxes to toggle completion
- Click "Delete" to remove a task
- Click "Clear Completed" to remove all done tasks
- Watch statistics update in real-time
```bash
PORT=8081 go run .
```

## How It Works

### Server Side (Go)
### Controller Pattern

Simple reactive API with array state management:
The app uses LiveTemplate's controller pattern where each action maps to a typed method:

```go
type TodoItem struct {
ID string
Text string
Completed bool
}

type TodoState struct {
Title string
Todos []TodoItem
TotalCount int
CompletedCount int
RemainingCount int
type TodoController struct {
Queries *db.Queries
}

// Implement the Store interface
func (s *TodoState) Change(action string, data map[string]interface{}) {
switch action {
case "add":
text := livetemplate.GetString(data, "text")
s.Todos = append(s.Todos, TodoItem{
ID: fmt.Sprintf("todo-%d", time.Now().UnixNano()),
Text: text,
})
case "toggle":
id := livetemplate.GetString(data, "id")
// Find and toggle todo
case "delete":
id := livetemplate.GetString(data, "id")
// Remove todo from slice
case "clear_completed":
// Filter out completed todos
func (c *TodoController) Add(state TodoState, ctx *livetemplate.Context) (TodoState, error) {
var input AddInput
if err := ctx.BindAndValidate(&input, validate); err != nil {
return state, err
}

s.updateStats()
// Create todo in database, reload list
return c.loadTodos(dbCtx, state, ctx.UserID())
}

func main() {
state := &TodoState{Title: "Todo App", Todos: []TodoItem{}}

// Auto-discovers todos.tmpl in current directory
tmpl := livetemplate.New("todos")

// Handle() auto-configures: WebSocket, HTTP, state cloning, updates
http.Handle("/", tmpl.Handle(state))
http.ListenAndServe(":8080", nil)
}
func (c *TodoController) Toggle(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
func (c *TodoController) Delete(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
func (c *TodoController) ClearCompleted(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
func (c *TodoController) Search(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
func (c *TodoController) Sort(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
func (c *TodoController) NextPage(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
func (c *TodoController) PrevPage(state TodoState, ctx *livetemplate.Context) (TodoState, error) { ... }
```

**Key concepts:**
- **Array State Management**: Todos stored as slice, automatically tracked
- **Auto-discovery**: Automatically finds and parses `todos.tmpl`
- **Auto Updates**: Handle() automatically generates and sends updates after Change()
- **Auto Cloning**: Each WebSocket connection gets its own state copy
- **Session Management**: HTTP connections get session-based persistence

### Client Side (Pico CSS + LiveTemplate)

**Zero JavaScript needed** - Pico CSS + LiveTemplate handles everything:
Actions are routed from HTML via form `name` and button `name` attributes (Tier 1 pattern):

```html
<!-- Pico CSS via CDN - no custom classes needed! -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">

<main class="container">
<article>
<!-- Add Todo Form -->
<form lvt-submit="add">
<fieldset role="group">
<input type="text" name="text" placeholder="What needs to be done?" required>
<button type="submit">Add</button>
</fieldset>
</form>

<!-- Statistics Table -->
<table>
<tbody>
<tr><td>Total</td><td><strong>{{.TotalCount}}</strong></td></tr>
<tr><td>Completed</td><td><strong>{{.CompletedCount}}</strong></td></tr>
<tr><td>Remaining</td><td><strong>{{.RemainingCount}}</strong></td></tr>
</tbody>
</table>

<!-- Todo List -->
{{range .Todos}}
<tr data-key="{{.ID}}">
<td>
<input type="checkbox" {{if .Completed}}checked{{end}}
lvt-change="toggle" lvt-data-id="{{.ID}}">
</td>
<td>{{.Text}}</td>
<td>
<button lvt-click="delete" lvt-data-id="{{.ID}}">Delete</button>
</td>
</tr>
{{end}}
</article>
</main>

<!-- Auto-initializing client library -->
<script src="livetemplate-client.js"></script>
<!-- Form name="add" routes to Add() method -->
<form method="POST" name="add">
<input type="text" name="text" placeholder="What needs to be done?" required />
<button type="submit" name="add">Add</button>
</form>

<!-- Hidden input passes data; form name routes to Toggle() -->
<form method="POST" name="toggle">
<input type="hidden" name="id" value="{{ .ID }}" />
<input type="checkbox" onchange="this.form.requestSubmit()" />
</form>

<!-- Button name routes to ClearCompleted() -->
<button name="clearCompleted">Clear Completed</button>
```

**Pico CSS features used:**
- `<main class="container">` - Responsive centered layout
- `<article>` - Card-style sections with automatic spacing
- `<form>` with `<fieldset role="group">` - Inline form controls
- `<table>` - Clean, styled tables
- `<button>` - Semantic button styling (primary, secondary variants)
- Semantic HTML elements automatically styled
### Authentication

**LiveTemplate features:**
- `lvt-submit` - Form submission with all field values
- `lvt-change` - Checkbox change events
- `lvt-click` - Button click events
- `lvt-data-*` - Custom data passed to actions
- `data-key` - Item tracking for range updates
Basic auth with hardcoded demo users. `ctx.UserID()` returns the authenticated username, used to isolate each user's todos in SQLite:

## Architecture

```
Browser WebSocket/HTTP Go Server
┌─────────────────┐ ┌──────────┐ ┌──────────────────┐
│ todos.tmpl │ │ │ │ TodoState │
│ (Pico CSS) │ │ │ │ Todos []Item │
│ │ │ │ │ Stats │
│ [Add] [Delete] │◄──────►│ / │◄─────────────►│ │
│ [✓] Checkboxes │ │ │ │ Change(action, │
│ │ │ │ │ data) │
│ LiveTemplate │ │ │ │ │
│ Client JS │ │ Auto- │ │ Handle() │
│ │ │ detects │ │ - Clones state │
│ lvt-* attrs │ │ transport│ │ - Generates │
│ - submit │ │ │ │ updates │
│ - click │ │ │ │ - Broadcasts │
│ - change │ │ │ │ │
└─────────────────┘ └──────────┘ └──────────────────┘
```

## Example Update Payloads

**Initial State (no todos):**
```json
{
"s": ["<main class=\"container\">...", "...</main>"],
"0": "Todo App",
"1": "No tasks yet. Add one above!"
}
```go
auth := livetemplate.NewBasicAuthenticator(func(username, password string) (bool, error) {
users := map[string]string{"alice": "password", "bob": "password"}
pass, ok := users[username]
return ok && pass == password, nil
})
```

**After Adding Todo (only changed values):**
```json
{
"1": "<table><tbody><tr data-key=\"todo-1234\">...",
"2": "1",
"3": "0",
"4": "1"
}
```
### Database

This demonstrates LiveTemplate's bandwidth efficiency - subsequent updates contain only changed dynamic values, not the full HTML.
SQLite via [sqlc](https://sqlc.dev/)-generated queries. The `db/` directory contains generated code from `queries.sql`. Schema migrations run automatically on startup, including detection and recreation of outdated schemas.

## Testing

### WebSocket Integration Test

```bash
go test -v -run TestWebSocketBasic
```

Tests:
- WebSocket connection establishment
- Add todo action
- Toggle completion action
- Response validation

### Browser E2E Test

```bash
go test -v -run TestTodosE2E
```

Tests:
- Initial page load with Pico CSS
- WebSocket connectivity
- LiveTemplate wrapper preservation
- Semantic HTML structure

Requires Docker for Chrome headless testing.

## Development Notes

- **Port**: Defaults to `:8080`, override with `PORT` environment variable
- **Endpoint**: `/` handles both WebSocket upgrades and HTTP POST requests
- **Template Path**: Reads from `examples/todos/todos.tmpl`
- **Client Library**: Serves via `internal/testing.ServeClientLibrary()` (dev only)
- **State Isolation**: Each WebSocket connection gets its own todo list
- **Session Management**: HTTP connections persist state via cookies
- **Pico CSS**: Loaded from CDN, no build step required

## Why Pico CSS?

- **Zero configuration** - Works with semantic HTML
- **No custom classes** - `<article>`, `<table>`, `<form>` are pre-styled
- **Responsive** - Mobile-friendly by default
- **Dark mode** - Automatic theme switching
- **Accessible** - Proper ARIA roles and keyboard navigation
- **Minimal** - ~10KB gzipped

Perfect for LiveTemplate demos where you want beautiful UIs without CSS complexity!
- **Database**: `todos.db` in the current directory (`:memory:` when `TEST_MODE=1`)
- **Client Library**: Served via `e2etest.ServeClientLibrary` in dev mode, CDN in production
Loading
Loading