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.
<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. -->storage: null, which keeps the views in memory. In your own app, leaving storage out gives you localStorage instead. 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.
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 with | Encodes 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, refusedShareTooLongError, read length off it, and offer a saved view instead. Reading and Driving It
From getSavedViews(grid). The common calls are on grid.api as well.
| Member | What it does |
|---|---|
views | SavedView[], each an id, a name and a snapshot |
active / activeId | The open view, or null |
modified | True 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
| Option | Description |
|---|---|
views | Views shipped with the app, merged with whatever storage holds |
storage | Where views live. null keeps them in memory only |
key | The storage key, so two grids do not share one list |
onChange | Called 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.
No data
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.