How Server Data Works
Server-side Kanban loads card records from your application’s backend. Kanban places them into columns and handles interaction; your application decides how to save changes and enforce server-wide rules.
For example, the browser requests the first six cards, displays them, then requests more as needed. Moving a card changes its status and rank. An application event listener sends the changed records back to the server.
A board connected to a server
The supplied demo backend serves 24 cards through HTTP. The application saves moves from committed events.
Try it: Load another page, move a card, then refresh to verify the saved status.
HTTP reads: 0 · writes: 0
What to notice: Reads and writes use separate application-owned endpoints.
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 }); } }), ] }; },};Live HTTP example
Section titled “Live HTTP example”This example uses an isolated simulated backend implemented with the shared demo Service Worker. Requests use real fetch calls, but the data is in-memory demonstration state, not a deployed database. Each mounted example has its own session.
Try loading another page, moving a card and refreshing. The saved move should return from that example’s backend. Reset restores its seeded cards.
Read and save are separate
Section titled “Read and save are separate”| Task | Owner | Guide |
|---|---|---|
| Fetch pages of cards | kanban.remote.loadData | Load cards |
| Request later pages or indexed slots | Kanban remote reader | Load pages |
| Reuse reads or refresh them | remote.query read policy | Caching and refresh |
| Submit changes and show pending/errors | Your application events/API | Save card changes |
| Recover a failed read | Reader retry or application refresh | Loading errors and retry |
The read configuration has no commit method. The optional managed saving adapter is a separate complete-dataset integration and cannot be combined with paged remote loading.
Start small
Section titled “Start small”- Make a local first board work with your field names.
- Replace its local records with the remote loader.
- Add saving only when your app is ready to persist changes.
- Add paging, cache settings and error recovery as your workflow needs them.
Counts, search and local WIP checks describe the records available to the board. The backend must enforce permissions, concurrency, full-board counts and capacity when the complete dataset is not present.
Next steps
Section titled “Next steps”Continue with Load Cards and Save Card Changes. See the Kanban API for exact types.