Load Cards
Start with the server-side overview. A loader returns ordinary application cards; Kanban does not need pre-rendered board rows.
Load cards from your backend
A local HTTP demo backend returns pages of canonical cards.
Try it: Refresh normally, then try a slow connection and watch the loading state.
HTTP reads: 0 · writes: 0
What to notice: Kanban receives cards and total-count metadata; the app owns the endpoint.
TypeScript source
The supplied backend handlers use MSW simulation in the browser. Replace the transport with your application endpoint.
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.import { defineCustomElements } from '@revolist/revogrid/loader';import { KanbanPlugin, type KanbanConfig } from '@revolist/kanban';import '@revolist/revogrid-pro/styles.css';import '@revolist/kanban/styles.css';
export interface WorkCard { id: string; title: string; status: string; order: number; description?: string; team?: string; owner?: string; priority?: string; tags?: string[]; progress?: number; startDate?: string; endDate?: string;}export const demoCards: WorkCard[] = [ { id: 'task-1', title: 'Research customer needs', description: 'Collect feedback for the next release.', status: 'todo', team: 'product', owner: 'Ana', priority: 'normal', order: 1000 }, { id: 'task-2', title: 'Build onboarding', description: 'Make the first session useful.', status: 'doing', team: 'product', owner: 'Mia', priority: 'high', order: 1000 }, { id: 'task-3', title: 'Improve API errors', description: 'Return actionable validation messages.', status: 'todo', team: 'platform', owner: 'Rui', priority: 'normal', order: 2000 }, { id: 'task-4', title: 'Release checklist', description: 'Ready for the next deployment.', status: 'done', team: 'platform', owner: 'Sam', priority: 'normal', order: 1000 },];export const boardConfig: KanbanConfig<WorkCard> = { columns: [{ prop: 'todo', name: 'To do', size: 210 }, { prop: 'doing', name: 'In progress', size: 210 }, { prop: 'done', name: 'Done', size: 210 }], card: { titleField: 'title' }, cardRowHeight: 88, addCard: false, swimlaneColumn: false,};
export async function configureBoard( grid: HTMLRevoGridElement, config: KanbanConfig<WorkCard> = boardConfig, cards: readonly WorkCard[] = demoCards, extraPlugins: HTMLRevoGridElement['plugins'] = [],) { await defineCustomElements(); grid.plugins = [KanbanPlugin, ...extraPlugins]; // The packaged editor and history install automatically. grid.columns = [{ prop: 'title', name: 'Title' }, { prop: 'status', name: 'Status' }, { prop: 'owner', name: 'Owner' }]; // Cached remote data can hydrate immediately: finish initial grid setup first. grid.kanban = config.remote ? { ...config, remote: false } : config; grid.source = cards.map(card => ({ ...card })); await grid.componentOnReady(); const plugins = await grid.getPlugins(); const kanban = plugins.find((plugin): plugin is KanbanPlugin<WorkCard> => plugin instanceof KanbanPlugin); if (!kanban) throw new Error('Kanban could not start.'); if (config.remote) grid.kanban = config; return kanban;}
// Give your container a width and mount once after it exists in the DOM.export async function createKanbanBoard(container: HTMLElement) { await defineCustomElements(); const grid = document.createElement('revo-grid'); grid.style.cssText = 'display:block;width:100%;height:400px'; container.append(grid); try { const kanban = await configureBoard(grid); return { grid, kanban, destroy() { grid.remove(); } }; } catch (error) { grid.remove(); throw error; }}import { http, HttpResponse } from 'msw';import type { DemoBackendModule } from '@revolist/revogrid-examples/demo-api-mocks';
export interface ServerCard { id: string; title: string; status: string; order: number; }export function createKanbanDocsStore() { let failNextMove = false; let cards: ServerCard[] = Array.from({ length: 24 }, (_, index) => ({ id: `remote-${index + 1}`, title: `Server task ${index + 1}`, status: ['todo', 'doing', 'done'][index % 3], order: (index + 1) * 1000, })); return { failNextMove() { failNextMove = true; }, page(skip: number, limit: number) { if (!Number.isSafeInteger(skip) || skip < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new Error('Invalid page'); return { data: structuredClone(cards.slice(skip, skip + limit)), total: cards.length }; }, move(changes: unknown) { if (failNextMove) { failNextMove = false; throw new Error('Simulated save unavailable'); } if (!Array.isArray(changes) || !changes.length) throw new Error('Changed cards are required'); const ids = new Set<string>(); for (const card of changes) { if (!card || typeof card.id !== 'string' || ids.has(card.id) || !cards.some(item => item.id === card.id) || !['todo', 'doing', 'done'].includes(card.status) || !Number.isFinite(card.order)) throw new Error('Invalid card changes'); ids.add(card.id); } // Apply one validated move batch atomically, including rank-rebalanced neighbors. cards = cards.map(card => { const change = changes.find(item => item.id === card.id); return change ? { ...card, status: change.status, order: change.order } : card; }); return { saved: ids.size }; }, };}
/** HTTP backend supplied with the docs; isolated data per preview session. */export const kanbanDocsBackend: DemoBackendModule = { id: 'kanban-docs', create({ apiUrl, signal }) { const store = createKanbanDocsStore(); return { handlers: [ http.get(`${apiUrl}/cards`, ({ request }) => { const params = new URL(request.url).searchParams; try { return HttpResponse.json(store.page(Number(params.get('skip') ?? 0), Number(params.get('limit') ?? 6))); } catch { return HttpResponse.json({ message: 'Invalid page' }, { status: 400 }); } }), http.post(`${apiUrl}/control/fail-next-write`, () => { store.failNextMove(); return HttpResponse.json({ armed: true }); }), http.post(`${apiUrl}/moves`, async ({ request }) => { try { const body = await request.json() as { changedCards?: unknown }; signal.throwIfAborted(); request.signal.throwIfAborted(); return HttpResponse.json(store.move(body.changedCards)); } catch (error) { return HttpResponse.json({ message: error instanceof Error ? error.message : 'Invalid move batch' }, { status: error instanceof Error && error.message === 'Simulated save unavailable' ? 503 : 400 }); } }), ] }; },};Try it: load the example and refresh it. Watch the loading message and returned card count. This is a simulated backend; replace its URL with your application’s endpoint.
Minimal remote loader
Section titled “Minimal remote loader”Start with Your First Board; these recipes extend its grid and config.
grid.kanban = { columns: config.columns, remote: { chunkSize: 50, loadData: async (skip, limit, order, single, multi, quick, temporal, ast, execution, context) => { const response = await fetch(`/api/cards?skip=${skip}&limit=${limit}`, { signal: context?.signal, }); if (!response.ok) throw new Error('Unable to load cards'); return response.json(); // { data, total?, hasMore? } }, },};Response shape
Section titled “Response shape”Return either a card array or an object with data, optional total and optional hasMore:
return { data: cards, total: 24, hasMore: true };Cards must have the configured ID, status, rank and optional lane fields. Use stable server ordering so offsets mean the same thing between requests. Do not return Kanban’s projected grid rows.
Loading events
Section titled “Loading events”Listen to kanbanremoteloadstart, kanbanremoteloadsuccess and kanbanremoteloaderror for application feedback. A ready plugin exposes getRemoteState() with loading, next offset, continuation, total and error state. See Events and Methods.
Cancellation
Section titled “Cancellation”The shared loader signature keeps cancellation context at argument ten. A typed rest-argument callback avoids naming unused filter arguments:
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('Unable to load cards'); return response.json();};Card is your application card type. Forward the signal to network work so leaving or replacing the board can release a pending read.
Supplied backend
Section titled “Supplied backend”The live client is apps/portal/src/components/kanban/KanbanRemote.astro; its backend is examples/components/src/components/kanban/docs/backend.ts. The backend validates pages and move batches before applying them, and each mounted example has its own seeded store. Teardown aborts requests and disposes that session. Production APIs need application-owned authentication, durable storage, ordering and concurrency control.
Next steps
Section titled “Next steps”Continue with load pages, read errors and save changes. See the Kanban API for exact types.