Vanilla JavaScript
You can use GridStorm without any framework by combining @gridstorm/core (the headless engine) and @gridstorm/dom-renderer (the DOM rendering layer) directly. This is the most lightweight approach and works in any JavaScript or TypeScript project.
Installation
npm install @gridstorm/core @gridstorm/dom-renderer @gridstorm/theme-default
Creating a Grid
Every GridStorm instance requires three steps: create the engine with createGrid(), create a DomRenderer, and mount it to a container element.
import { createGrid } from '@gridstorm/core';
import { DomRenderer } from '@gridstorm/dom-renderer';
import '@gridstorm/theme-default';
const container = document.getElementById('my-grid')!;
// 1. Create the headless grid engine
const engine = createGrid({
columns: [
{ field: 'name', headerName: 'Name', width: 200, sortable: true },
{ field: 'age', headerName: 'Age', width: 100, sortable: true },
{ field: 'department', headerName: 'Department', width: 150 },
],
rowData: [
{ name: 'Alice', age: 30, department: 'Engineering' },
{ name: 'Bob', age: 25, department: 'Design' },
{ name: 'Charlie', age: 35, department: 'Marketing' },
],
getRowId: (params) => params.data.name,
rowHeight: 40,
headerHeight: 48,
});
// 2. Create the DOM renderer and mount it
const renderer = new DomRenderer({
container,
engine,
});
renderer.mount();
Your HTML just needs a container with a defined height:
<div id="my-grid" style="height: 400px; width: 100%;"></div>
Adding Plugins
Plugins are passed to createGrid() in the plugins array. Each plugin is an object created by its factory function.
import { createGrid } from '@gridstorm/core';
import { DomRenderer } from '@gridstorm/dom-renderer';
import { SortingPlugin } from '@gridstorm/plugin-sorting';
import { FilteringPlugin } from '@gridstorm/plugin-filtering';
import { SelectionPlugin } from '@gridstorm/plugin-selection';
import { PaginationPlugin } from '@gridstorm/plugin-pagination';
import '@gridstorm/theme-default';
const engine = createGrid({
columns: [
{ field: 'name', headerName: 'Name', sortable: true, filter: true },
{ field: 'email', headerName: 'Email', sortable: true, filter: true },
{ field: 'role', headerName: 'Role', filter: true },
],
rowData: employees,
getRowId: (params) => params.data.id,
plugins: [
SortingPlugin(),
FilteringPlugin(),
SelectionPlugin({ mode: 'multiple' }),
PaginationPlugin({ pageSize: 25 }),
],
rowSelection: 'multiple',
pagination: true,
paginationPageSize: 25,
});
const renderer = new DomRenderer({
container: document.getElementById('my-grid')!,
engine,
floatingFilter: true,
checkboxSelection: true,
enablePagination: true,
pageSizeOptions: [10, 25, 50, 100],
});
renderer.mount();
Using the API
The engine.api object provides methods for programmatic control of the grid.
const { api } = engine;
// Update row data
api.setRowData(newEmployees);
// Sort programmatically
api.setSortModel([{ colId: 'name', sort: 'asc' }]);
// Filter programmatically
api.setFilterModel({
role: { filterType: 'text', type: 'equals', filter: 'Engineer' },
});
// Quick filter (searches all columns)
api.setQuickFilter('alice');
// Selection
api.selectAll();
const selected = api.getSelectedRows();
api.deselectAll();
// Column manipulation
api.setColumnVisible('email', false);
api.setColumnWidth('name', 300);
api.setColumnPinned('name', 'left');
api.moveColumn('role', 0);
// Pagination
api.paginationGoToPage(2);
// Get current state
const state = engine.store.getState();
console.log('Displayed rows:', state.displayedRowIds.length);
console.log('Sort model:', state.sortModel);
Listening to Events
The engine.eventBus provides typed event subscriptions. Each on() call returns an unsubscribe function.
// Cell click
const unsubClick = engine.eventBus.on('cell:clicked', (event) => {
console.log('Clicked cell:', event.colId, 'value:', event.value);
});
// Sort changes
engine.eventBus.on('column:sort:changed', (event) => {
console.log('New sort model:', event.sortModel);
});
// Selection changes
engine.eventBus.on('selection:changed', (event) => {
console.log('Selected rows:', event.selectedNodes.length);
});
// Filter changes
engine.eventBus.on('filter:changed', (event) => {
console.log('Filter model:', event.filterModel);
});
// Row data changes
engine.eventBus.on('rowData:changed', (event) => {
console.log('Row count:', event.rowCount);
});
// Cell editing
engine.eventBus.on('cell:editingStarted', (event) => {
console.log('Editing:', event.colId, event.node.id);
});
engine.eventBus.on('cell:editingStopped', (event) => {
console.log('Edit stopped, cancelled:', event.cancelled);
});
// Pagination
engine.eventBus.on('pagination:changed', (event) => {
console.log('Page:', event.currentPage, 'of', event.totalPages);
});
// Column resized
engine.eventBus.on('column:resized', (event) => {
console.log('Column', event.column.colId, 'resized to', event.column.width);
});
// Unsubscribe when no longer needed
unsubClick();
Subscribing to State Changes
The engine.store allows you to subscribe to any state change:
const unsubscribe = engine.store.subscribe(() => {
const state = engine.store.getState();
document.getElementById('row-count')!.textContent =
`${state.displayedRowIds.length} rows`;
});
Dispatching Commands
You can dispatch commands directly through the command bus for operations not exposed on the API:
// Toggle sort on a column
engine.commandBus.dispatch('sort:toggle', { colId: 'name', multiSort: false });
// Set selection
engine.commandBus.dispatch('selection:set', {
selectedRowIds: new Set(['row-1', 'row-2']),
});
Theming
Apply themes by setting data-theme on the grid container or any ancestor element:
<div data-theme="dark">
<div id="my-grid" style="height: 400px;"></div>
</div>
Switch themes at runtime:
const wrapper = document.getElementById('grid-wrapper')!;
wrapper.setAttribute('data-theme', 'dark');
wrapper.setAttribute('data-density', 'compact');
Destroying the Grid
Always destroy the grid when you are done with it to clean up event listeners, observers, and DOM elements:
// Destroy the renderer first (removes DOM elements and observers)
renderer.destroy();
// Then destroy the engine (cleans up plugins, event bus, store)
engine.destroy();
Full Working Example
Here is a complete standalone example with sorting, filtering, selection, and pagination:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>GridStorm Vanilla Example</title>
<style>
#grid-wrapper { height: 500px; width: 100%; }
.toolbar { margin-bottom: 8px; display: flex; gap: 8px; }
.toolbar button { padding: 4px 12px; }
.toolbar input { padding: 4px 8px; }
</style>
</head>
<body>
<div class="toolbar">
<input id="search" type="text" placeholder="Quick filter..." />
<button id="btn-select-all">Select All</button>
<button id="btn-deselect">Deselect All</button>
<button id="btn-theme">Toggle Dark Mode</button>
<span id="status"></span>
</div>
<div id="grid-wrapper" data-theme="light">
<div id="my-grid" style="height: 100%;"></div>
</div>
<script type="module" src="./main.ts"></script>
</body>
</html>
import { createGrid } from '@gridstorm/core';
import { DomRenderer } from '@gridstorm/dom-renderer';
import { SortingPlugin } from '@gridstorm/plugin-sorting';
import { FilteringPlugin } from '@gridstorm/plugin-filtering';
import { SelectionPlugin } from '@gridstorm/plugin-selection';
import { PaginationPlugin } from '@gridstorm/plugin-pagination';
import '@gridstorm/theme-default';
// Sample data
const rowData = Array.from({ length: 200 }, (_, i) => ({
id: String(i),
name: `Employee ${i + 1}`,
department: ['Engineering', 'Design', 'Marketing', 'Sales'][i % 4],
salary: 50000 + Math.floor(Math.random() * 80000),
}));
// Create grid engine
const engine = createGrid({
columns: [
{ field: 'name', headerName: 'Name', width: 200, sortable: true, filter: true },
{ field: 'department', headerName: 'Department', width: 150, sortable: true, filter: true },
{
field: 'salary',
headerName: 'Salary',
width: 120,
sortable: true,
valueFormatter: ({ value }) => `$${Number(value).toLocaleString()}`,
},
],
rowData,
getRowId: (params) => params.data.id,
plugins: [
SortingPlugin(),
FilteringPlugin(),
SelectionPlugin({ mode: 'multiple' }),
PaginationPlugin({ pageSize: 50 }),
],
rowSelection: 'multiple',
pagination: true,
paginationPageSize: 50,
});
// Create and mount renderer
const renderer = new DomRenderer({
container: document.getElementById('my-grid')!,
engine,
floatingFilter: true,
checkboxSelection: true,
enablePagination: true,
});
renderer.mount();
// Toolbar: quick filter
document.getElementById('search')!.addEventListener('input', (e) => {
engine.api.setQuickFilter((e.target as HTMLInputElement).value);
});
// Toolbar: selection buttons
document.getElementById('btn-select-all')!.addEventListener('click', () => {
engine.api.selectAll();
});
document.getElementById('btn-deselect')!.addEventListener('click', () => {
engine.api.deselectAll();
});
// Toolbar: theme toggle
let isDark = false;
document.getElementById('btn-theme')!.addEventListener('click', () => {
isDark = !isDark;
document.getElementById('grid-wrapper')!.setAttribute(
'data-theme',
isDark ? 'dark' : 'light',
);
});
// Status bar
const statusEl = document.getElementById('status')!;
engine.store.subscribe(() => {
const state = engine.store.getState();
const selected = state.selection.selectedRowIds.size;
statusEl.textContent = `${state.displayedRowIds.length} rows | ${selected} selected`;
});
Next Steps
- React Adapter -- use GridStorm with React hooks and portals
- Angular Adapter -- use GridStorm with Angular
- Theming -- customize the grid appearance
- Events & Commands -- the event and command system in depth