Skip to main content
Examples

Examples

These examples combine the earlier topics into complete, small interactions. Each includes loading, success, and failure handling.

1. Live search with debounce

const input = document.querySelector('#query');
const results = document.querySelector('#results');

let timer;
let controller;

input.addEventListener('input', () => {
  clearTimeout(timer);
  controller?.abort();

  timer = setTimeout(async () => {
    controller = new AbortController();
    results.setAttribute('aria-busy', 'true');

    try {
      const response = await fetch(
        `/search?q=${encodeURIComponent(input.value)}`,
        { signal: controller.signal }
      );

      if (!response.ok) throw new Error(`HTTP ${response.status}`);

      const items = await response.json();
      results.replaceChildren();

      for (const item of items) {
        const li = document.createElement('li');
        li.textContent = item.title;
        results.append(li);
      }
    } catch (error) {
      if (error.name !== 'AbortError') {
        results.textContent = 'Search is unavailable right now.';
      }
    } finally {
      results.setAttribute('aria-busy', 'false');
    }
  }, 250);
});

2. Load more button

const button = document.querySelector('#load-more');
const list = document.querySelector('#list');
let nextPage = 2;

button.addEventListener('click', async () => {
  button.disabled = true;

  try {
    const response = await fetch(`/items?page=${nextPage}`);
    if (!response.ok) throw new Error('Could not load more items');

    list.insertAdjacentHTML('beforeend', await response.text());
    nextPage++;
  } catch (error) {
    showError(error.message);
  } finally {
    button.disabled = false;
  }
});

3. Delete with confirmation

async function remove(id, row) {
  if (!confirm('Delete this item?')) return;

  const response = await fetch(`/items/${id}`, {
    method: 'DELETE',
    headers: { 'X-CSRF-TOKEN': csrfToken() },
  });

  if (response.status === 204 || response.ok) {
    row.remove();
  } else {
    showError('Delete failed. Please try again.');
  }
}

4. Polling with visibility awareness

async function refresh() {
  if (document.hidden) return;

  const response = await fetch('/api/status');
  if (response.ok) render(await response.json());
}

setInterval(refresh, 30000);
document.addEventListener('visibilitychange', refresh);

5. Submit a form with feedback

form.addEventListener('submit', async (event) => {
  event.preventDefault();

  const button = form.querySelector('button');
  button.disabled = true;
  button.textContent = 'Saving...';

  try {
    const response = await fetch(form.action, {
      method: 'POST',
      body: new FormData(form),
      headers: { Accept: 'application/json' },
    });

    if (!response.ok) throw new Error('Save failed');

    form.reset();
    flash('Saved successfully.');
  } catch (error) {
    flash(error.message, true);
  } finally {
    button.disabled = false;
    button.textContent = 'Save';
  }
});

Each example uses the same structure: prevent the default action, show a busy state, check the status, update the DOM, and restore the interface. Reuse that structure instead of inventing a new flow for every endpoint.