Advanced

Saved Views

Named views kept in localStorage or storage of your own, and a link that carries a whole grid state. Packing a wide grid can pass what a URL holds, so sharing refuses rather than handing back a link that will not open.

Basic Usage

A view is a name over a snapshot, so it captures whatever the registered features serialize: the sort, the filters, which columns are hidden or pinned. Register a feature and its slice joins the view without anything being wired for it.

A pivot joins it the same way, with its axes, its measures and its settings. A view saved from a flat grid carries that too, so applying it turns a crossing off, and the same holds for grouping.

Sort a column or hide one below, save the result, change the grid again, and switch back.

Views No saved views
0 views no view open
Hoang Kowalski
Design
Manager
$127,691.00
Bruno Nguyen
Growth
Support
$105,146.00
Bruno Dubois
Design
Manager
$86,538.00
Farid Haddad
Growth
Analyst
$76,443.00
Jonas Yilmaz
Core
Support
$129,812.00
Quyen Tanaka
Growth
Analyst
$72,011.00
Mai Nguyen
Support
Analyst
$83,226.00
Bruno Novak
Support
Analyst
$71,507.00
Grace Dubois
Support
Manager
$94,212.00
Tuan Ivanov
Design
Engineer
$105,520.00
Bruno Andersen
Growth
Support
$62,389.00
Bruno Yilmaz
Core
Designer
$139,212.00
Grace Costa
Platform
Support
$96,734.00
Niko Muller
Core
Manager
$76,769.00
Olga Yilmaz
Platform
Analyst
$66,068.00
Dara Okafor
Platform
Support
$87,888.00
Quyen Silva
Design
Manager
$121,817.00
Grace Haddad
Support
Designer
$137,633.00
Keiko Novak
Core
Analyst
$125,071.00
Tuan Dubois
Growth
Engineer
$73,645.00
Chi Kowalski
Support
Analyst
$121,177.00
Chi Silva
Design
Designer
$130,525.00
Lucas Muller
Core
Support
$59,734.00
Hoang Rossi
Core
Support
$130,572.00
Grace Dubois
Growth
Engineer
$87,488.00
Ines Dubois
Core
Designer
$63,756.00
Tuan Dubois
Growth
Support
$93,991.00
Mai Silva
Support
Manager
$116,879.00
Dara Haddad
Platform
Analyst
$119,508.00
Tuan Haddad
Support
Support
$130,078.00
30 rows
<script lang="ts">
  import { createDataGrid, DataGrid, Grid, savedViews } from '@sv5ui/datagrid';

  const grid = createDataGrid<Person>({
    columns, data, getRowId,
    features: [sorting(), filtering(), columnOps(), savedViews()]
  });
</script>

<Grid.SavedViews {grid} />
<DataGrid {grid} toolbar class="h-96" />

<!-- A view is a name over a snapshot, so it captures whatever the registered
     features serialize: the sort, the filters, which columns are hidden,
     grouped or pinned, the formatting rules. Register a feature and its slice
     joins the view without anything being wired. -->

Where Views Live

localStorageViews is the default, and it is also the shape to copy for a store of your own: a read and a write, nothing more.

import { localStorageViews, savedViews } from '@sv5ui/datagrid';

savedViews();                              // localStorage, under a default key
savedViews({ key: 'orders-grid' });        // one key per grid
savedViews({ storage: null });             // in memory only, nothing persisted
savedViews({ views: seeded });             // ship views with the app

// Keeping them on a server instead is two methods:
const serverViews: SavedViewStorage = {
  read: (key) => cache.get(key) ?? null,
  write: (key, views) => { cache.set(key, views); void save(key, views); }
};
savedViews({ storage: serverViews, onChange: (views) => sync(views) });

// read is synchronous, because the grid asks for the views while it is being
// built. Hand it what you already have and write through to the network in the
// background, as above, rather than making the grid wait.

Sharing a State as a Link

The whole grid state, deflated and packed into one URL parameter. Sort or filter the grid below, then build a link and read what comes out.

Hoang Kowalski
Design
Manager
$127,691.00
Bruno Nguyen
Growth
Support
$105,146.00
Bruno Dubois
Design
Manager
$86,538.00
Farid Haddad
Growth
Analyst
$76,443.00
Jonas Yilmaz
Core
Support
$129,812.00
Quyen Tanaka
Growth
Analyst
$72,011.00
Mai Nguyen
Support
Analyst
$83,226.00
Bruno Novak
Support
Analyst
$71,507.00
Grace Dubois
Support
Manager
$94,212.00
Tuan Ivanov
Design
Engineer
$105,520.00
Bruno Andersen
Growth
Support
$62,389.00
Bruno Yilmaz
Core
Designer
$139,212.00
Grace Costa
Platform
Support
$96,734.00
Niko Muller
Core
Manager
$76,769.00
Olga Yilmaz
Platform
Analyst
$66,068.00
Dara Okafor
Platform
Support
$87,888.00
Quyen Silva
Design
Manager
$121,817.00
Grace Haddad
Support
Designer
$137,633.00
Keiko Novak
Core
Analyst
$125,071.00
Tuan Dubois
Growth
Engineer
$73,645.00
Chi Kowalski
Support
Analyst
$121,177.00
Chi Silva
Design
Designer
$130,525.00
Lucas Muller
Core
Support
$59,734.00
Hoang Rossi
Core
Support
$130,572.00
Grace Dubois
Growth
Engineer
$87,488.00
Ines Dubois
Core
Designer
$63,756.00
Tuan Dubois
Growth
Support
$93,991.00
Mai Silva
Support
Manager
$116,879.00
Dara Haddad
Platform
Analyst
$119,508.00
Tuan Haddad
Support
Support
$130,078.00
30 rows
import { getSavedViews, ShareTooLongError } from '@sv5ui/datagrid';

const views = getSavedViews(grid)!;

// The whole grid state, deflated and packed into one URL parameter.
const link = await views.shareLink();          // the current page's URL
const link2 = await views.shareLink('https://app.test/orders');
const token = await views.shareToken();        // just the parameter value

// And back in, on the receiving page:
await views.applyLink();          // reads window.location
await views.applyLink(href);
await views.applyToken(token);

// A token that is damaged or from another version reads back as no change
// rather than throwing, so a stale link in a chat window is harmless.

When a Link Will Not Fit

A URL cannot carry an unbounded amount, so shareLink and shareToken refuse rather than hand back something that will not open. The limit is 1800 characters of encoded state.

The state is deflated first, so what matters is how compressible it is rather than how much of it there is. These are measured in your browser as the page loads, on the same grid the demo above shares:

A set filter withEncodes to
Measuring...

So it takes a set filter with hundreds of distinct values ticked to reach the ceiling. A sort, a few column filters, and even a set filter of a hundred and fifty addresses all fit with room to spare; six hundred repetitions of one word cost almost nothing at all, because the state is deflated before it is counted.

try {
  const link = await views.shareLink();
  if (link) await navigator.clipboard.writeText(link);
} catch (error) {
  if (error instanceof ShareTooLongError) {
    toast(`${error.length} characters will not fit a link. Save it as a view.`);
  } else {
    throw error;
  }
}

// The state is deflated first, so what matters is how compressible it is
// rather than how much of it there is. Against the 1800 character limit:
//
//   600 repeated values ticked    1759, fits
//   100 distinct values ticked    1379, fits
//   150 distinct values ticked    2013, refused

Reading and Driving It

From getSavedViews(grid). The common calls are on grid.api as well.

MemberWhat it does
viewsSavedView[], each an id, a name and a snapshot
active / activeIdThe open view, or null
modifiedTrue once the grid has moved away from the open view, null when none is open. What a Save button reads to know whether it has anything to do
save(name)Capture the grid as it is now
update(id)Overwrite that view with the grid as it is
rename(id, name) / remove(id)The rest of the list management
apply(id) / revert()Open a view, or drop the changes made since one was opened
shareLink(base?) / shareToken()The whole state as a URL, or as the parameter alone. Both can refuse
applyLink(href?) / applyToken(token)Read one back in. A damaged token is no change rather than a throw

Options

OptionDescription
viewsViews shipped with the app, merged with whatever storage holds
storageWhere views live. null keeps them in memory only
keyThe storage key, so two grids do not share one list
onChangeCalled whenever the list changes, for writing through to a server

Views Over Rows on a Server

A view holds state, never rows, and that is what makes it work over a server row model at all: a sort and a set of filters are exactly what the grid turns into a request. Applying one asks the server again rather than rearranging rows the browser does not have.

Two views are seeded below. Apply one and the row count changes to whatever the server says matched. It is also why a shared link stays the same size over five thousand rows as over five million: the link carries the question, not the answer.

Watch the log while you switch between them. Either view costs one request, whether it carries a sort alone or a sort and a filter together: a snapshot is applied a slice at a time, and the row model folds the changes into a single fetch at the end of the tick rather than asking once per slice and drawing the answers in between.

Views
POST /datagrid/api/people ? of 5,000 matched

No data

0 rows

The last few requests

  • Nothing yet. Apply a view.
const grid = createDataGrid<Person>({
  columns, data: [], getRowId,
  rowModel: 'server',
  features: [
    sorting(), filtering(), columnOps(),
    savedViews({ key: 'people-grid' }),
    virtualization({ rowHeight: 40, overscan: 6 }),
    serverRowModel(source, { mode: 'infinite', blockSize: 100, placeholder })
  ]
});

const views = getSavedViews(grid)!;
views.apply(id);   // one request goes out, carrying the view's sort and filters

// A view holds state, never rows. That is what makes it work here at all: a
// sort and a filter are exactly what a server row model turns into a
// request, so applying a view asks the server again rather than rearranging
// rows it does not have.
//
// It is also why a shared link stays small over five thousand rows and over
// five million: the link carries the question, and the rows are wherever
// they were. Nothing in the snapshot is data, so nothing in it leaks.

Driving It From Code

views.views;        // SavedView[]: { id, name, snapshot }
views.active;       // the open view, or null
views.activeId;
views.modified;     // true once the grid has moved away from it, null if none

views.save('Overdue, by owner');   // capture the grid as it is now
views.update(id);            // overwrite that view with the grid as it is
views.rename(id, 'Overdue');
views.apply(id);
views.revert();              // back to the open view, dropping the changes
views.remove(id);

// modified is what a Save button reads to know whether it has anything to do,
// and what the panel marks so a reader is not surprised by losing a change.
// The common calls are on grid.api too, each optional because the grid that
// has them is the one that registered savedViews():
grid.api.saveView?.('Overdue');
grid.api.applyView?.(id);
grid.api.removeView?.(id);
await grid.api.shareGridLink?.();
await grid.api.applyGridLink?.();

// A view holds a snapshot, which is the same shape State Persistence
// documents. So a view survives a version bump the same way, through the
// migrate hook, and a snapshot saved by hand can be handed to a view.