Server-side grid with Northwind OData
by stitot
HTML
<script src="https://cdn.jsdelivr.net/npm/@highcharts/grid-pro/grid-pro.js"></script>
<div class="demo-shell">
<div class="demo-intro">
<h1>Grid Pro with one-request server-side products</h1>
<p>
This demo uses the public Northwind OData API. Sorting, filtering,
and pagination all happen on the server, and each interaction is
handled with one request. Nice and tidy, like a query string that
actually knows where it is going.
</p>
</div>
<div id="notice" class="notice" aria-live="polite">
Loading products from Northwind...
</div>
<div id="container"></div>
<p class="highcharts-description">
Click a header to sort, or shift-click a second one for multi-column
sorting. Use the filter icons in the headers to send matching OData
filters to the server.
</p>
</div>
CSS
@import url("https://cdn.jsdelivr.net/npm/@highcharts/grid-pro/css/grid-pro.css");
body {
font-family:
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Helvetica,
Arial,
sans-serif;
background:
linear-gradient(180deg, #f7fafc 0%, #edf7f4 100%);
color: #1f2937;
margin: 0;
}
.demo-shell {
max-width: 1120px;
margin: 0 auto;
padding: 20px 16px 28px;
}
.demo-intro {
margin-bottom: 14px;
}
.demo-intro h1 {
margin: 0 0 8px;
font-size: 1.8rem;
line-height: 1.1;
}
.demo-intro p,
.highcharts-description {
margin: 0;
max-width: 78ch;
line-height: 1.55;
}
.notice {
margin: 0 0 12px;
padding: 10px 12px;
border: 1px solid #cfe7df;
border-radius: 10px;
background: #f1fbf8;
color: #1f5e52;
}
.notice[data-level="error"] {
border-color: #f0b4b4;
background: #fff1f1;
color: #8b1e1e;
}
#container {
min-height: 640px;
max-height: 720px;
}
.highcharts-description {
padding-top: 12px;
}
JavaScript
const API_BASE = 'https://services.odata.org/V4/Northwind/Northwind.svc/Products';
const COLUMN_IDS = [
'ProductID',
'ProductName',
'QuantityPerUnit',
'UnitPrice',
'UnitsInStock',
'Discontinued'
];
const COLUMN_TYPES = {
ProductID: 'number',
ProductName: 'string',
QuantityPerUnit: 'string',
UnitPrice: 'number',
UnitsInStock: 'number',
Discontinued: 'boolean'
};
const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
const noticeEl = document.getElementById('notice');
function setNotice(message, level) {
noticeEl.textContent = message;
noticeEl.dataset.level = level || 'info';
}
function getActiveSortings(query) {
const sortings = query.sorting.currentSortings ||
(query.sorting.currentSorting ? [query.sorting.currentSorting] : []);
return sortings.filter((sorting) => sorting?.columnId && sorting.order);
}
function getFilterRootCondition(state) {
return state.query.filtering.modifier?.options?.condition;
}
function escapeODataString(value) {
return String(value).replace(/'/g, "''");
}
function normalizeNumericValue(value) {
const normalized = typeof value === 'number' ? value : Number(value);
return Number.isFinite(normalized) ? String(normalized) : '';
}
function buildOrderBy(state) {
const orderBy = getActiveSortings(state.query)
.filter((sorting) => COLUMN_TYPES[sorting.columnId])
.map((sorting) => `${sorting.columnId} ${sorting.order}`)
.join(',');
return orderBy ? encodeURIComponent(orderBy) : '';
}
function buildEmptyCondition(columnId, columnType) {
if (columnType === 'string') {
return `(${columnId} eq null or ${columnId} eq '')`;
}
return `${columnId} eq null`;
}
function buildStringCondition(columnId, operator, value) {
const normalizedValue = String(value || '');
if...