AkurAI Build
Menu

Public repository / f1f33c69567a554b6ce011822661d3289c1e9c77

AkurAI-Framework / site/content/docs/boundaries.md

Latest change 4c854a168e94887cd3c9ba5ec2150c94c7bda235 - feat(batch8-c): add loading and error boundaries for SPA nav and async data by Ólafur Búi Ólafsson

# Loading & Error Boundaries

> Graceful UI for SPA navigation and async data fetching. No dependencies, no build step.

## Overview

The boundaries module provides two layers of protection for your SPA:

1. **Navigation boundary** — A top-of-page progress bar that shows when the SPA router is navigating between pages.
2. **Data boundary** — A `withBoundary()` helper that wraps async data fetches in graceful loading and error UI, with built-in Retry.

Both are **progressive enhancements**: if JavaScript is disabled or the script fails, navigation still works (via full-page reloads) and data fetches silently fail (no broken UI).

## Installation

Add `boundaries.js` to your page, ideally before the `app.js` loader:

```html
<script type="module" src="/boundaries.js"></script>
```

The module initializes itself on load and exports the `SpaNavBoundary` global with two APIs:
- `SpaNavBoundary.withBoundary(el, asyncFn, opts)` — Wrap an async data fetch
- `SpaNavBoundary.startNavProgress()` — Manually trigger the progress bar
- `SpaNavBoundary.endNavProgress()` — Manually end it

## Navigation Boundary

The SPA router (`spa.js`) intercepts same-origin link clicks and fetches pages asynchronously. The boundaries module adds a **progress bar** that animates from left to right during the fetch.

### How it works

1. User clicks a link
2. Boundaries detects the click (via capturing listener) and shows the progress bar
3. `spa.js` fetches the page
4. SPA router emits `spa:navigated` event
5. Boundaries catches `spa:navigated` and hides the progress bar

### Styling

The progress bar is injected at the top of the page with these inline styles:
- Position: fixed, top, height 3px
- Gradient from `--accent` to `--accent-2`
- Opacity 0 by default; fades in when navigating

You can override it with CSS:

```css
.spa-nav-progress {
  /* your overrides */
  background: #your-color !important;
  height: 4px !important;
}
```

### Manual control

If you want to trigger the progress bar without SPA navigation:

```js
SpaNavBoundary.startNavProgress();
// ... do something async
SpaNavBoundary.endNavProgress();
```

## Data Boundary

The `withBoundary()` helper wraps async functions (typically `fetch()` calls) in three states:

1. **Loading** — Shows a spinner and "Loading…" message
2. **Success** — Renders the returned content
3. **Error** — Shows an error banner with a Retry button

### Basic usage

```js
const el = document.getElementById("data-container");

const fetchData = async () => {
  const res = await fetch("/api/data");
  if (!res.ok) throw new Error(`status ${res.status}`);
  return res.text();  // HTML string to render
};

SpaNavBoundary.withBoundary(el, fetchData, {
  loading: true,  // show spinner while fetching
  error: true     // show error banner on failure
});
```

### Return types

The async function can return:
- **HTML string** — `res.text()` or `"<div>content</div>"`
- **HTMLElement** — A DOM node created with `document.createElement()`
- **Any other value** — Will be coerced to string and rendered

```js
// Return HTML string
SpaNavBoundary.withBoundary(el, async () => {
  const res = await fetch("/api/html");
  return res.text();
});

// Return DOM element
SpaNavBoundary.withBoundary(el, async () => {
  const div = document.createElement("div");
  div.className = "card";
  div.textContent = "Loaded!";
  return div;
});
```

### Options

The third argument controls loading/error UI:

```js
{
  loading: true,   // (default) show a spinner while fetching
  error: true      // (default) show an error banner with Retry
}
```

Set either to `false` to hide that state:

```js
// No loading spinner, just fail silently on error
SpaNavBoundary.withBoundary(el, asyncFn, {
  loading: false,
  error: false
});
```

### Error handling

When the async function rejects:

1. First retry: shows error banner + Retry button
2. Second retry: shows error without button (to prevent infinite loops)

The error message is extracted from the caught exception:

```js
// Custom error message
throw new Error("Failed to load user data");
// → displays in the banner as "Failed to load user data"
```

### Retry behavior

The Retry button re-runs the entire boundary:

```js
SpaNavBoundary.withBoundary(el, asyncFn, {
  loading: true,
  error: true
});
// User clicks Retry → calls withBoundary again
```

This clears both the error state and the `isRetry` flag, allowing fresh attempts.

## Custom Loading States

By default, `withBoundary()` shows a centered spinner. To use custom loading UI (e.g., skeleton screens), pre-populate the container before calling the boundary:

```js
// Custom skeleton loading
const el = document.getElementById("content");
el.innerHTML = `
  <div class="skeleton skeleton-card"></div>
  <div class="skeleton-line"></div>
  <div class="skeleton-line skeleton-line-sm"></div>
`;

// Disable the default spinner
SpaNavBoundary.withBoundary(el, asyncFn, {
  loading: false,  // don't show default spinner
  error: true      // but still show error UI
});
```

The boundary will replace your skeleton with real content once the fetch completes.

## Accessibility

Boundaries integrate `aria-busy` and semantic roles for screen readers:

- **Loading** — `aria-busy="true"` on the container
- **Error** — `role="alert"` on the banner (via CSS `.banner`)
- **Status** — `role="status"` on the loading message

## Console logging

All boundaries emit `[boundaries]` breadcrumb logs:

```
[boundaries] initialized (SPA nav boundaries + withBoundary helper)
[boundaries] navigation start
[boundaries] showing loading state
[boundaries] content rendered
[boundaries] navigation end
[boundaries] fetch error: Network error
[boundaries] showing error UI: Network error
```

Open DevTools → Console to debug loading/error states in real time.

## Progressive enhancement

The entire boundaries module is a progressive enhancement:

- **Without JavaScript**: links still work (full-page navigation), form submissions work
- **With JavaScript disabled**: no spinners, no error banners, no fancy UX
- **If boundaries.js fails to load**: `spa.js` still works, but you lose the progress bar and error UI

This is why `withBoundary()` doesn't throw on init — the page keeps functioning even if the boundary is absent.

## Demo

Visit [/boundaries-demo](/boundaries-demo) to see loading boundaries and error handling in action.

## Performance

- **Lazy init** — The module only adds listeners when `document.readyState` is "interactive" or later
- **Minimal overhead** — One progress bar DOM element, one listener per navigation, one per async boundary
- **No dependencies** — Pure vanilla JS; zero third-party code

## Browser support

Requires:
- `fetch()` API
- `CustomEvent` (for `spa:navigated` listening)
- `document.readyState` (for init guarding)

Gracefully degrades on very old browsers (IE9 and earlier).