# Filtered table

Page: https://svelte-lean.vercel.app/query/recipes/filtered-table · Markdown: https://svelte-lean.vercel.app/query/recipes/filtered-table.md · Index: https://svelte-lean.vercel.app/llms.txt

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.

## Example

Type a name, pick a team, page through the result.

```svelte
<!-- 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" />
```

The component shown, against an in-memory server with twenty-four members that answers after a fixed delay.

`src/lib/api/members.ts`

```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}` passes `manual`, `data`, `rowCount`, `pageCount`, `loading`, `fetching` and `refreshing`; 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`, `perPage` without one); the endpoint's `params` type holds the filters only.

## Variations

### Another pager

```ts
// 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

```ts
// 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](https://svelte-lean.vercel.app/query/lists.md): `createPaged` and `createInfinite`.
- [Server data](https://svelte-lean.vercel.app/table/server-data.md) in the Table documentation.
- [Search button](https://svelte-lean.vercel.app/query/recipes/search-button.md): search on submit instead.
