Query Recipes
Filtered table
A server-paged DataTable with a search field and a team filter. The page resets on a filter change; the previous rows stay on screen while the next page loads.
Markdown version of this page, for assistants (Markdown for AI)
On this page
Example
Type a name, pick a team, page through the result.
The example runs in the browser.
Requests the server received
- None yet.
Code
<!-- src/routes/members/+page.svelte -->
<script lang="ts">
import { createQuery } from '@svelte-lean/query';
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, listTeams } from '$api/members';
import type { Member } from '$api/types';
const columns: Column<Member>[] = [
{ id: 'name', header: 'Name' },
{ id: 'email', header: 'Email' },
{ id: 'team', header: 'Team' },
{ id: 'status', header: 'Status' }
];
let view = $state(defaultState({ pageSize: 5 }));
let filters = $state({ search: '', team: '' });
const teams = createQuery(listTeams);
// The page lives in the table state; a filter change sends page 1.
const list = createPaged(
listMembers,
() => ({ search: filters.search || undefined, team: filters.team || undefined }),
tablePaging(() => view)
);
</script>
<form role="search" aria-label="Filter members" onsubmit={(event) => event.preventDefault()}>
<label>Search <input type="search" bind:value={filters.search} /></label>
<label>
Team
<select bind:value={filters.team}>
<option value="">All teams</option>
{#each teams.data ?? [] as team (team.id)}
<option value={team.name}>{team.name} ({team.members})</option>
{/each}
</select>
</label>
</form>
<DataTable bind:state={view} {...list.table} {columns} getRowId={(m) => m.id} label="Members" />src/lib/api/members.ts
export const listMembers = defineQuery({
get: 'api/members',
params: type<MemberFilters>(),
result: page<Member>(),
entity: 'Member', // every row carries tag('Member', row.id)
tags: ['Members'],
fresh: '15s',
access: 'members.view'
});How it works
| Moment | Request | Table |
|---|---|---|
| First render | GET api/members?page=1&perPage=5 and GET api/teams | skeleton rows (loading) |
| Next page | page=2, cause params | page 1 stays until page 2 answers |
| A keystroke in the search field | page=1 with the new search; the unanswered request is aborted | previous rows stay |
| A page past the end after a filter | clamped to the last page | the last page |
| Back to a page seen before | none while fresh (fresh: '15s' on the definition) | rows at once |
{...list.table}passesmanual,data,rowCount,pageCount,loading,fetchingandrefreshing; each prop follows its own field.tablePaging(() => view)keeps the page and the page size in the table state, so the table's pager drives the query.- The page params come from the transport profile (
page,perPagewithout one); the endpoint'sparamstype holds the filters only.
Variations
Another pager
TypeScript
// Without a DataTable: the list owns its page; render any pager.
const list = createPaged(listMembers, () => filters, { perPage: 25 });
// list.rows, list.page, list.lastPage, list.setPage(n)Polling and the next page
TypeScript
// Poll the shown page every 30 s while the tab is visible; prefetch the next page.
const polled = createPaged(listMembers, () => filters, {
poll: () => '30s',
prefetch: 'next'
});Related
- Lists:
createPagedandcreateInfinite. - Server data in the Table documentation.
- Search button: search on submit instead.