Pages and server data
With pagination the grid shows one page at a time and puts a pager under the rows. People can jump to the first, previous, next or last page, and pick the…
Live demoPages & server data
Page through rows, or load them from your server in blocks as people scroll.
Code for this demo
import { createGrid, type ColumnDef, type GridApi, type GridOptions, type Row, type ServerParams } from '@freegridjs/core';
import { makeRows } from '../data';
import { button, separator, text, tr, type DemoContext } from '../ui';
type Mode = 'pages' | 'server' | 'infinite' | 'serverPages';
/** A pretend server in the browser: sorts, filters and answers after a short delay. */
function fakeServer(rows: Row[], log: (m: string) => void, knowsTotal = true) {
return {
getRows(p: ServerParams): Promise<{ rows: Row[]; total?: number }> {
let list = rows;
const q = p.quickFilter.trim().toLowerCase();
if (q) list = list.filter(r => `${r.name} ${r.company} ${r.country}`.toLowerCase().includes(q));
const s = p.sort[0];
if (s) {
const dir = s.dir === 'desc' ? -1 : 1;
list = [...list].sort((a, b) => (a[s.colId] < b[s.colId] ? -dir : a[s.colId] > b[s.colId] ? dir : 0));
}
log(`getRows → ${p.start}–${p.end}${s ? ` sort ${s.colId} ${s.dir}` : ''}${q ? ` "${q}"` : ''}`);
const page = list.slice(p.start, p.end);
return new Promise(resolve => setTimeout(() => resolve(knowsTotal ? { rows: page, total: list.length } : { rows: page }), 350));
},
};
}
export default function mount(ctx: DemoContext) {
const { lang } = ctx;
const h = (en: string, ko: string) => tr(lang, { en, ko });
const columns: ColumnDef[] = [
{ field: 'id', header: 'ID', type: 'number', width: 90 },
{ field: 'name', header: h('Name', '이름'), width: 170 },
{ field: 'company', header: h('Company', '회사'), width: 150 },
{ field: 'country', header: h('Country', '국가'), width: 150, filter: false },
{ field: 'total', header: h('Total', '합계'), type: 'number', format: '#,##0', width: 130 },
{ field: 'joined', header: h('Joined', '가입일'), type: 'date', width: 130 },
];
const big = makeRows(100000);
let grid: GridApi | null = null;
const show = (mode: Mode) => {
grid?.destroy();
const options: Partial<GridOptions> =
mode === 'pages' ? { data: makeRows(1000), pagination: { pageSize: 50, pageSizes: [20, 50, 100] } }
: mode === 'server' ? { datasource: fakeServer(big, ctx.log), blockSize: 100 }
: mode === 'infinite' ? { datasource: fakeServer(big, ctx.log, false), blockSize: 100 }
: { datasource: fakeServer(big, ctx.log), pagination: { pageSize: 50, pageSizes: [20, 50, 100] } };
// the pretend server only understands sorting and the quick search
const cols = mode === 'pages' ? columns : columns.map(c => ({ ...c, filter: false as const }));
grid = createGrid(ctx.host, { columns: cols, locale: lang, theme: 'erp', statusBar: true, ...options });
grid.on('pageChange', e => ctx.log(`pageChange → ${e.page + 1} / ${e.pageCount}`));
grid.on('rowsLoaded', e => ctx.log(`rowsLoaded → ${e.start}–${e.end}${e.total !== undefined ? ` of ${e.total}` : ''}`));
};
text(ctx, h('The server modes fetch 100 rows at a time from a pretend server with a short delay. Scroll fast to see the placeholders.',
'서버 모드는 지연이 있는 가짜 서버에서 100행씩 받아 옵니다. 빠르게 스크롤하면 불러오는 동안의 자리 표시를 볼 수 있습니다.'));
separator(ctx);
button(ctx, h('Pages (1,000 rows in the browser)', '페이지 (브라우저의 1,000행)'), () => show('pages'));
button(ctx, h('Server, 100,000 rows', '서버, 10만 행'), () => show('server'));
button(ctx, h('Infinite scroll', '무한 스크롤'), () => show('infinite'));
button(ctx, h('Server pages', '서버 페이지'), () => show('serverPages'));
separator(ctx);
button(ctx, h('Search "Kim" on the server', '서버에서 "Kim" 검색'), () => grid?.setQuickFilter('Kim'));
button(ctx, h('Clear search', '검색 지우기'), () => grid?.setQuickFilter(''));
button(ctx, h('Reload rows', '다시 불러오기'), () => grid?.refreshRows());
show('pages');
return grid;
}Live demoServer-side grouping
Groups with counts and totals from the server, loaded as they open.
Code for this demo
import { createGrid, type Row, type ServerParams } from '@freegridjs/core';
import { makeRows } from '../data';
import { select, separator, text, tr, type DemoContext } from '../ui';
export default function mount(ctx: DemoContext) {
const { lang } = ctx;
const h = (en: string, ko: string) => tr(lang, { en, ko });
const all = makeRows(20000);
/**
* A pretend server. With groupBy it answers, for the open groups (groupKeys),
* one row per group with its count and totals — what a GROUP BY query returns.
*/
const datasource = {
getRows(p: ServerParams) {
const by = p.groupBy ?? [];
const keys = p.groupKeys ?? [];
let rows = all.filter(r => keys.every((k, i) => r[by[i]] === k));
let answer: { rows: Row[]; total?: number };
if (keys.length < by.length) {
const col = by[keys.length];
const groups = new Map<unknown, { count: number; q1: number; total: number }>();
for (const r of rows) {
const g = groups.get(r[col]) ?? { count: 0, q1: 0, total: 0 };
g.count++;
g.q1 += r.q1;
g.total += r.total;
groups.set(r[col], g);
}
const desc = p.sort.find(s => s.colId === col)?.dir === 'desc';
answer = { rows: [...groups].sort((a, b) => String(a[0]).localeCompare(String(b[0])) * (desc ? -1 : 1)).map(([v, g]) => ({ [col]: v, ...g })) };
ctx.log(`getRows(groupKeys: ${JSON.stringify(keys)}) → ${answer.rows.length} ${h('groups', '그룹')}`);
} else {
rows = rows.slice(p.start, p.end);
answer = { rows, total: rows.length };
ctx.log(`getRows(groupKeys: ${JSON.stringify(keys)}) → ${rows.length} ${h('rows', '행')}`);
}
return new Promise<typeof answer>(resolve => setTimeout(() => resolve(answer), 250));
},
};
const grid = createGrid(ctx.host, {
columns: [
{ field: 'country', header: h('Country', '국가'), width: 160 },
{ field: 'company', header: h('Company', '회사'), width: 160 },
{ field: 'name', header: h('Name', '이름'), width: 160 },
{ field: 'q1', header: 'Q1', type: 'number', format: '#,##0', width: 130, aggFunc: 'sum' },
{ field: 'total', header: h('Total', '합계'), type: 'number', format: '#,##0', width: 140, aggFunc: 'sum' },
],
datasource, getRowId: r => String(r.id), locale: lang,
groupBy: ['country', 'company'],
});
text(ctx, h('20,000 rows stay on the "server". Opening a group asks only for what is inside it; the log shows each request.',
'2만 행은 "서버"에만 있습니다. 그룹을 펼칠 때 그 안에 든 것만 요청합니다. 아래 기록에서 요청을 볼 수 있습니다.'));
separator(ctx);
select(ctx, h('Group by', '그룹 기준'), [['country,company', h('Country → Company', '국가 → 회사')], ['company', h('Company', '회사')], ['', h('None', '없음')]], 'country,company',
v => grid.setGroupBy(v ? v.split(',') : []));
return grid;
}With pagination the grid shows one page at a time and puts a pager under the rows. People can jump to the first, previous, next or last page, and pick the number of rows per page from pageSizes. Sorting or filtering goes back to the first page.
When the data lives on a server, pass a datasource instead of data. The grid asks getRows only for the rows on screen, in blocks of blockSize, and draws grey placeholders until they arrive. Sorting, filters and the quick search are sent along so the server can apply them. Return total when you know it. Leave it out for infinite scroll, which keeps loading until a block comes back short. Add pagination as well, and every page becomes one request. Grouping and adding or removing rows only work with data in the browser.
// Pages in the browser
createGrid(el, { columns, data, pagination: { pageSize: 50, pageSizes: [20, 50, 100] } });
// Rows from your server, 100 at a time, as people scroll
createGrid(el, {
columns,
datasource: {
async getRows({ start, end, sort, filters, quickFilter }) {
const res = await fetch('/api/orders?' + new URLSearchParams({
start, end, sort: JSON.stringify(sort), filters: JSON.stringify(filters), q: quickFilter,
}));
const { rows, total } = await res.json();
return { rows, total }; // leave out total for infinite scroll
},
},
blockSize: 100,
// pagination: { pageSize: 50 }, // add this for server-side pages
});