sveltelean Query
Version v0.2.0

Paged table

createPaged takes the endpoint and the filters; list.table is the set of props a <DataTable manual> needs.

src/routes/members/+page.svelte
<!-- src/routes/members/+page.svelte -->
<script lang="ts">
	import { createPaged, tablePaging } from '@svelte-lean/query/paged';
	import { DataTable, defaultState, type Column } from '@svelte-lean/table';
	import '@svelte-lean/table/style.css';
	import { listMembers } from '$api/members';
	import type { Member } from '$api/types';

	const columns: Column<Member>[] = [
		{ id: 'name', header: 'Name', accessor: 'name' },
		{ id: 'email', header: 'Email', accessor: 'email' },
		{ id: 'team', header: 'Team', accessor: 'team' }
	];

	let view = $state(defaultState({ pageSize: 25 }));
	let team = $state('');

	// The page lives in the table state; a filter change resets it to 1.
	const list = createPaged(listMembers, () => ({ team: team || undefined }), {
		...tablePaging(() => view),
		poll: () => '30s'
	});
</script>

<label>
	Team
	<select bind:value={team}>
		<option value="">All</option>
		<option>Engineering</option>
		<option>Design</option>
	</select>
</label>

<DataTable
	bind:state={view}
	{...list.table}
	{columns}
	getRowId={(member) => member.id}
	label="Members"
/>
  • A filter change resets the page to 1 in the same update; no request for the old page.
  • A page past the end is clamped to the last page.
  • Rows stay on screen while the next page loads (loading dims them); a poll sets refreshing, a thin line, and writes no row that did not change.
  • Leaving a page before its answer aborts that request.

Table props

list.table is one object created with the list; each prop reads its own signal, so a poll that flips refreshing re-renders neither the rows nor the pager.

PropValue
manualtrue: the server pages, sorts and filters
datathe rows of the current page
rowCountthe total, or undefined when the server sent a bare list
pageCountthe last page
loading, fetchinga fetch the user waits for
refreshinga quiet background fetch

Page state in the table

state
let view = $state(defaultState({ pageSize: 25 }));
let team = $state('');

// The page lives in the table state; a filter change resets it to 1.
const list = createPaged(listMembers, () => ({ team: team || undefined }), {
	...tablePaging(() => view),
	poll: () => '30s'
});

tablePaging(() => view) keeps the page and the page size in the table state (0-based there, 1-based here); bind:state replacing the object is supported.

List with a pager

members.svelte
<!-- A paged list without DataTable: page state and a native pager -->
<script lang="ts">
	import { createPaged } from '@svelte-lean/query/paged';
	import { listMembers } from '$api/members';

	let search = $state('');
	const list = createPaged(listMembers, () => ({ search }), {
		perPage: 20,
		prefetch: 'next' // the next page is requested once this one has settled
	});
</script>

<input type="search" aria-label="Search members" bind:value={search} />
<ul aria-busy={list.loading}>
	{#each list.rows as member (member.id)}<li>{member.name}</li>{/each}
</ul>
<nav aria-label="Pages">
	<button disabled={list.page <= 1} onclick={() => list.setPage(list.page - 1)}>Previous</button>
	<span>Page {list.page} of {list.lastPage}</span>
	<button disabled={list.page >= list.lastPage} onclick={() => list.setPage(list.page + 1)}
		>Next</button
	>
</nav>

Options

OptionTypeDefaultEffect
pagenumber | Controlled<number>1The page, 1-based; a Controlled value lives elsewhere (table state, URL).
perPagenumber | Controlled<number>20Rows per page; changing it resets the page to 1.
keepPreviousbooleantrueThe previous page stays on screen while the next one loads.
pollPollOption | (() => PollOption)–Polls the current page only.
whenboolean | (() => boolean)–Pauses fetching and polling; keeps the rows.
prefetch'next' | 'neighbours' | falsefalseRequests the adjacent pages after an answer has settled.
freshDurationthe endpoint’sPer-use freshness.

Page params come from the transport profile ({ page, perPage } without one, page and per_page with laravel()); keep them out of the filters. An endpoint that names or sends them differently declares pageParams: (page, perPage) => object: (page) => ({ page }) when the server sizes its pages, () => ({}) for a list it does not page.

Load more

src/routes/audit/+page.svelte
<!-- src/routes/audit/+page.svelte -->
<script lang="ts">
	import { createInfinite } from '@svelte-lean/query/infinite';
	import { listAuditEvents } from '$api/audit';

	let actor = $state('');
	const feed = createInfinite(listAuditEvents, () => ({ actor: actor || undefined }), {
		perPage: 50,
		poll: () => '15s' // the head page only
	});
</script>

<input type="search" aria-label="Filter by actor" bind:value={actor} />
{#if feed.loading}<p>Loading events…</p>{/if}
<ol>
	{#each feed.rows as event (event.id)}
		<li><time datetime={event.at}>{event.at}</time> {event.actor} {event.action}</li>
	{/each}
</ol>
{#if feed.moreError}<p role="alert">The next page could not be loaded.</p>{/if}
{#if feed.hasMore}
	<button onclick={() => feed.loadMore()} disabled={feed.loadingMore}>
		{feed.loadingMore ? 'Loading…' : 'Load more'}
	</button>
{/if}
  • Each page is an ordinary entry: it fails, refetches and is evicted on its own.
  • Only the head page polls; a failed deeper page sets moreError and keeps the rows above it.
  • A row id seen on an earlier page is kept at its first position.
  • refetch() refetches every walked page in parallel.
  • loading, refreshing, busy, blocked and info read like those of a query (of the head page).

Options

OptionTypeDefaultEffect
perPagenumber20Rows per page.
pollPollOption | (() => PollOption)–Polls the head page only.
shift'refetch' | 'carry''refetch'What a head refresh with new rows does to the deeper pages.
maxPagesnumbernoneA hard cap on the walk.
keepPreviousbooleanfalseKeep the rows of the previous params while the new walk starts.
whenboolean | (() => boolean)–Pauses the walk and polling; keeps the rows.

The endpoint answers page<T>(), like a paged one:

src/lib/api/audit.ts
// src/lib/api/audit.ts — the workspace audit log, read page by page.
import { defineQuery, page, type } from '@svelte-lean/query';
import type { AuditEvent } from './types';

declare module '@svelte-lean/query' {
	interface Tags {
		AuditEvents: void;
	}
}

export const listAuditEvents = defineQuery({
	get: 'api/audit',
	params: type<{ actor?: string }>(),
	result: page<AuditEvent>(),
	tags: ['AuditEvents']
});

Reference