developer guide
Saved Views in a JavaScript Data Grid
A view captures how the grid is arranged, the sort, the filter, column order and width, grouping and the formatting rules, under a name a reader can return to. Views can live in your own storage or in the browser, and one of them can be the default.
Developer guide › Presenting and saved views › Saved Views in a JavaScript Data Grid
Saved views
A view is a named grid state: sort, filters, grouping, column order, widths, visibility. There are two kinds and the picker keeps them apart.
Views you ship, and views the user saves
views: {
saved: [
{ id: 'escalations', name: 'Escalations',
description: 'Escalated circuits, worst SLA first',
state: {
filters: { col: 'statusId', op: 'eq', value: 4 },
sort: [{ col: 'utilisation', dir: 'desc' }],
}},
{ id: 'commercial', name: 'Commercial', isDefault: true,
state: { columns: [{ id: 'notes', hidden: true }] }},
],
allowSave: true,
}
Views in saved are defined views: part of the application,
listed under their own heading, and neither renamable nor deletable: refused by the model as
well as hidden in the interface. A view flagged isDefault is applied on load.
Everything the user saves sits below, with rename, share, make-default and delete.
Applying a view is a destination, not a patch. A view's state names only the sections it cares about, so applying one resets to the grid's starting state first. Without that, clicking "APAC capacity" after "Commercial" would inherit Commercial's hidden columns, the same view giving a different grid depending on what preceded it, which is the one thing a named view must not do.
When the columns change underneath a saved view
A saved view is user data written months ago against a column set that has since moved on. A release adds columns, renames one, drops another; the views people saved must survive it.
| What changed | What a saved view does |
|---|---|
| A column was added | It appears, in the state its definition declares, placed after every column the view names. A view is not a whitelist, it says nothing about columns it has never seen, and silence is not an instruction to hide. |
| A column was added, and should not appear yet | Declare it layout: { hidden: true }. The view does not mention it, so nothing overrides that, and it stays hidden until the user shows it. |
| A column was removed | The entries naming it are skipped and reported; the rest of the view applies. A sort or grouping on the missing column is dropped rather than left pointing at nothing. |
| A column was renamed | That is a removal and an addition. The old id is skipped, the new column appears at the end, and any width or pinning the user had set is lost with the old id. |
Applying a view never throws and never refuses. It returns a report,
{ applied, skipped }: naming each thing it could not use and why. Refusing the
whole view because one column has gone would lose a layout the user built deliberately, and
throwing during a page load would lose the page. So a view degrades to as much of itself as
still makes sense, and the host decides whether the user needs telling.
Telling the user their view has aged
const report = grid.state.apply(saved.state);
if (report.skipped.length) {
// e.g. [{ key: 'columns.legacyRef', reason: 'unknown column' }]
notify(`This view was saved against an older layout; ${report.skipped.length} setting(s) no longer apply.`);
}
The consequence worth planning for is the first one: a column added in a new release is visible to everyone, including users with a saved view. That is usually what you want (a new field nobody can see is a field nobody uses) but if a release adds several at once, every saved view gains them all at the right-hand end. Ship them hidden if that is not the introduction you want.
Persisting them
The grid makes no network calls. It tells you what happened and you decide what that means.
To a server
grid.on('view:saved', e => api.post('/views', e.view));
grid.on('view:renamed', e => api.patch(`/views/${e.view.id}`, { name: e.view.name }));
grid.on('view:removed', e => api.delete(`/views/${e.view.id}`));
grid.on('view:default', e => api.patch(`/views/${e.view.id}`, { isDefault: true }));
Each event carries the one view that moved, so you send a single record rather than diffing
two lists. Since the grid does not track whether your write landed, catch the failure and
call grid.views.reload().
With no backend at all
createGrid(el, {
views: { local: true, allowSave: true },
});
The other half of the same seam. views.storage above is where
a developer plugs in their own backend, a real server, reached over the network. Not every
grid has one to plug in, and a picker offering "Save" that quietly does nothing until a backend
exists is worse than not offering it. views.local: true is the no-backend answer:
saved views live in this browser's own localStorage, under a default key shared by
every grid on the origin unless you pass one of your own,
views: { local: { key: 'orders-grid-views' } }: to keep two grids' views apart.
Given alongside an explicit storage, the explicit adapter always wins and
local is silently (well, not silently: it warns once) ignored, so a page cannot
end up writing to both without meaning to. The adapter itself is exported as
createLocalViewStorage(opts), for anyone who wants it directly, a custom key
without the shorthand, or a different Storage-shaped backing such as
sessionStorage for views scoped to one tab rather than persisted across visits.
Owners and permissions
Everything above still works with no notion of "who". Set views.user and a shared
view gains an owner, and views.rename, views.save (when it would
overwrite), views.remove and views.unshare all refuse anyone who is
neither that owner nor an admin - a thrown ViewPermissionError, code
views/forbidden, with the store left exactly as it was. Everyone else can still
views.apply it, views.duplicate it into a private copy of their own,
and views.setDefault it for themselves.
createGrid(el, {
views: {
user: () => session.user, // { id, name }; read fresh on every check
isAdmin: (user) => user.role === 'admin',
adminSeesPrivate: false, // default: an admin's edit rights do not imply browsing others' private views
},
});
A host's own button asks the same question the panel does.
views.canEdit(view) is the non-throwing twin: true while views.user is
unset, and otherwise the same owner-or-admin answer views.rename would enforce by
throwing. The saved-views panel calls it to decide whether to offer Rename, Delete and Unshare
on a row at all, so a custom button wired to the same store never disagrees with it.
A view saved before any of this was configured has no recorded owner. It is editable only by an
admin - views.rename, views.remove and views.unshare
refuse everyone else - but applying and duplicating it stay open to all, the same as any other
view. A private view (shared: false) is listed only to its own owner, unless
adminSeesPrivate: true lets an admin see it too; a shared one stays visible to
everyone regardless of who owns it.