StockSharp JS Data Grid is the browser table component behind StockSharp's
web applications: a column-driven DataGrid that owns its header, body, sort
state and exported sheet, a ColumnSettings adapter for tables the server
already rendered, and a dependency-free .xlsx writer.
StockSharp website · GitHub repository · Issue tracker
npm install @stocksharp/gridsimport { DataGrid } from '@stocksharp/grids';
interface Order { id: number; symbol: string; side: number; price: number; }
const grid = new DataGrid<Order>({
head: document.querySelector<HTMLElement>('#orders thead')!,
body: document.querySelector<HTMLElement>('#orders tbody')!,
columns: [
{ key: 'id', header: 'ID', exportable: true, value: (o) => o.id },
{ key: 'symbol', header: 'Symbol', exportable: true, value: (o) => o.symbol },
{
key: 'side',
header: 'Side',
exportable: true,
value: (o) => o.side,
render: (o) => (o.side === 0 ? 'Buy' : 'Sell'),
cellClass: (o) => (o.side === 0 ? 'side-buy' : 'side-sell'),
exportValue: (o) => (o.side === 0 ? 'Buy' : 'Sell'),
},
{ key: 'price', header: 'Price', exportable: true, value: (o) => o.price },
],
defaultSort: { col: 'id', dir: 'desc' },
rowKey: (o) => String(o.id),
emptyText: 'No orders',
});
grid.setRows(orders);
grid.download('orders', 'Orders'); // orders-20260807-120000.xlsxThe package also ships a ready-to-use browser bundle exposed as window.SSGrid:
<script src="https://cdn.jsdelivr.net/npm/@stocksharp/grids@0.1.0/dist/ssgrid.js"></script>
<script>
const { DataGrid } = window.SSGrid;
</script>A blotter used to declare its columns three times — the header cells in markup, the sort accessors next to the widget, and the header/row arrays in its export — with nothing keeping the three in step. Here a column states its key, caption, how to read its value, how to render it, how to class it and whether it exports, and the grid derives the header, the body, the sort and the sheet from that one declaration.
Two things a naive column pipeline cannot express are first-class:
render()may return aNode. A cell can hold a real control with its ownaddEventListenerinstead of an inlineonclickattribute reaching a global. ADocumentFragmentworks too, so a cell can mix text and an element.- Pinned rows sit outside the sort and outside the export. A balance summary
stays on top whichever column the user sorts by, and never lands in the sheet.
pinnedRows()is re-read on every render, so live figures update.
The grid renders elements, never HTML strings, so nothing in it can be an
injection site. Colour stays with the host: a column returns class names
(cellClass, rowClass) and the host stylesheet decides what they look like.
Three class names come out of the package itself and an adopting site has to
style them: grid-empty on the cell that spans the table when there is nothing
to show, visually-hidden on a header whose caption is for screen readers only,
and sort-asc / sort-desc on the sorted header. The first is ours; the other
two are spelled the way Bootstrap spells them, which is a coupling worth knowing
about before adopting.
renderLimit caps what is painted, not what is exported — which is what a
watchlist wants: a screen-sized table over a full sheet. afterRender() fires
after every repaint, including one caused by a sort click the caller never saw,
so a host can re-subscribe to the symbols now on screen.
TableSort is the sort state behind the grid, and is usable on its own. Clicking
a <th data-sort="key"> cycles asc → desc → default: there is no "unsorted"
state, because a table always has a deterministic order. Each table declares its
resting order (defaultSort), so a first paint is meaningful instead of exposing
the raw arrival order of a cache hydration, a snapshot and live updates. Rows
with no value for the sorted column sink to the end in both directions, and
apply() always returns a copy, so the caller's array is never reordered.
ColumnSettings is the other half: a table the server already rendered, where
the columns exist as markup and the only thing missing is the user's say over
which are shown and in what order. It discovers the columns from the data-col
keys the header already carries, and moves and hides cells in place.
It knows nothing about the page it is on — the dialog, the store and the affordance that opens the picker are all arguments:
import { ColumnSettings } from '@stocksharp/grids';
const settings = new ColumnSettings({
table: document.querySelector<HTMLTableElement>('#users')!,
dialog: {
list: document.querySelector<HTMLElement>('#column-list')!,
moveUpTitle: t('Move up'),
moveDownTitle: t('Move down'),
classes: {
item: 'list-group-item d-flex align-items-center gap-2',
toggle: 'form-check-input',
label: 'flex-grow-1',
move: 'btn btn-sm btn-outline-secondary',
moveUpIcon: 'bi bi-arrow-up',
moveDownIcon: 'bi bi-arrow-down',
},
open: () => modal.show(),
close: () => modal.hide(),
},
store: {
read: () => new URLSearchParams(location.search).get('columns')?.split(',') ?? null,
write: (visible) => updateQueryString(visible),
},
});
// The host owns the dialog chrome — its markup, its buttons, its modal library —
// so the host wires them too. openPicker() only fills the list and shows the
// dialog; without the two below, nothing the user picks can ever be committed.
header.addEventListener('contextmenu', () => settings.openPicker());
confirmButton.addEventListener('click', () => settings.applyPicked());
resetButton.addEventListener('click', () => settings.resetToDefault());applyPicked() and resetToDefault() each apply the layout, persist it through
the store and call dialog.close() — the adapter closes the dialog it was told
how to close, and does nothing else to it.
The table needs a real <thead>: a parser inserts a missing <tbody> but never
a header, and the columns are only knowable from its cells, so a table without
one is rejected at construction rather than faulting later.
A column with no data-col key (an Actions column) is a fixed anchor: it keeps
its slot and is never hidden. A row whose cell count does not match the header (a
colspan "nothing to show" row) is left alone. write(null) means "this IS the
table default", so a store that lives in a URL carries its parameter only while
it means something.
TableExport.download(baseName, sheetName, headers, rows) builds a real OOXML
workbook — a store-only ZIP with the minimal SpreadsheetML part set, strings as
inline strings and finite numbers as native numeric cells — so Excel,
LibreOffice and Numbers open it without a conversion warning. No third-party
library is involved, and CSV is never renamed to .xlsx.
DataGrid.download() routes through it with the exportable columns, every held
row, in the order on screen. exportData() returns the same headers and rows
without touching the browser, which is how the sheet's content is asserted in a
test.
src/
index.ts complete public entry point
data-grid.ts DataGrid: columns, rendering, pinned rows, export
table-sort.ts TableSort: the column-sort state behind the grid
column-settings.ts ColumnSettings: column picker for a rendered table
table-export.ts TableExport: dependency-free .xlsx writer
tests/ unit suite plus the fake DOM it runs against
Applications can let their own esbuild/Vite build compile the TypeScript published inside the package:
{
"dependencies": {
"@stocksharp/grids": "^0.1.0"
}
}import { DataGrid } from '@stocksharp/grids/source';
import { TableExport } from '@stocksharp/grids/source/table-export';./source exists for the case where there is nothing compiled to resolve.
dist/ is a build output and is gitignored, so a checkout of this repository
that has not been built yet ships only src/ — the . and ./data-grid entry
points point at files that are not there. A consumer that depends on such a
checkout imports through ./source and lets its own bundler compile the
TypeScript, which is what StockSharp's own web bundles do.
For sibling-repository development, replace the version with a relative path to
the checkout. The depth is whatever the consumer's own location makes it: from
Broker/Broker.Web.Trader/package.json, three directories below the workspace
root that holds Grids/, it is "file:../../../Grids". The import paths stay
identical either way.
Dedicated entry points are available for consumers with narrower needs:
@stocksharp/grids/data-grid— the grid alone;@stocksharp/grids/table-sort— sort state without a grid;@stocksharp/grids/column-settings— the picker for a server-rendered table;@stocksharp/grids/table-export— the workbook writer on its own.
npm run build produces:
| File | Purpose |
|---|---|
dist/esm/** |
complete ESM module tree |
dist/types/** |
TypeScript declarations |
dist/ssgrid.js |
complete browser IIFE exposed as window.SSGrid |
npm ci
npm test
npm run build
npm run pack:check
npm run release:patch
npm run release:minor
npm run release:major
npm run api:check
npm run api:update # only after reviewing an intentional public API change
CI verifies type checking, the reviewed declaration snapshot, the unit tests, the bundles and the tarball contents.
Publishing is driven by the version in package.json. release.yml runs on
every push to main and publishes only when that version is not yet on npm, so
an ordinary push is a no-op. To cut a release, bump the version and push:
npm run release:patch # or release:minor / release:major
git pushThe new version triggers release.yml, which rebuilds and tests the repository,
publishes to npm with provenance, then creates the v<version> tag and GitHub
Release. A failed publication can be retried from the workflow's Run workflow
action; already-published versions are detected and skipped.
Publishing authenticates through npm trusted publishing (OIDC) — there is no
NPM_TOKEN secret. The workflow grants id-token: write, and npm exchanges the
GitHub OIDC token for a short-lived publish token. The npm Trusted Publisher is
bound to the workflow filename release.yml, so do not rename that workflow
without updating the trusted publisher configuration on npmjs first.
Copyright © 2010-present StockSharp Platform LLC and/or its affiliates. All rights reserved. Use is governed by the StockSharp EULA and LICENSE.