Lattice Grid Buy a licence

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 partPushed asNotes
Filtering$filterThe full condition tree, with and and or groups.
Sorting$orderbyMulti-column, in order.
Paging$skip and $topThe grid asks for the window it needs.
Count$count=trueThe exact match count, for a true scrollbar. On by default.
Quick filter$searchOnly 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.