Server-Side Plugin
The Server-Side plugin enables virtual scrolling with lazy loading from a remote API. It’s designed for large datasets (10,000+ rows) where loading all data upfront would be impractical or slow.
When to Use This Plugin
Section titled “When to Use This Plugin”| Scenario | Recommended Approach |
|---|---|
| < 1,000 rows | Load all data upfront (no plugin needed) |
| 1,000 - 10,000 rows | Consider based on network speed and data complexity |
| 10,000+ rows | Use ServerSidePlugin |
| Infinite scroll / pagination | Use ServerSidePlugin |
| Data changes frequently on server | Use ServerSidePlugin with refresh() |
Key Features
Section titled “Key Features”- Block-based fetching: Loads only visible rows plus a buffer
- LRU caching: Keeps recently viewed blocks in memory
- Automatic prefetching: Loads next blocks as user scrolls
- Concurrent request limiting: Prevents overwhelming the server
- Loading placeholders: Shows loading state for pending rows
- Integration with sorting/filtering: Works with
sortHandlerandfilterHandler
Installation
Section titled “Installation”import '@toolbox-web/grid/features/server-side';Basic Usage
Section titled “Basic Usage”The feature requires a data source that implements the getRows method. Set it via getPluginByName('serverSide') after the grid is ready:
import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/server-side';
const dataSource = { async getRows(params) { // params: { startRow, endRow, sortModel, filterModel } const response = await fetch(`/api/data?start=${params.startRow}&end=${params.endRow}`); const data = await response.json(); return { rows: data.rows, totalRowCount: data.total, // Required for scroll height calculation }; },};
const grid = queryGrid('tbw-grid');grid.gridConfig = { columns: [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ], features: { serverSide: { pageSize: 50 }, },};
// Set data source after grid is readygrid.ready().then(() => { const plugin = grid.getPluginByName('serverSide'); plugin.setDataSource(dataSource);});import '@toolbox-web/grid-react/features/server-side';import { DataGrid, useGrid } from '@toolbox-web/grid-react';import { useEffect } from 'react';
const dataSource = { async getRows(params) { const response = await fetch(`/api/data?start=${params.startRow}&end=${params.endRow}`); const data = await response.json(); return { rows: data.rows, totalRowCount: data.total }; },};
function ServerSideGrid() { const { ref, element } = useGrid();
useEffect(() => { if (element) { element.ready().then(() => { const plugin = element.getPluginByName('serverSide'); plugin?.setDataSource(dataSource); }); } }, [element]);
return ( <DataGrid ref={ref} columns={[ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ]} serverSide={{ pageSize: 50 }} style={{ height: '400px' }} /> );}<script setup>import '@toolbox-web/grid-vue/features/server-side';import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';import { ref, onMounted } from 'vue';
const gridRef = ref(null);
const dataSource = { async getRows(params) { const response = await fetch(`/api/data?start=${params.startRow}&end=${params.endRow}`); const data = await response.json(); return { rows: data.rows, totalRowCount: data.total }; },};
onMounted(async () => { const grid = gridRef.value?.element; await grid?.ready(); grid?.getPluginByName('serverSide')?.setDataSource(dataSource);});</script>
<template> <TbwGrid ref="gridRef" :server-side="{ pageSize: 50 }" style="height: 400px"> <TbwGridColumn field="id" header="ID" /> <TbwGridColumn field="name" header="Name" /> <TbwGridColumn field="email" header="Email" /> </TbwGrid></template>// Feature import - enables the [serverSide] inputimport '@toolbox-web/grid-angular/features/server-side';
import { Component, viewChild, ElementRef } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { ColumnConfig, GridElement } from '@toolbox-web/grid';
@Component({ selector: 'app-server-side-grid', imports: [Grid], template: ` <tbw-grid #grid [columns]="columns" [serverSide]="{ pageSize: 50 }" style="height: 400px; display: block;"> </tbw-grid> `,})export class ServerSideGridComponent { gridRef = viewChild<ElementRef<GridElement>>('grid');
columns: ColumnConfig[] = [ { field: 'id', header: 'ID' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, ];
private dataSource = { getRows: async (params: any) => { const response = await fetch(`/api/data?start=${params.startRow}&end=${params.endRow}`); const data = await response.json(); return { rows: data.rows, totalRowCount: data.total }; }, };
ngAfterViewInit() { this.gridRef()?.nativeElement.ready().then(() => { const plugin = this.gridRef()?.nativeElement.getPluginByName('serverSide'); plugin?.setDataSource(this.dataSource); }); }}Virtual Scroll Mode
Section titled “Virtual Scroll Mode”<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/pinned-rows';import '@toolbox-web/grid/features/server-side';
const columns = [ { field: 'id', header: 'ID', type: 'number', sortable: true }, { field: 'name', header: 'Name', sortable: true }, { field: 'department', header: 'Department', sortable: true }, { field: 'salary', header: 'Salary', type: 'number', sortable: true }, { field: 'email', header: 'Email' },];
function generateMockData(count: number) { const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance']; const data = []; for (let i = 0; i < count; i++) { data.push({ id: i + 1, name: `Employee ${i + 1}`, department: departments[i % departments.length], salary: 50000 + Math.floor(Math.random() * 50000), email: `employee${i + 1}@example.com`, }); } return data;}
const grid = queryGrid('tbw-grid')!;const allData = generateMockData(10000);
const createDataSource = () => ({ async getRows(params: any) { await new Promise((r) => setTimeout(r, 200)); const data = [...allData]; if (params.sortModel?.length) { const { field, direction } = params.sortModel[0]; data.sort((a: any, b: any) => { const cmp = a[field] < b[field] ? -1 : a[field] > b[field] ? 1 : 0; return direction === 'asc' ? cmp : -cmp; }); } return { rows: data.slice(params.startRow, params.endRow), totalRowCount: data.length }; },});
function rebuild(pageSize = 50) { grid.gridConfig = { columns, features: { serverSide: { pageSize, cacheBlockSize: pageSize }, pinnedRows: { position: 'bottom', showRowCount: true, customPanels: [{ id: 'scroll-info', position: 'right', render: () => '<em>Scroll to load more rows...</em>' }], }, }, }; grid.ready().then(() => grid.getPluginByName('serverSide')?.setDataSource(createDataSource() as any));}
rebuild();Paging Mode
Section titled “Paging Mode”<tbw-grid style="height: 350px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/server-side';
const grid = queryGrid('tbw-grid');if (!grid) throw new Error('Grid element not found');
const pageSize = 50;let currentPage = 0;let totalPages = 1;
// Build pager UI and insert it after the gridconst pager = document.createElement('div');pager.style.cssText = 'padding: 8px; display: flex; gap: 8px; align-items: center; justify-content: center;';pager.innerHTML = ` <button class="prev" style="padding: 4px 8px;">← Prev</button> <span class="page-info">Page 1</span> <button class="next" style="padding: 4px 8px;">Next →</button>`;grid.after(pager);
const prevBtn = pager.querySelector('.prev') | null;const nextBtn = pager.querySelector('.next') | null;const pageInfo = pager.querySelector('.page-info') | null;
function generateMockData(count: number) { const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance']; return Array.from({ length: count }, (_, i) => ({ id: i + 1, name: `Employee ${i + 1}`, department: departments[i % departments.length], salary: 50000 + Math.floor(Math.random() * 50000), email: `employee${i + 1}@example.com`, }));}
const allData = generateMockData(500);
function updatePager() { if (pageInfo) pageInfo.textContent = `Page ${currentPage + 1} of ${totalPages}`; if (prevBtn) prevBtn.disabled = currentPage === 0; if (nextBtn) nextBtn.disabled = currentPage >= totalPages - 1;}
// Use ServerSidePlugin with a data source that returns one page at a timegrid.gridConfig = { columns: [ { field: 'id', header: 'ID', type: 'number', sortable: true }, { field: 'name', header: 'Name', sortable: true }, { field: 'department', header: 'Department', sortable: true }, { field: 'salary', header: 'Salary', type: 'number', sortable: true }, { field: 'email', header: 'Email' }, ], features: { serverSide: { pageSize } },};
function loadPage(page: number) { currentPage = page; const start = page * pageSize; totalPages = Math.ceil(allData.length / pageSize);
// Simulate a server response by slicing data for the requested page grid.rows = allData.slice(start, start + pageSize); updatePager();}
prevBtn?.addEventListener('click', () => loadPage(currentPage - 1));nextBtn?.addEventListener('click', () => loadPage(currentPage + 1));
loadPage(0);Server-Side Sorting
Section titled “Server-Side Sorting”<tbw-grid style="height: 400px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
const columns = [ { field: 'id', header: 'ID', type: 'number', sortable: true }, { field: 'name', header: 'Name', sortable: true }, { field: 'department', header: 'Department', sortable: true }, { field: 'salary', header: 'Salary', type: 'number', sortable: true }, { field: 'email', header: 'Email' },];
function generateMockData(count: number) { const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance']; const data = []; for (let i = 0; i < count; i++) { data.push({ id: i + 1, name: `Employee ${i + 1}`, department: departments[i % departments.length], salary: 50000 + Math.floor(Math.random() * 50000), email: `employee${i + 1}@example.com`, }); } return data;}
const grid = queryGrid('tbw-grid');
const serverData = generateMockData(1000);
grid.gridConfig = { columns, sortHandler: async (rows, sortState) => { // Show loading indicator grid.setAttribute('aria-busy', 'true');
try { // Simulate server delay await new Promise((r) => setTimeout(r, 300));
// Server-side sort simulation const sorted = [...serverData].sort((a, b) => { const aVal = (a)[sortState.field]; const bVal = (b)[sortState.field]; const cmp = aVal! < bVal! ? -1 : aVal! > bVal! ? 1 : 0; return cmp * sortState.direction; });
return sorted; } finally { grid.removeAttribute('aria-busy'); } }, };
grid.rows = serverData;Use the sortHandler config option for async sorting. When a user clicks a sortable column header,
your handler is called instead of the built-in sorting logic. This is ideal for large datasets
where sorting should happen on the backend.
grid.gridConfig = { columns: [...], sortHandler: async (rows, sortState, columns) => { // sortState: { field: 'name', direction: 1 } // 1 = asc, -1 = desc const response = await fetch( `/api/data?sort=${sortState.field}&dir=${sortState.direction === 1 ? 'asc' : 'desc'}` ); return response.json(); },};The handler receives:
rows- Current row array (may be useful for optimistic updates)sortState-{ field: string, direction: 1 | -1 }columns- Column configurations (accesssortComparatorif needed)
Return the sorted array directly or a Promise that resolves to it.
Server-Side Filtering
Section titled “Server-Side Filtering”For server-side filtering, use the FilteringPlugin’s async handlers.
See the Filtering Plugin documentation for details on valuesHandler and filterHandler.
Configuration Options
Section titled “Configuration Options”See ServerSideConfig for the full list of options and defaults.
TypeScript Interfaces
Section titled “TypeScript Interfaces”ServerSideDataSource— Data source contract withgetRows()methodGetRowsParams— Parameters passed togetRows()(startRow, endRow, sortModel, filterModel)GetRowsResult— Return type withrowsandtotalRowCount
Programmatic API
Section titled “Programmatic API”const plugin = grid.getPluginByName('serverSide');
plugin.setDataSource(newDataSource); // Set or replace the data sourceplugin.refresh(); // Reload current viewport from serverplugin.purgeCache(); // Clear all cached blocksplugin.getTotalRowCount(); // Get server-reported total row countplugin.isRowLoaded(index); // Check if a specific row is in cacheplugin.getLoadedBlockCount(); // Number of blocks currently cachedArchitecture: How It Works
Section titled “Architecture: How It Works”The plugin uses a block-based caching strategy:
┌────────────────────────────────────────────────┐│ Total Dataset: 100,000 rows (on server) │├────────────────────────────────────────────────┤│ Block 0: rows 0-99 [CACHED] ││ Block 1: rows 100-199 [CACHED] ││ Block 2: rows 200-299 [LOADING...] ││ Block 3: rows 300-399 [NOT LOADED] ││ ... ││ Block 999: rows 99900-99999 [NOT LOADED] │└────────────────────────────────────────────────┘Scroll triggers:
onScrollevent fires- Plugin calculates which blocks are needed for the visible viewport
- Missing blocks are requested from the data source
requestRender()is called when data arrives- Grid re-renders with new data
Loading state: Rows that haven’t loaded yet are represented with placeholder objects:
{ __loading: true, __index: 42 }You can detect loading rows via a custom cell renderer and style them accordingly:
columns: [{ field: 'name', renderer: (value, row) => row.__loading ? '<span class="loading">Loading…</span>' : value,}]Combining with Other Plugins
Section titled “Combining with Other Plugins”The ServerSidePlugin works well with:
| Plugin | Integration |
|---|---|
FilteringPlugin | Use filterHandler for server-side filtering |
MultiSortPlugin | Use sortHandler for server-side sorting |
SelectionPlugin | Works normally - selection state is client-side |
EditingPlugin | Works normally - edits are local until you sync |
ExportPlugin | Only exports cached rows by default |
Note: RowGroupingPlugin and TreePlugin are not recommended with server-side data as they require all data to be present for grouping calculations.
See Also
Section titled “See Also”- Filtering — Client-side filtering
- Multi-Sort — Client-side multi-sort
- Export — Export grid data
- Common Patterns — Full application recipes
- Plugins Overview — Plugin compatibility and combinations