Save and restore table view configurations with localStorage persistence for Svelte applications
A lightweight, framework-agnostic Svelte package that lets users save, manage, and restore table configurations (filters, sorting, column order, visibility) as named "views". Perfect for data-heavy applications where users need to switch between different table configurations quickly.
npm install svelte-table-views
<script lang="ts">
import { ViewSelector, SaveViewModal, viewActions, activeViewId, activeViewModified } from 'svelte-table-views'
import type { TableConfig, SavedView } from 'svelte-table-views'
let showSaveModal = false
let capturedConfig: TableConfig | null = null
// Your table state
let filters = []
let sort = null
let columns = ['id', 'name', 'email']
let columnOrder = ['id', 'name', 'email']
</script>
<div class="flex items-center justify-between gap-4 mb-4">
<!-- View Selector Dropdown -->
<ViewSelector on:viewSelected={handleViewSelected} />
<!-- Save/Update Button -->
{#if $activeViewId && $activeViewModified}
<!-- Split button when view is modified -->
<div class="inline-flex">
<button on:click={handleUpdateView}>Update View</button>
<button on:click={openSaveModal}>Save New</button>
</div>
{:else}
<button on:click={openSaveModal}>Save View</button>
{/if}
</div>
<!-- Your Table Component -->
<YourTable {filters} {sort} {columns} {columnOrder} />
<!-- Save View Modal -->
{#if showSaveModal && capturedConfig}
<SaveViewModal
bind:open={showSaveModal}
config={capturedConfig}
on:save={handleViewSaved}
/>
{/if}
function openSaveModal() {
capturedConfig = {
filters,
sort,
columns,
columnOrder,
columnWidths: {},
pageSize: 25
}
showSaveModal = true
}
async function handleViewSelected(event: CustomEvent<{ view: SavedView }>) {
const view = event.detail.view
// Apply saved config to your table
filters = view.config.filters
sort = view.config.sort
columns = view.config.columns
columnOrder = view.config.columnOrder
}
async function handleUpdateView() {
if ($activeViewId) {
await viewActions.update($activeViewId, {
config: {
filters,
sort,
columns,
columnOrder,
columnWidths: {},
pageSize: 25
}
})
}
}
function handleViewSaved(event: CustomEvent<{ id: string; name: string }>) {
console.log('View saved:', event.detail.name)
}
<ViewSelector>Dropdown component for selecting, searching, renaming, and deleting saved views.
Props:
Events:
viewSelected: CustomEvent<{ view: SavedView }> - Fired when user selects a viewdeleteView: CustomEvent<{ id: string }> - Fired when user deletes a viewFeatures:
<SaveViewModal>Modal component for saving new table views.
Props:
open: boolean - Controls modal visibility (use bind:open)config: TableConfig - Table configuration to saveoriginalQuery?: string - Optional: original NL query that generated this configEvents:
save: CustomEvent<{ id: string; name: string }> - Fired when view is savedFeatures:
savedViewsWritable store containing all saved views.
import { savedViews } from 'svelte-table-views'
$savedViews // SavedView[]
recentViewsDerived store containing recent views (last 7 days, top 5, sorted by lastUsed).
import { recentViews } from 'svelte-table-views'
$recentViews // SavedView[]
activeViewIdWritable store tracking the currently active view ID.
import { activeViewId } from 'svelte-table-views'
$activeViewId // string | null
activeViewModifiedWritable store tracking whether the active view has been modified.
import { activeViewModified } from 'svelte-table-views'
$activeViewModified // boolean
activeViewDerived store containing the full active view object.
import { activeView } from 'svelte-table-views'
$activeView // SavedView | null
viewActions.save(input: SavedViewInput): Promise<SavedView>Save a new view.
const newView = await viewActions.save({
name: 'High Priority Items',
description: 'Items with priority > 5',
config: {
filters: [{ columnId: 'priority', operator: 'greaterThan', value: 5 }],
sort: { columnId: 'createdAt', direction: 'desc' },
columns: ['id', 'name', 'priority'],
columnOrder: ['priority', 'name', 'id'],
columnWidths: {},
pageSize: 25
}
})
viewActions.load(id: string): Promise<SavedView | undefined>Load an existing view. Updates usage statistics and sets as active.
const view = await viewActions.load('view-id-123')
viewActions.update(id: string, updates: Partial<SavedView>): Promise<void>Update an existing view.
await viewActions.update('view-id-123', {
config: updatedConfig,
description: 'Updated description'
})
viewActions.delete(id: string): Promise<void>Delete a view.
await viewActions.delete('view-id-123')
viewActions.rename(id: string, newName: string): Promise<void>Rename a view.
await viewActions.rename('view-id-123', 'New View Name')
viewActions.markModified(): voidMark the active view as modified (shows split button).
viewActions.markModified()
viewActions.clearActive(): voidClear the active view.
viewActions.clearActive()
viewActions.nameExists(name: string, excludeId?: string): Promise<boolean>Check if a view name already exists.
const exists = await viewActions.nameExists('My View')
viewActions.getStorageStats(): Promise<{ count: number; limit: number; percentFull: number }>Get storage usage statistics.
const stats = await viewActions.getStorageStats()
console.log(`${stats.count}/${stats.limit} views (${stats.percentFull}% full)`)
TableConfiginterface TableConfig {
filters: FilterCondition[]
sort: SortConfig | null
columns: string[]
columnOrder: string[]
columnWidths: Record<string, number>
pageSize: number
grouping?: string[]
}
FilterConditioninterface FilterCondition {
columnId: string
operator: string
value: any
}
SortConfiginterface SortConfig {
columnId: string
direction: 'asc' | 'desc'
}
SavedViewinterface SavedView {
// Identity
id: string
name: string
description?: string
// Configuration
config: TableConfig
// Optional: original NL query for reference
originalQuery?: string
// Metadata
createdAt: number
updatedAt: number
usageCount: number
lastUsed: number
}
SavedViewInputtype SavedViewInput = Omit<SavedView, 'id' | 'createdAt' | 'updatedAt' | 'usageCount' | 'lastUsed'>
The package automatically validates columns when loading a view:
function handleViewSelected(event: CustomEvent<{ view: SavedView }>) {
const view = event.detail.view
const availableColumns = ['id', 'name', 'email', 'created_at']
// Filter out missing columns
const validColumns = view.config.columns.filter(col =>
availableColumns.includes(col)
)
// Warn user if columns are missing
const missingColumns = view.config.columns.filter(col =>
!availableColumns.includes(col)
)
if (missingColumns.length > 0) {
alert(`Some columns no longer exist: ${missingColumns.join(', ')}`)
}
// Apply valid config
columns = validColumns
// ... rest of config
}
The default localStorage key is 'svelte_table_views_saved_views'. To customize it, fork the package and modify src/lib/stores/saved-views.ts:
const STORAGE_KEY = 'my_app_saved_views'
The default limit is 50 views. To change it, modify the getStorageStats function:
async getStorageStats() {
const views = get(savedViews)
const count = views.length
const limit = 100 // Change this
return { count, limit, percentFull: Math.round((count / limit) * 100) }
}
The package uses Tailwind CSS classes. If you're not using Tailwind, you have two options:
npm install -D tailwindcss
npx tailwindcss init
Target the component classes in your global CSS:
/* Override ViewSelector styles */
.view-selector button {
/* Your styles */
}
/* Override SaveViewModal styles */
.save-view-modal {
/* Your styles */
}
crypto.randomUUID() supportContributions are welcome! Please read CONTRIBUTING.md before submitting PRs.
MIT © Jason (Shotley Builder)
See CHANGELOG.md for version history.