developer guide
OData adapter
Point the grid at an OData service and it pushes filtering, sorting, paging and the count to the endpoint, so an enterprise feed of any size is answered server-side.
A grid over an OData feed, with nothing to write
OData was built for exactly this: filter, order, top, skip and count all travel in the URL, which is why the grid needs no negotiation and no server of ours to sit in front of one. Point the adapter at an entity-set URL and the grid turns each sort, filter and scroll into an OData query, sends it, and shows the rows that come back. A feed of any size stays fast because the service answers the query rather than the browser thinning a download.
import { createGrid, createPushdownSource, odataAdapter } from '@toclocoinc/lattice-grid';
import * as compute from '@toclocoinc/lattice-grid';
createGrid(el, {
columns,
rowKey: 'OrderID',
source: createPushdownSource({
adapter: odataAdapter({
url: 'https://services.odata.org/V4/Northwind/Northwind.svc/Orders',
}),
compute, // the grid's own kernels, for anything the service did not do
pageSize: 100,
}),
});
The grid stays a client library. The adapter builds a URL and reads JSON, and imports nothing, so the bundle is the same whether a page uses it or not.
What pushes down to the service
This is the honest version, because the value of a server source is exactly which half of the query reaches the server. The OData adapter pushes:
| Query part | Pushed as | Notes |
|---|---|---|
| Filtering | $filter | The full condition tree, with and and or groups. |
| Sorting | $orderby | Multi-column, in order. |
| Paging | $skip and $top | The grid asks for the window it needs. |
| Count | $count=true | The exact match count, for a true scrollbar. On by default. |
| Quick filter | $search | Only when you pass search: true, because not every service implements it. |
// The URL the adapter builds for a filtered, sorted page:
GET /Orders?$filter=(Country eq 'Germany' and Freight gt 50)
&$orderby=OrderDate desc
&$skip=100&$top=100
&$count=true
The operators pushed into $filter are the ones OData v4 expresses directly: equals and not
equals, the four inequalities, contains, startsWith and endsWith,
and blank and not-blank as eq null and ne null. A filter that uses only these
is answered entirely by the service.
What runs in the browser
Aggregation does not push down. The adapter does not declare grouping, so when the grid is grouped or totalled, the grouping and the totals are computed in the browser over the rows the service returns, not by the service. A filter using an operator OData cannot express is split: the supported conjuncts go to the service and the remainder is applied locally after the fetch.
When anything is left for the browser to finish, the grid stops asking for a window and asks for the
whole matching result, because filtering or sorting a single page locally would return the wrong rows.
That is correct but slower, and it is said out loud: the source warns once, naming the part it could not
push. And if the adapter is ever handed a fraction of the result it asked for while there is residual
work to do, it refuses the result and surfaces an error rather than filtering a fraction and reporting it
as the whole. A wrong answer is never preferred to a visible failure. You can see the split after every
query with source.lastPlan().
Server-driven paging is followed
When the whole result is needed and the service applies its own page size, it says so with an
@odata.nextLink rather than by failing. The adapter follows that link until the result is
complete, so "everything" means everything, up to a ceiling of two hundred pages, at which point a
filter that matches far more than expected fails loudly rather than fetching a table forever.
Authentication and options
Authenticate with a header, or pass a custom fetch for a token that refreshes. Turn the
count off if your service does not support $count, and turn search on only if
it implements $search.
odataAdapter({
url: 'https://your-service/odata/Orders',
headers: { Authorization: 'Bearer ' + token }, // a static header
// ...or a custom fetch, for a token that refreshes:
fetch: (url, init) => authedFetch(url, init),
count: true, // default; sends $count=true so the total is exact
search: true, // only if your service implements $search
})
See it running: pushdown to a live OData service, which drives a
public service and shows lastPlan() as you filter. The
pushdown guide covers the split rules that apply across every adapter, and
connect your data lists the other options.