vue-shapeshifter-table
v1.1.0
Published
A responsive Vue 3 table with inline editing, sortable and draggable columns, search, pagination, saved column order, and TypeScript support.
Maintainers
Readme
Vue Shapeshifter Table
Saved column order (1.1.0)
Use applyColumnOrder after loading table data to restore a user's preferred order. It returns new headers and rows arrays with matching column positions; it does not read browser storage or modify your inputs.
import { applyColumnOrder } from 'vue-shapeshifter-table'
// Save only keys after the user chooses to save their preferences.
function saveOrder() {
try {
localStorage.setItem('my-table:column-order:v1', JSON.stringify(headers.value.map(h => h.key)))
} catch {
// Storage can be unavailable or full. Keep the table usable.
}
}
// In onMounted (or after fetching data), so SSR does not access localStorage.
function restoreOrder() {
try {
const order = JSON.parse(localStorage.getItem('my-table:column-order:v1') ?? 'null')
const restored = applyColumnOrder(headers.value, rows.value, order)
headers.value = restored.headers
rows.value = restored.rows
} catch {
// Malformed saved JSON or an invalid schema: retain the current table.
}
}Headers must have unique string or finite numeric keys. Saved keys for removed columns and duplicate/invalid saved entries are ignored. New columns are appended in their current schema order. Non-array preferences leave the order unchanged. Numeric 0 and string '0' are distinct keys. Short rows keep empty positions, and extra cells beyond the schema are retained. Header/cell objects are shallow copies; nested custom metadata remains shared.
Restore only after loading the matching row data. Persist keys rather than row contents; the library makes no storage or network calls. Reset by restoring your default key list and removing your own storage key. Column widths are not currently supported. The demo's explicit save/reset controls illustrate this flow.
Version 1.1.0 also includes pagination, drag-and-drop, sorting/search, and TypeScript declarations. See CHANGELOG.md for the release contents. Publishing to npm is a separate authenticated step; a version in this repository does not confirm registry availability.
Sorting and search
Enable sortable for a separate sort button on each heading and filterable for global search. Both are off by default. They work with pagination and column dragging without adding dependencies:
<ShapeShifterTable
v-model:headers="headers"
v-model:table-data="rows"
v-model:sort="sort"
v-model:filter="filter"
sortable filterable pagination
:page-size="10"
/>Initialize sort as ref<TableSort | null>(null) (import TableSort from the package) and filter as ref(''). In JavaScript, use ref(null) for sort. Bindings are optional; internal state also works. sort has the shape { key: 'name', direction: 'asc' }, using a stable header key. Clicking the sort button cycles ascending → descending → original order; it emits update:sort. Search emits update:filter with the entered text.
Search uses a trimmed, case-insensitive substring across columns defined by the headers. It matches string, number, boolean, and bigint cell fields; missing cells and objects are treated as empty. Sorting is stable, compares finite numbers numerically, and otherwise uses English natural text order (so Item 2 precedes Item 10). Empty and unsupported values stay last in either direction. No custom comparator or per-column filters are provided in this release.
The view applies filter → sort → paginate. Sorting and filtering never reorder or trim the parent row array. Slot indices and edit/delete payloads keep their absolute source indices. Changing criteria returns to page 1; pagination totals show matching rows, while the main footer still reports the full dataset. Sort selection follows the header key during dragging and clears when that column is removed. Disable either feature to ignore its stored criteria.
Draft edits do not affect sorting or filtering until committed. After saving, a row can move or stop matching the search. Searching, sorting, or navigating cancels any editor still open; ordinary blur saves as before. All rows remain in memory: this is client-side sorting and filtering.
TypeScript
The package includes declarations for the named component, default plugin, props, events, and slots. No separate @types package is needed. Use Vue 3.3+ and TypeScript 5+ with moduleResolution: "Bundler" or "NodeNext".
import { ref } from 'vue'
import { ShapeShifterTable, type TableHeader, type TableRow, type CellUpdate } from 'vue-shapeshifter-table'
const headers = ref<TableHeader[]>([{ key: 'name', field: 'Name' }])
const rows = ref<TableRow[]>([[{ key: 'ada', field: 'Ada' }]])
function onCellUpdate(event: CellUpdate) {
console.log(event.rowIndex, event.columnIndex, event.value)
}Additional exported types include ShapeShifterTableProps, TableCell, TableKey, TableFooter, TableMenuItem, TableSlots, TableEventPayloads, TableEmits, HeaderUpdate, ColumnChange, RowChange, ColumnMove, and ContextEvent.
Cell fields and custom metadata are unknown: narrow or format them before use. Edits normally produce strings, but blurring an unchanged field preserves its original value. Short rows may have missing cells, so slot consumers should use cell?.field. All event/slot row indices refer to the complete dataset, including when paginated. The plugin registers the component at runtime; import the named component in typed SFCs for template inference.
Run npm run test:types for local Vue consumer checks. npm test also extracts the npm tarball and checks TypeScript and Vue consumers against its exported declarations using both Bundler and NodeNext resolution. The declarations add no JavaScript or runtime dependencies.
Drag-and-drop columns
Enable draggable-columns on ShapeShifterTable to display drag handles. Drag a handle onto another heading to move its column to that position; the target heading is highlighted. Pointer events support mouse, touch, and pen. Moving a column also moves its cells in every row, including rows hidden by pagination.
Focus a handle and press Left or Right to move using the keyboard. Escape, pointer cancellation, or dropping outside this table cancels a drag. The existing Move left/right menu actions remain available. Reordering emits update:headers, update:tableData, and move-column with zero-based { from, to } indices. Bind both data models to retain changes.
Dragging is off by default and adds no dependencies. This implementation does not auto-scroll the table during a drag; use the move buttons or scroll before dragging to a distant column.
Pagination
Pagination is optional and off by default. Enable it to render a page of the supplied rows while retaining the full array in v-model:table-data:
<ShapeShifterTable
v-model:headers="headers"
v-model:table-data="rows"
v-model:page="page"
v-model:page-size="pageSize"
pagination
:page-size-options="[10, 25, 50]"
/>Initialize page with ref(1) and pageSize with ref(10). These bindings are optional: the component also maintains page state internally. page is one-based; pageSize defaults to 10. Invalid numbers fall back to 1 and 10 respectively. The current page size is always included in the selector.
Changing the page size returns to page 1. Deleting rows or replacing the dataset clamps the page to the last available page. Empty tables show page 1 of 1 with navigation disabled. Added rows are appended to the dataset without moving the current page. Cell events and slot rowIndex values always refer to the full dataset, not the visible page. Navigation cancels any draft still open; ordinary input blur saves edits as before.
This is client-side pagination, not server-side fetching or virtualization: all supplied rows remain in memory. No additional runtime dependencies are required.
A lightweight, editable table component for Vue 3.
Edit cells and headings, add or remove rows and columns, and move columns together with their data. Includes customization slots, sticky headings, horizontal scrolling, and compact spacing. Vue 3.3 or newer is required. The package is ESM-only and includes its own CSS; no additional UI framework is required.
Quick start · API · Examples · Development · Demo · License
Install
npm install vue-shapeshifter-tableRegister the plugin globally:
import { createApp } from 'vue'
import ShapeshifterTable from 'vue-shapeshifter-table'
import 'vue-shapeshifter-table/style.css'
import App from './App.vue'
createApp(App).use(ShapeshifterTable).mount('#app')Or import the component directly:
<script setup>
import { ref } from 'vue'
import { ShapeShifterTable } from 'vue-shapeshifter-table'
import 'vue-shapeshifter-table/style.css'
const headers = ref([
{ field: 'Name', key: 'name', editable: true },
])
const rows = ref([
[{ field: 'Maya', key: 'maya-name', editable: true }],
])
</script>
<template>
<ShapeShifterTable v-model:headers="headers" v-model:table-data="rows" />
</template>Table API
Use v-model:headers and v-model:table-data to keep parent data synchronized with edits. The component edits local copies and emits replacement arrays; it no longer mutates the supplied header and cell objects. One-way props still display data, but parents must handle the update events to retain changes.
Headers use { field, key, editable }; rows are arrays of cell objects in header order. Give headers and cells unique, stable keys. Set editable: false to make a heading or cell read-only. Click an editable value to edit it, then press Enter or leave the input to save; Escape discards the draft.
| Prop | Default | Purpose |
| --- | --- | --- |
| headers, tableData | [] | Column definitions and rows. |
| footers | [] | Footer objects whose field values are displayed. |
| contextMenuColumn, contextMenuRow | [] | Custom actions using { text, event }. |
| title, eyebrow | '' | Optional toolbar heading and label. |
| emptyText | 'Your table is ready' | Empty-state heading. |
| maxHeight | '34rem' | Maximum height of the scrollable table. |
| stickyHeader | true | Keep column headings visible while scrolling. |
| addable, removable | true | Show built-in add and delete controls. |
| compact | false | Reduce cell spacing. |
Column menus move columns left or right together with their cells. Custom menu actions emit context-events with { event, menu_id, type }; consumers implement those custom actions themselves.
| Event | Payload |
| --- | --- |
| update:headers, update:tableData | Replacement header or row array. |
| header-update | { key, value, editKey, columnIndex } |
| cell-update | { key, value, editKey, rowIndex, columnIndex } |
| add-column, delete-column | { header, columnIndex } |
| add-row, delete-row | { row, rowIndex } |
| move-column | { from, to } |
| context-events | { event, menu_id, type } |
| Slot | Slot props |
| --- | --- |
| toolbar | addColumn, addRow |
| header | header, columnIndex |
| cell | cell, header, rowIndex, columnIndex |
| empty | None |
| footer | footers |
Slot props with multiple words are exposed using camelCase names (columnIndex, rowIndex, addColumn, addRow). Custom header and cell slots replace the default editing controls. The table provides scoped styles, horizontal scrolling, and reduced-motion support.
Examples
Data format
Each row is an array in the same order as the headers. The field property is the
displayed value, key identifies the header or cell, and editable defaults to
enabled unless explicitly set to false. Optional editKey metadata is included
in edit events. Prefer plain string or number values; input edits produce strings.
const headers = ref([
{ key: 'name', field: 'Name', editable: false },
{ key: 'role', field: 'Role', editable: true },
])
const rows = ref([
[
{ key: 'maya-name', field: 'Maya', editable: false },
{ key: 'maya-role', field: 'Designer', editable: true },
],
])Changing rows or headers in the parent updates the table. The component copies
header and cell objects before editing; nested custom metadata is not deep-cloned.
Custom cells and toolbar
Use this template with the headers and rows refs from the quick start:
<ShapeShifterTable
v-model:headers="headers"
v-model:table-data="rows"
title="Team"
:addable="false"
>
<template #toolbar="actions">
<button type="button" @click="actions.addRow()">Add person</button>
</template>
<template #cell="{ cell }">
<strong>{{ cell?.field }}</strong>
</template>
<template #empty>No team members yet.</template>
</ShapeShifterTable>This cell slot displays values instead of the built-in editor. addable controls
the default add buttons; toolbar callbacks remain available.
Handling changes and custom actions
<ShapeShifterTable
v-model:headers="headers"
v-model:table-data="rows"
:context-menu-row="[{ text: 'Inspect cell', event: 'inspect' }]"
@cell-update="onCellUpdate"
@context-events="onContextAction"
/>function onCellUpdate({ key, value, rowIndex, columnIndex }) {
console.log('Edited cell', { key, value, rowIndex, columnIndex })
}
function onContextAction({ event, menu_id, type }) {
console.log('Custom action', { event, menu_id, type })
}Row context menus appear on individual cells, so their menu_id is the cell key.
Column context menus supply the header key. Custom actions emit events only;
implement their effects in the parent. Persistence is also the parent's
responsibility: connect updates to your API or storage if needed.
Appearance
<ShapeShifterTable
v-model:headers="headers"
v-model:table-data="rows"
compact
max-height="24rem"
:sticky-header="true"
style="--sst-accent: #047857; --sst-accent-strong: #065f46"
/>The root exposes --sst-accent, --sst-accent-strong, --sst-ink,
--sst-muted, and --sst-line CSS variables. Some decorative colors are fixed.
Always import vue-shapeshifter-table/style.css in the consuming application.
Current limits
The component renders all rows unless pagination is enabled. It does not provide virtualization,
server-side queries, validation, or persistent storage. addable and removable
control UI visibility; they are not authorization rules. Column movement remains
available when more than one column exists, even with both set to false.
Development
Development requires Node.js 20.19 or newer.
npm install
npm run dev
npm test
npm run build
npm run build:demo
npm run previewnpm run build creates the externalized, ESM-only library bundle in dist/. npm run build:demo creates the standalone demonstration site in demo-dist/, and npm run preview serves that site. The two builds keep their output separate.
npm pack rebuilds the library automatically before creating the package archive.
The demo imports the package's public JavaScript and CSS exports. npm run dev
and npm run build:demo first build the library automatically. After editing the
library component during a demo session, run npm run build again to refresh
the consumed bundle.
Publishing
Automatic publishing
.github/workflows/publish.yml publishes new stable package versions automatically
after changes reach main. It also supports Actions → Publish npm package → Run
workflow on main to retry a failed release. Already-published versions are
skipped; registry errors stop the release. Versions must increase and match the
lockfile. Tests, the demo build, the security audit, and a package dry run must pass.
One-time maintainer setup in npm: open this package's Settings → Trusted publishing, choose GitHub Actions, and enter:
- Organization or user:
SaqifHoque - Repository:
vue-shape-shifter-table - Workflow filename:
publish.yml(not the full path) - Environment: leave blank (the workflow does not use a GitHub environment)
- Allowed actions: enable direct publishing with
npm publish
This uses short-lived OIDC credentials, not an NPM_TOKEN secret, and publishes
provenance. See npm trusted publishing.
Protect main with required reviews and checks, especially for workflow changes.
The workflow must be merged into main and the npm trust configured before it
can publish. Merging a version bump authorizes a public release, so review the
package contents and changelog first. The npm package page's README updates with
publication; README-only changes require a new package version to appear there.
For each release, update package.json, package-lock.json, and CHANGELOG.md
in a PR. After merging, check the Actions result and verify the registry with
npm view vue-shapeshifter-table version.
Manual fallback
Before publishing manually:
- Update the version and release notes.
- Run
npm test,npm run build:demo, andnpm run security:audit. - Inspect the package contents with
npm pack --dry-run. - Publish with
npm publishafter signing in to npm.
prepublishOnly runs the test suite and blocks publication when npm reports a
moderate-or-higher vulnerability. Package consumers receive no runtime
dependencies; Vue remains a peer dependency.
Security
Report suspected vulnerabilities privately through the repository's Security tab. See SECURITY.md for supported versions, reporting details, and the component's security design.
Demo
Run npm run dev to explore the product launch board with three sample projects. Try editing cells and headings, adding or deleting rows and columns, and moving columns from their action menus. The toolbar announces each completed action.
The demo uses local state: changes are discarded when you refresh the page. Build it with npm run build:demo and serve it with npm run preview.
License
Licensed under MIT. This permits commercial use, modification, and redistribution, provided the copyright and license notices are retained. The software is provided without warranty. See the MIT license text.
