aRustyDev / aRustyDev/mdbook-htmx

docs(adr): ADR-0006: Out-of-Band Swaps for Coordinated Updates

Open
#14 0 comments 0 reactions 1 assignee Claimed by @aRustyDev View on GitHub
documentation
Dominant language
Rust
Stars
0
Forks
1
PR merge metrics
No merged PRs in 30d

Description

# ADR-0006: Out-of-Band Swaps for Coordinated Updates

## Status

Accepted

## Context

When navigating between documentation pages, multiple UI elements need to update simultaneously:

1. **Main content** - The new page content
2. **Sidebar** - Active chapter highlight
3. **Breadcrumbs** - Current path trail
4. **Table of Contents** - In-page headings

HTMX's default behavior swaps a single target. We need a strategy for coordinated updates.

## Decision Drivers

1. **Single Request** - Avoid multiple round-trips
2. **Atomicity** - All updates happen together
3. **Simplicity** - Minimize complex orchestration
4. **Performance** - Fast, predictable updates

## Options Considered

### Option A: Multiple Requests

Each component makes its own request:

```html

```

Sidebar and breadcrumb listen for events and fetch their own updates.

**Pros:**
- Components are independent
- Each can cache separately

**Cons:**
- 3 HTTP requests per navigation
- Race conditions
- Complex event coordination
- Inconsistent if one fails

### Option B: Client-Side JavaScript

Fetch content, then update multiple elements with JS:

```javascript
htmx.on('htmx:afterSwap', (event) => {
updateSidebar(event.detail.pathInfo.path);
updateBreadcrumb(event.detail.pathInfo.path);
});
```

**Pros:**
- Single request
- Full control

**Cons:**
- Requires custom JavaScript
- Against HTMX philosophy
- Sidebar state must be client-side

### Option C: Out-of-Band Swaps (Recommended)

Server response includes main content plus OOB elements:

```html

Chapter 2 content...


Chapter 1
Chapter 2

Home / Docs / Chapter 2

```

**Pros:**
- Single HTTP request
- Atomic updates (all or nothing)
- Pure HTML response
- No client-side logic
- HTMX-native pattern

**Cons:**
- Larger response size
- Server must render all components
- Repeated content if components unchanged

### Option D: hx-select-oob Attribute

Client specifies which OOB elements to extract:

```html

```

Server returns full page; HTMX selects elements.

**Pros:**
- Server returns simple full page
- Client controls what updates

**Cons:**
- Larger response (full page even for HTMX)
- Client-side parsing overhead

## Decision

**Use Out-of-Band Swaps (Option C) for coordinated updates.**

Rationale:
1. Single HTTP request = single network round-trip
2. Server controls exactly what updates
3. Pure HTML - no client-side rendering
4. HTMX-native, well-documented pattern
5. Progressive enhancement (full page works without JS)

## Implementation

### Fragment Response Structure

```html

Chapter 2: Configuration


Content here...


{% for chapter in navigation.chapters %}

{{ chapter.title }}

{% endfor %}

Home /
Docs /
{{ page.title }}

{% for heading in page.headings %}
{{ heading.text }}
{% endfor %}

```

### Server Rendering

```typescript
app.get('/docs/*', async (c) => {
const page = getPage(c.req.path);

if (isHtmxRequest(c.req)) {
// Return content + OOB components
return c.html(`
${await render('fragments/page.html', { page })}
${await render('partials/sidebar.html', {
chapters: navigation,
currentPath: page.path
})}
${await render('partials/breadcrumb.html', { page })}
${await render('partials/toc.html', { headings: page.headings })}
`);
}

// Full page for non-HTMX requests
return c.html(await render('layout.html', { page }));
});
```

### Build-Time Generation

The backend generates OOB partials for each page:

```
book/htmx/
├── fragments/
│ └── chapter-2.html # Content only
├── oob/
│ ├── chapter-2/
│ │ ├── sidebar.html # Sidebar with ch2 active
│ │ ├── breadcrumb.html # Breadcrumb for ch2
│ │ └── toc.html # TOC for ch2
```

Or server renders dynamically using templates.

## Consequences

### Positive
- Single request, multiple updates
- Atomic - all components update together
- Server controls render logic
- No client-side state management
- Works with CDN caching (if pre-generated)

### Negative
- Larger response size (~2-3KB extra per nav)
- Server renders redundant HTML if component unchanged
- OOB elements must have stable IDs

### Mitigation
- Consider `hx-swap-oob="outerHTML:#sidebar"` for conditional updates
- Lazy-load TOC only when sidebar visible
- Cache OOB partials at CDN edge

## Alternatives for Specific Cases

### Conditional OOB

Only update if changed (using HX-Trigger response header):

```typescript
// Server detects if breadcrumb unchanged
if (prevPath.startsWith(currentPath)) {
response.headers.set('HX-Trigger', 'breadcrumbUnchanged');
// Don't include breadcrumb OOB
}
```

### View Transitions

For smoother updates, combine with View Transitions API:

```html

::view-transition-old(sidebar) { animation: none; }
::view-transition-new(sidebar) { animation: none; }

```

## References

- [HTMX Out of Band Swaps](https://htmx.org/docs/#oob_swaps)
- [hx-swap-oob Attribute](https://htmx.org/attributes/hx-swap-oob/)
- [Hypermedia Patterns - Coordinated Updates](https://hypermedia.systems/)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.