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
69 changes: 69 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# LiveTemplate Examples

## Progressive Complexity

All examples follow the **progressive complexity** model introduced in livetemplate v0.8.7:

- **Tier 1 (Standard HTML)** is the default. Use native HTML forms, buttons, and inputs.
- **Tier 2 (`lvt-*` attributes)** only when standard HTML cannot express the behavior.

### Tier 1 Constructs

| Construct | Pattern | Routes to |
|-----------|---------|-----------|
| Form submission | `<form method="POST" name="add">` | `Add()` method |
| Button action | `<button name="save">` | `Save()` method |
| Hidden data | `<input type="hidden" name="id" value="...">` | `ctx.GetString("id")` |
| Auto-submit | `<form method="POST">` (no name) | `Submit()` method |
| Live updates | Controller with `Change()` method | Auto-wired 300ms debounce |
| Validation | `required`, `minlength`, `pattern` | `ctx.ValidateForm()` |

### Tier 2 Constructs (use sparingly)

| Attribute | Purpose | Example |
|-----------|---------|---------|
| `lvt-scroll` | Auto-scroll behavior | Chat message container |
| `lvt-upload` | Chunked file uploads | Avatar upload |
| `lvt-debounce` | Custom timing control | Search with custom delay |
| `lvt-keydown` | Keyboard shortcuts | Global key bindings |
| `lvt-animate` | Entry/exit animations | Toast notifications |

### Action Resolution Order

When a form is submitted, the framework resolves the action in this order:
1. `lvt-submit` attribute on the form
2. Clicked button's `name` attribute
3. Form's `name` attribute
4. Default: `"submit"`

## Creating New Examples

### Checklist

1. Start with Tier 1 — use standard HTML forms and button names
2. Only add `lvt-*` attributes when standard HTML can't express the interaction
3. Add `method="POST"` to forms and `name` attributes for action routing
4. Use form `name` for both client-side (WebSocket/fetch) and server-side (HTTP POST) action routing
5. Use button `name` for HTTP POST fallback (browser includes button name in form data on click)
6. Add hidden inputs for data passing: `<input type="hidden" name="id" value="{{.ID}}">`

### Testing

- All examples must have chromedp E2E tests
- Use `e2etest.StartDockerChrome()` for browser testing
- For WebSocket CRUD verification, use `window.liveTemplateClient.send({action: '...', data: {...}})` directly
- HTTP POST tests should use button name encoding: `"add=&field=value"`
- Run `./test-all.sh` to verify all examples pass

### Dependencies

- livetemplate: v0.8.7+
- lvt (testing): latest pseudo-version
- Client library: served via `e2etest.ServeClientLibrary` (dev) or CDN (production)

### Reference Examples

- `todos-progressive/` — Canonical Tier 1 example (zero `lvt-*` attributes)
- `profile-progressive/` — Simple Tier 1 form with validation
- `live-preview/` — Tier 1 with `Change()` method for live updates
- `chat/` — Tier 1+2 (uses `lvt-scroll` for auto-scroll)
158 changes: 27 additions & 131 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,136 +2,32 @@

Example applications demonstrating LiveTemplate usage with various features and patterns.

## Examples

### 1. Counter - Simple State Management
**Directory**: `counter/`

Basic counter demonstrating reactive state updates.

```bash
cd counter
go run main.go
# Visit http://localhost:8080
```

**Features**:
- Simple state management
- Button click handling
- Real-time updates

---

### 2. Chat - Real-time Communication
**Directory**: `chat/`

Multi-user chat application with WebSocket communication.

```bash
cd chat
go run main.go
# Visit http://localhost:8080
```

**Features**:
- Multi-user chat rooms
- WebSocket messaging
- User presence
- Message history

---

### 3. Todos - Full CRUD Application
**Directory**: `todos/`

Complete todo list with database, validation, and full CRUD operations.

```bash
cd todos
go run main.go
# Visit http://localhost:8080
```

**Features**:
- SQLite database
- CRUD operations
- Form validation
- Database migrations
- E2E tests with Chromedp

---

### 4. Graceful Shutdown
**Directory**: `graceful-shutdown/`

Demonstrates proper server shutdown handling.

```bash
cd graceful-shutdown
go run main.go
# Press Ctrl+C to trigger graceful shutdown
```

**Features**:
- Signal handling
- Connection draining
- Cleanup procedures

---

### 5. Observability
**Directory**: `observability/`

Logging, metrics, and tracing example.

```bash
cd observability
go run main.go
```

**Features**:
- Structured logging
- Custom metrics
- Request tracing
- Performance monitoring

---

### 6. Testing
**Directory**: `testing/01_basic/`
## Progressive Complexity

All examples follow the [progressive complexity](https://github.com/livetemplate/livetemplate/blob/main/docs/guides/progressive-complexity.md) model. Tier 1 (standard HTML) is preferred; Tier 2 (`lvt-*` attributes) is used only when necessary.

| Example | Tier | Description | Tier 2 Attributes |
|---------|------|-------------|--------------------|
| `counter/` | 1 | Simple state management | None |
| `chat/` | 1+2 | Real-time multi-user chat | `lvt-scroll` |
| `todos/` | 1 | Full CRUD with SQLite | None |
| `todos-progressive/` | 1 | Zero-attribute CRUD demo | None |
| `todos-components/` | 1+2 | Component library (modal, toast) | Component-internal |
| `flash-messages/` | 1 | Flash notification patterns | None |
| `avatar-upload/` | 1+2 | File upload with progress | `lvt-upload` |
| `graceful-shutdown/` | 1 | Server shutdown patterns | None |
| `observability/` | 1 | Logging, metrics, tracing | None |
| `progressive-enhancement/` | 1 | Works with/without JS | None |
| `profile-progressive/` | 1 | Form validation | None |
| `ws-disabled/` | 1 | HTTP-only mode | None |
| `live-preview/` | 1 | Change() live updates | None |
| `login/` | 1 | Authentication + sessions | None |
| `testing/01_basic/` | 1 | E2E test patterns | None |
| `production/single-host/` | 1 | Production deployment | None |

E2E testing patterns with Chromedp.

```bash
cd testing/01_basic
go test -v
```

**Features**:
- Browser automation
- E2E test patterns
- Test helpers
- Assertions

---

### 7. Production
**Directory**: `production/single-host/`

Production deployment configuration.

```bash
cd production/single-host
go run main.go
```

**Features**:
- Production server setup
- Environment configuration
- Health checks
- Deployment best practices
## Examples

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

## Examples is currently an empty section heading (it immediately jumps to ## Running Examples). Either remove the empty heading or add content under it so the README doesn't have a dead section.

Suggested change
The directories listed in the table above are individual example applications. Each folder contains a minimal, self-contained project that demonstrates a specific LiveTemplate pattern or feature.

Copilot uses AI. Check for mistakes.
---
The directories listed in the table above are individual example applications. Each folder contains a minimal, self-contained project that demonstrates a specific LiveTemplate pattern or feature.

## Running Examples

Expand Down Expand Up @@ -187,9 +83,9 @@ For local development, examples can serve the client library locally using `gith

## Dependencies

- **Core Library**: `github.com/livetemplate/livetemplate v0.1.0`
- **LVT Testing** (for examples with E2E tests): `github.com/livetemplate/lvt v0.1.0`
- **Client Library**: `@livetemplate/client@0.1.0` (via CDN)
- **Core Library**: `github.com/livetemplate/livetemplate v0.8.7`
- **LVT Testing** (for examples with E2E tests): `github.com/livetemplate/lvt` (latest)
- **Client Library**: `@livetemplate/client@latest` (via CDN)

## Related Projects

Expand Down
2 changes: 1 addition & 1 deletion avatar-upload/avatar-upload.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@
</div>
</div>

<form lvt-submit="UpdateProfile">
<form method="POST" name="updateProfile">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" value="{{.Name}}" required>
Expand Down
4 changes: 2 additions & 2 deletions chat/chat.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
</div>

{{if not .CurrentUser}}
<form lvt-submit="join">
<form method="POST" name="join">
<label>
Choose your username:
<input type="text" name="username" required autofocus>
Expand All @@ -101,7 +101,7 @@
{{end}}
</div>

<form class="input-form" lvt-submit="send">
<form class="input-form" method="POST" name="send">
<input type="text" name="message" placeholder="Type your message..." autocomplete="off" autofocus>
<button type="submit">Send</button>
</form>
Expand Down
16 changes: 8 additions & 8 deletions chat/chat_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,17 @@ func TestChatE2E(t *testing.T) {
err := chromedp.Run(browserCtx,
// Capture initial state
chromedp.Text(".stats", &initialStatsText, chromedp.ByQuery),
chromedp.Evaluate(`document.querySelector('form[lvt-submit="join"]') !== null`, &initialFormVisible),
chromedp.Evaluate(`document.querySelector('form[name="join"]') !== null`, &initialFormVisible),

// Fill and submit join form
chromedp.SetValue(`input[name="username"]`, "testuser", chromedp.ByQuery),
chromedp.Click(`button[type="submit"]`, chromedp.ByQuery),
chromedp.Evaluate(`document.querySelector('form[name="join"] button[type="submit"]').click()`, nil),
waitFor(`document.querySelector('.messages') !== null`, 5*time.Second),

// Capture after-join state
chromedp.Text(".stats", &afterStatsText, chromedp.ByQuery),
chromedp.Evaluate(`document.querySelector('.messages') !== null`, &afterChatVisible),
chromedp.Evaluate(`document.querySelector('form[lvt-submit="join"]') !== null`, &afterFormVisible),
chromedp.Evaluate(`document.querySelector('form[name="join"]') !== null`, &afterFormVisible),
)

if err != nil {
Expand Down Expand Up @@ -190,9 +190,9 @@ func TestChatE2E(t *testing.T) {
chromedp.Run(browserCtx,
chromedp.WaitVisible(`input[name="username"]`, chromedp.ByQuery),
chromedp.SetValue(`input[name="username"]`, "testuser", chromedp.ByQuery),
chromedp.Click(`button[type="submit"]`, chromedp.ByQuery),
chromedp.Evaluate(`document.querySelector('form[name="join"] button[type="submit"]').click()`, nil),
waitFor(`document.querySelector('.messages') !== null`, 5*time.Second),
chromedp.WaitVisible(`.messages`, chromedp.ByQuery), // Explicitly wait for messages container
chromedp.WaitVisible(`.messages`, chromedp.ByQuery),
)
t.Log("Join completed, .messages container is visible")
}
Expand All @@ -210,7 +210,7 @@ func TestChatE2E(t *testing.T) {
return nil
}),
chromedp.SetValue(`input[name="message"]`, "First message", chromedp.ByQuery),
chromedp.Click(`form[lvt-submit="send"] button[type="submit"]`, chromedp.ByQuery),
chromedp.Evaluate(`document.querySelector('form[name="send"] button[type="submit"]').click()`, nil),
waitFor(`document.querySelectorAll('.messages .message').length >= 1`, 5*time.Second),

chromedp.ActionFunc(func(ctx context.Context) error {
Expand All @@ -231,7 +231,7 @@ func TestChatE2E(t *testing.T) {
return nil
}),
chromedp.SetValue(`input[name="message"]`, "Second message", chromedp.ByQuery),
chromedp.Click(`form[lvt-submit="send"] button[type="submit"]`, chromedp.ByQuery),
chromedp.Evaluate(`document.querySelector('form[name="send"] button[type="submit"]').click()`, nil),
waitFor(`document.querySelectorAll('.messages .message').length >= 2`, 5*time.Second),

chromedp.ActionFunc(func(ctx context.Context) error {
Expand All @@ -252,7 +252,7 @@ func TestChatE2E(t *testing.T) {
return nil
}),
chromedp.SetValue(`input[name="message"]`, "Third message", chromedp.ByQuery),
chromedp.Click(`form[lvt-submit="send"] button[type="submit"]`, chromedp.ByQuery),
chromedp.Evaluate(`document.querySelector('form[name="send"] button[type="submit"]').click()`, nil),
waitFor(`document.querySelectorAll('.messages .message').length >= 3`, 5*time.Second),

chromedp.ActionFunc(func(ctx context.Context) error {
Expand Down
6 changes: 3 additions & 3 deletions counter/counter.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
<div>
<p>Counter: {{.Counter}}</p>
<div>
<button lvt-click="increment">+1</button>
<button lvt-click="decrement">-1</button>
<button lvt-click="reset">Reset</button>
<button name="increment">+1</button>
<button name="decrement">-1</button>
<button name="reset">Reset</button>
</div>
</div>
<footer>
Expand Down
13 changes: 8 additions & 5 deletions flash-messages/flash.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,12 @@

<!-- Add Item Form -->
<div class="form-group">
<form lvt-submit="add_item">
<form method="POST" name="addItem">
<input type="text"
name="item"
placeholder="Enter item name"
class="{{if .lvt.HasError "item"}}error{{end}}" />
<button type="submit">Add Item</button>
<button type="submit" name="addItem">Add Item</button>
{{if .lvt.HasError "item"}}
<div class="field-error">{{.lvt.Error "item"}}</div>
{{end}}
Expand All @@ -180,8 +180,8 @@

<!-- Action Buttons -->
<div class="form-group">
<button lvt-click="clear_items" class="btn-warning">Clear All</button>
<button lvt-click="simulate_error" class="btn-danger">Simulate Error</button>
<button name="clearItems" class="btn-warning">Clear All</button>
<button name="simulateError" class="btn-danger">Simulate Error</button>
</div>

<!-- Items List -->
Expand All @@ -191,7 +191,10 @@
{{range .Items}}
<div class="item">
<span class="item-name">{{.}}</span>
<button lvt-click="remove_item" lvt-data-item="{{.}}" class="remove-btn btn-secondary">Remove</button>
<form method="POST" name="removeItem" style="display:inline; margin:0;">
<input type="hidden" name="item" value="{{.}}">
<button type="submit" name="removeItem" class="remove-btn btn-secondary">Remove</button>
</form>
</div>
{{end}}
{{else}}
Expand Down
Loading
Loading