Saved views (localStorage)

Per-user grid layouts. Each "view" snapshots column widths, pinning, sort, filter, page index, and (optionally) grouping. Switching views restores the snapshot.

The examples on this page run against these rows:

<script lang="ts">
  import { SvGrid, type GridColumns, tableFeatures, rowSortingFeature, columnFilteringFeature } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    email: string
    department: string
    age: number
    salary: number
    city: string
    startDate: string
    active: boolean
  }

  type Order = {
    id: string
    customer: string
    product: string
    quantity: number
    total: number
    status: 'pending' | 'shipped' | 'delivered'
    orderedAt: string
  }

  const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   email: '[email protected]',   department: 'Engineering', age: 36, salary: 142000, city: 'London',   startDate: '2021-03-01', active: true },
    { id: 2, name: 'Grace Hopper',   email: '[email protected]', department: 'Engineering', age: 45, salary: 168000, city: 'New York', startDate: '2019-07-15', active: true },
    { id: 3, name: 'Linus Torvalds', email: '[email protected]', department: 'Platform',    age: 54, salary: 155000, city: 'Portland', startDate: '2020-01-20', active: false },
    { id: 4, name: 'Radia Perlman',  email: '[email protected]', department: 'Networking',  age: 49, salary: 161000, city: 'Seattle',  startDate: '2022-09-05', active: true },
    { id: 5, name: 'Barbara Liskov', email: '[email protected]', department: 'Platform',  age: 52, salary: 172000, city: 'Boston',   startDate: '2018-11-11', active: true },
  ]

  let rows = $state<Person[]>(people)

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 200 },
    { field: 'department', header: 'Department', width: 150 },
    { field: 'city',       header: 'City',       width: 140 },
    { field: 'age',        header: 'Age',        width: 90 },
    { field: 'salary',     header: 'Salary',     width: 130, format: { type: 'currency', currency: 'USD' } },
  ]
</script>
<script lang="ts">
  import { SvGrid, type SvGridApi } from '@svgrid/grid'

  type View = {
    name: string
    widths:  Record<string, number>
    pinning: { left: string[]; right: string[] }
    sort:    Array<{ id: string; desc: boolean }>
    filters: Record<string, { operator: string; value: string; valueTo?: string }>
  }

  const KEY = 'svgrid:views'
  let api = $state<SvGridApi<typeof features, Order> | null>(null)
  let views = $state<View[]>(JSON.parse(localStorage.getItem(KEY) ?? '[]'))

  function save(name: string) {
    if (!api) return
    const v: View = {
      name,
      widths:  api.getColumnWidths(),
      pinning: api.getColumnPinning(),
      sort:    [], // hook into your sort state
      filters: api.getFilters(),
    }
    views = [...views.filter((x) => x.name !== name), v]
    localStorage.setItem(KEY, JSON.stringify(views))
  }

  function load(v: View) {
    if (!api) return
    for (const [id, w] of Object.entries(v.widths)) api.setColumnWidth(id, w)
    api.setColumnPinning(v.pinning)
    for (const [id, f] of Object.entries(v.filters)) api.setFilter(id, f as never)
  }
</script>

<div class="view-bar">
  {#each views as v (v.name)}
    <button onclick={() => load(v)}>{v.name}</button>
  {/each}
  <button onclick={() => save(prompt('Name?') ?? 'View')}>Save current</button>
</div>

<SvGrid data={rows} columns={columns} features={features}
  onApiReady={(next) => (api = next)} />

GDPR / HIPAA tip

The snapshot stores column STATE only - never row VALUES. If you were to save api.getDisplayedRows() you'd land row data in localStorage; don't. See GDPR + data residency.

Try it

A view is a snapshot you can switch back to. Held in memory here; the only change for a real app is where views is read from and written to.

<script lang="ts">
  type View = { name: string; groupBy: string[]; summary: boolean }

  const views: View[] = [
    { name: 'Flat',          groupBy: [],             summary: false },
    { name: 'By department', groupBy: ['department'], summary: true },
    { name: 'By city',       groupBy: ['city'],       summary: true },
  ]

  let current = $state<View>(views[0]!)
</script>

<div>
  {#each views as view (view.name)}
    <button type="button" aria-pressed={current.name === view.name} onclick={() => (current = view)}>
      {view.name}
    </button>
  {/each}
</div>

<SvGrid
  data={rows}
  {columns}
  groupBy={current.groupBy}
  summary={current.summary}
  groupable
  sortable
/>

See also

Related articles