Load More

The button sends the current $offset; the endpoint answers with three SSE events: patchElements (mode append) adds the next batch to #entries, patchSignals advances the offset, and — once everything is loaded — removeElements('#load-more-button') takes the button away. ?offset= deep-links the already-loaded state.

Walks (23)

  • Canal towpath loopA 3 km route, roughly 45 minutes at an easy pace.
  • Castle mound circuitA 10 km route, roughly 58 minutes at an easy pace.
  • Old orchard trailA 8 km route, roughly 71 minutes at an easy pace.
  • River meadow walkA 6 km route, roughly 84 minutes at an easy pace.
  • Ironstone ridge pathA 4 km route, roughly 97 minutes at an easy pace.

The demo

src/components/demos/LoadMoreDemo.astro
---
/** Load-more demo. Deep link: ?offset=… server-renders what's loaded. */
import EntryBatch from '../EntryBatch.astro';
import { entries } from '../../data/entries';

const LIMIT = 5;
const raw = Number(Astro.url.searchParams.get('offset')) || LIMIT;
const offset = Math.min(
  Math.max(LIMIT, Math.ceil(raw / LIMIT) * LIMIT),
  Math.ceil(entries.length / LIMIT) * LIMIT,
);
const done = offset >= entries.length;
---

<section
  class="demo"
  data-signals={`{offset: ${offset}}`}
  data-replace-url={`'/examples/load-more' + ($offset > ${LIMIT} ? '?offset=' + $offset : '')`}
>
  <h2>Walks ({entries.length})</h2>
  <ul id="entries" class="entry-list">
    <EntryBatch offset={0} limit={offset} />
  </ul>
  {
    !done && (
      <div id="load-more-button">
        <button
          data-on:click="@get('/api/load-more')"
          data-indicator="_loading"
          data-attr:disabled="$_loading"
        >
          Load more
        </button>
      </div>
    )
  }
</section>

The shared component

src/components/EntryBatch.astro
---
/** One batch of entries; /api/load-more appends later batches to #entries. */
import { entries } from '../data/entries';

interface Props {
  offset: number;
  limit?: number;
}

const { offset, limit = 5 } = Astro.props;
const items = entries.slice(offset, offset + limit);
---

{
  items.map((entry) => (
    <li>
      <strong>{entry.title}</strong>
      <span class="hint">{entry.blurb}</span>
    </li>
  ))
}

The endpoint

src/pages/api/load-more.ts
import {
  patchElements,
  patchSignals,
  readSignals,
  removeElements,
  SignalsValidationError,
  sse,
} from '@wrux/astro-datastar/server';
import type { APIRoute } from 'astro';
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { z } from 'astro/zod';
import EntryBatch from '../../components/EntryBatch.astro';
import { entries } from '../../data/entries';

export const prerender = false;

const LIMIT = 5;

const signals = z.object({
  offset: z.number().int().min(0).catch(0),
});

export const GET: APIRoute = async ({ request }) => {
  let offset: number;
  try {
    ({ offset } = await readSignals(request, signals));
  } catch (err) {
    if (err instanceof SignalsValidationError) return err.response();
    throw err;
  }

  const container = await AstroContainer.create();
  const batch = await container.renderToString(EntryBatch, {
    props: { offset, limit: LIMIT },
  });

  const nextOffset = offset + LIMIT;
  const events = [
    patchElements(batch, { selector: '#entries', mode: 'append' }),
    patchSignals({ offset: nextOffset }),
  ];
  if (nextOffset >= entries.length) {
    events.push(removeElements('#load-more-button'));
  }
  return sse(...events);
};