Skip to content

Load Pages

Kanban reads one offset/limit stream for the whole board. It does not have a separate cursor for every workflow column.

Start with append loading: display the first page, then append more cards as users scroll. Use indexed placeholders only when the server knows each unloaded card’s workflow placement and rank.

Interactive example · supplied HTTP demo backend

Load the next page of cards

The endpoint holds 24 cards and returns six per request.

Try it: Choose Load next page until all cards are loaded, then refresh.

Starting demo backend…

HTTP reads: 0 · writes: 0

What to notice: Each request advances the offset. Refresh starts at the first page again.

TypeScript source

The supplied backend handlers use MSW simulation in the browser. Replace the transport with your application endpoint.

remote-board.ts
import type { KanbanConfig, KanbanCardMoveDetail } from '@revolist/kanban';
import type { WorkCard } from './first-board';
import { boardConfig, configureBoard } from './first-board';
// apiUrl is YOUR endpoint. The documentation supplies an isolated HTTP demo backend.
export function remoteBoard(apiUrl: string): KanbanConfig<WorkCard> {
return { ...boardConfig, remote: {
chunkSize: 6,
query: { retry: false, refetchOnWindowFocus: false },
async loadData(skip, limit, _order, _single, _multi, _quick, _temporal, _ast, _execution, context) {
const response = await fetch(`${apiUrl}/cards?skip=${skip}&limit=${limit}`, { signal: context?.signal });
if (!response.ok) throw new Error(`Card loading failed: HTTP ${response.status}`);
return response.json(); // { data: Card[], total: number }
},
} };
}
// Read-only setup; add app-owned write handling before enabling moves.
export async function createServerBoard(container: HTMLElement, apiUrl: string) {
const grid = document.createElement('revo-grid');
grid.style.cssText = 'display:block;width:100%;height:360px';
container.append(grid);
try {
// configureBoard waits for grid initialization before activating the remote loader.
const kanban = await configureBoard(grid, { ...remoteBoard(apiUrl), readOnly: true }, []);
return { grid, kanban, destroy() { grid.remove(); } };
} catch (error) { grid.remove(); throw error; }
}
export async function saveMove(apiUrl: string, detail: KanbanCardMoveDetail, signal?: AbortSignal) {
const response = await fetch(`${apiUrl}/moves`, {
method: 'POST', signal, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ changedCards: detail.changedCards }),
});
if (!response.ok) throw new Error(`Move save could not be confirmed: HTTP ${response.status}`);
return response.json();
}
// grid.kanban = remoteBoard(apiUrl);
// grid.addEventListener('kanbancardmove', event => { void saveMove(apiUrl, event.detail); });
// In your app, pause conflicting moves while saving; on failure refresh before more edits.
// Pagination, refresh and Query policy belong to remote. There is no remote.commit method.
Download remote-board.ts

Try it: choose Load next page and compare loaded cards with the known server total. Viewport preloading may request another page automatically.

Use kanban.remote when the canonical collection should arrive from one whole-board offset/limit stream:

All existing loadData(skip, limit) callbacks use the built-in query engine. Add optional remote.query for native polling, refetch, retry and shared-source policy; see Kanban query usage and configuration types.

Start with Your First Board; these recipes extend its grid and config.

// The backend must return this same repeating status/rank order.
const statuses = ['todo', 'doing', 'done'];
const loadCards = async (skip: number, limit: number) => {
const response = await fetch(`/api/cards?skip=${skip}&limit=${limit}`);
const result = await response.json();
return {
data: result.cards,
total: result.total,
};
};
grid.kanban = {
columns: config.columns,
remote: {
loadData: loadCards,
total: 100_000,
chunkSize: 100,
preloadThreshold: 0.75,
placeholder: (index) => ({
id: `remote-${index}`,
status: statuses[index % statuses.length],
order: Math.floor(index / statuses.length),
}),
},
};

With total and placeholder, Kanban works like Infinity Scroll: it creates the known number of lightweight positions, renders an animated skeleton for each unloaded card row, and replaces the requested 100-card slice in place. The placeholder only needs a unique temporary id, workflow column, swimlane when configured, and rank fields. Its ID may differ from the returned server card ID; the source offset owns replacement identity. The workflow, swimlane, and rank values must match the corresponding server card. Scrolling directly to an unloaded area requests that area’s chunk; approaching a loaded chunk boundary preloads the next one.

For automatic refresh, add query: { refetchInterval: 5_000 } to the same remote object. Indexed mode refreshes mounted blocks in their exact source slots. Append mode refreshes the retained sequence from its first page and recomputes subsequent offsets from the new page lengths. refreshRemote() still resets to page zero and clears local history. Automatic refetch applies authoritative server data without creating a local history entry.

The optional cancellation context uses position ten of the existing Infinity Scroll loader signature. Positions three through nine remain undefined for whole-board Kanban reads:

import type { KanbanRemoteConfig } from '@revolist/kanban';
const loadData: KanbanRemoteConfig<Card>['loadData'] = async (...args) => {
const [skip, limit] = args;
const response = await fetch(`/api/cards?skip=${skip}&limit=${limit}`, {
signal: args[9]?.signal,
});
if (!response.ok) throw new Error('Could not load cards');
return response.json();
};

Each board gets a private cache by default. Equivalent boards can explicitly share query.source; destroying one board releases only its subscriptions. Destroying the last subscriber aborts its signal and stops polling. query: false keeps direct manual reads and disables native caching/refetch policy. Loaded data is detached for each board. Server filter/dataset changes must use a matching source identity or replace the loader/query configuration; board filters remain local projection filters. There is no per-column paging contract.

Without placeholder, remote mode retains append paging: page zero replaces the current cards and later pages append as the board nears its scroll boundary. Both modes preserve loaded cards on errors and expose the same inline retry action.

The result can be a card array or { data, total?, hasMore? }. In append mode, a returned total takes precedence over hasMore; otherwise an explicit hasMore wins, then the configured or previously returned total is consulted, and only then does a short page end the stream. Indexed mode treats the configured total as its fixed layout: every request must return its complete requested slice, and response total or hasMore values must agree with that layout. A mismatch keeps the slice retryable and emits the remote error event.

Local update and move operations preserve indexed server positions. Create and delete are rejected in indexed mode because they would shift every later offset; perform those operations on the server and call refreshRemote(). Replacing grid.source externally restarts indexed loading from page zero. In append mode, local CRUD does not rewind the cursor and an external source replacement adopts its length as the next offset. Both behaviors invalidate a late response.

The loader type and indexed chunk behavior are shared with InfinityScrollPlugin, so the same application adapter can power a table or Kanban view. Kanban needs the additional placeholder factory because an unloaded card’s workflow column and optional swimlane cannot be inferred from a global total. The two plugins should not own one grid simultaneously because Kanban projects cards into its own virtual rows.

ModeUse whenWhat users see
AppendYou can fetch the next ordered card pageLoaded cards plus subsequent cards as they arrive
IndexedYou know a fixed total and can place every unloaded positionStable board slots with skeletons replaced in place

placeholder(index) must describe the same workflow column, lane and rank as the eventual server record. A global total alone cannot tell Kanban where an unloaded card belongs.

Search, derived filter choices and append-mode WIP counts cover loaded cards. Indexed placeholders can participate in counts, but they do not replace backend validation. Create/delete in indexed mode must happen on the server followed by refresh because inserting a record shifts later offsets.

See caching, server saving and large boards. See the Kanban API for exact types.