@atscript/vue-table
Vue 3 smart table powered by @atscript/ui + @atscript/ui-table. Tier-1 primary components (AsTableRoot, AsTable, AsWindowTable, AsFilters, AsPresetPicker, AsTableActions) are tagged in templates; Tier-2 defaults (cells, dialogs, header cells, filter UI) are swapped via the controls / types / components prop maps. A single useTable() call returns a ReactiveTableState that every Vue piece subscribes to.
Contents
- Tier 1 — Primary components
- Tier 2 — Cells
- Tier 2 — Dialogs
- Tier 2 — Filter UI
- Tier 2 — Headers & rows
- Composables — table root
- Composables — state contracts
- Composables — features
- Composables — presets
- Factories
- Types
- Utilities
Tier 1 — Primary components
AsTableRoot
Sets up useTable() once, provides the context, and renders the table chrome. Most apps tag this at the page level.
Props (excerpt — see UseTableOptions for the full reactive surface):
interface AsTableRootProps {
url: string;
limit?: number;
rowValueFn?: (row: Record<string, unknown>) => unknown;
selectionPersistence?: "clear" | "trim" | "persist";
forceFilters?: FilterExpr;
forceSorters?: SortControl[];
queryFn?: (
query: Uniquery,
page: number,
size: number,
) => Promise<PageResult<Record<string, unknown>>>;
queryOnMount?: boolean;
blockQuery?: boolean;
blockSize?: number;
dragReleaseDebounceMs?: number;
clientFactory?: ClientFactory;
controls?: TAsTableControls;
types?: TAsCellTypeComponents;
components?: Record<string, Component>;
formTypes?: TAsTypeComponents;
formComponents?: Record<string, Component>;
preset?: PresetConfig;
refreshOnAction?: boolean;
urlQuerySync?: UrlQuerySync;
}v-models: urlQuery (string), filterFields (string[]), columnNames (string[]), columnWidths (ColumnWidthsMap), sorters (SortControl[]), selectedRows (unknown[]).
Emits: action(action, ids, result, event?), main-action(row, absIndex, event).
Slot props (default slot, bound from state): tableDef, loadingMetadata, metadataError, allColumns, columnNames, columnWidths, columns, filterFields, filters, sorters, results, querying, queryingNext, totalCount, loadedCount, pagination, queryError, mustRefresh, searchTerm, selectedRows, selectedCount, navBridge, query, queryNext, resetFilters, showConfigDialog, openFilterDialog, closeFilterDialog, setFieldFilter, removeFieldFilter, addFilterField, removeFilterField, actions, prompt.
AsTable
Paginated table renderer. Subscribes to useTableContext(). Use inside <AsTableRoot> or supply your own context.
interface AsTableProps {
rows?: Record<string, unknown>[];
columns?: ColumnDef[];
stickyHeader?: boolean;
virtualRowHeight?: number;
virtualOverscan?: number;
columnMenu?: ColumnMenuConfig;
reorderable?: boolean;
resizable?: boolean;
columnMinWidth?: number;
/** Selection mode: "none" | "single" | "multi". */
select?: SelectionMode;
/** Row-delete opt-in. */
rowDelete?: boolean | RowDeleteOpt;
/**
* Synthesised row-actions pseudo-column. `'first'` / `'last'` prepend or
* append a fixed `__actions` column; `'merge-select'` only renders when
* `select === 'none'` so the row gutter shares space with the multi-select
* checkbox column. `false` (default) hides the column entirely.
*/
rowActionsColumn?: "first" | "last" | "merge-select" | false;
}Emits: row-click(row, event), row-dblclick(row, event), main-action(row, absIndex, event).
AsWindowTable
Windowed (virtualized) renderer for million-row datasets. Uses planFetch / page-aligned blocks under the hood. Same props as AsTable plus rowHeight?: number.
AsFilters
Renders the active-filters bar. Reads state.filterFields and state.filters, emits writes through the same refs.
AsTableActions
Toolbar action row. Reads state.actions and renders default/other actions per level. Slot-customizable.
AsPresetPicker
Dropdown picker showing the active preset, the system presets, the user's saved presets, and the favorite row. Backed by state.preset.
Tier 2 — Cells
Default cell renderers — all implement the standard cell-props contract (path, value, row, column, locale).
| Component | Default cell type | Notes |
|---|---|---|
AsTableCellValue | text, number, boolean, enum, ref | Generic value renderer with locale + value-help labels. |
AsCellNumber | number (when used explicitly) | Decimal-aware numeric cell. |
AsCellDate | date, datetime, relative | Locale-aware date renderer. |
AsCellArray | array | Pill chips for array values. |
AsCellJson | object | Collapsible JSON renderer. |
AsCellUnion | unions | Per-row dispatcher that renders the active union variant. |
Imports:
import {
AsTableCellValue,
AsCellNumber,
AsCellDate,
AsCellArray,
AsCellJson,
AsCellUnion,
} from "@atscript/vue-table";Tier 2 — Dialogs
AsConfigDialog
Tabbed config dialog (columns / filters / sorters). Open via state.configDialogOpen = true.
AsFilterDialog
Per-column filter editor. Open via state.filterDialogColumn = column.
AsPresetDialog
Manage saved presets — rename, delete, public toggle, favorite, default.
AsConfirmDialog
Pure confirm dialog driven by state.prompt(message, opts?). Replaces window.confirm().
AsActionFormDialog
Dialog for actions that declare an @InputForm schema. Opens via state.requestActionInput(action, ctx). Pulls in the full @atscript/vue-form runtime, so it is not exported from the main entry — <AsTableRoot> lazy-mounts it only when an @InputForm action is detected. Import from the dedicated subpath when you need to override or eager-load:
import AsActionFormDialog from "@atscript/vue-table/as-action-form-dialog";Tier 2 — Filter UI
AsFilterField
Single column's filter row inside the filter dialog or active-filters bar.
AsFilterInput
Input control for one condition — switches between text / number / date / boolean / ref based on column type.
Tier 2 — Headers & rows
AsTableHeaderCell
Default header cell — label, sort indicator, resize handle, column menu trigger.
AsColumnMenu
Dropdown menu shown on header click — sort, filter, hide, reset width. Configurable via ColumnMenuConfig.
AsRowActions
Per-row actions cell — single button or … dropdown.
Composables — table root
useTable(url, opts?)
Main entry point. Resolves table metadata, builds the TableDef, wires presets/drafts/selection, and returns the reactive state.
function useTable(url: string, opts?: UseTableOptions): ReactiveTableState;UseTableOptions (key fields):
interface UseTableOptions {
limit?: number;
rowValueFn?: (row: Record<string, unknown>) => unknown;
selectionPersistence?: "clear" | "trim" | "persist";
filterFields?: Ref<string[]>;
columnNames?: Ref<string[]>;
columnWidths?: Ref<ColumnWidthsMap>;
sorters?: Ref<SortControl[]>;
selectedRows?: Ref<unknown[]>;
forceFilters?: FilterExpr;
forceSorters?: SortControl[];
queryFn?: QueryFn;
queryOnMount?: boolean;
blockQuery?: boolean;
blockSize?: number;
dragReleaseDebounceMs?: number;
clientFactory?: ClientFactory;
controls?: TAsTableControls;
types?: TAsCellTypeComponents;
components?: Record<string, Component>;
formTypes?: TAsTypeComponents;
formComponents?: Record<string, Component>;
provideContext?: boolean;
refreshOnAction?: () => boolean;
onActionResolved?: (action, ids, result, event?) => void;
urlQueryReady?: Ref<boolean>;
onUrlQueryChange?: (urlString: string) => void;
urlQuerySync?: UrlQuerySync;
preset?: PresetConfig;
}clearTableCache()
Drops the shared /meta cache. Use after auth changes that invalidate server metadata.
Composables — state contracts
useTableContext() / useTableContextOptional()
Inject the context provided by AsTableRoot (or provideTableContext).
interface TableContext {
state: ReactiveTableState;
client: Client;
controls: TAsTableControls;
types?: TAsCellTypeComponents;
components?: Record<string, Component>;
formTypes?: TAsTypeComponents;
formComponents?: Record<string, Component>;
}
function useTableContext(): TableContext;
function useTableContextOptional(): TableContext | undefined;createTableState(opts)
Lower-level factory used by useTable. Returns { state, internals }. Reach for this when building a custom root (e.g. a multi-table dashboard).
function createTableState(opts: CreateTableStateOptions): {
state: ReactiveTableState;
internals: TableStateInternals;
};createStaticTableState(opts)
Builds a ReactiveTableState for static / in-memory data (no server, no /meta). Useful for stubs, demos, and unit tests.
function createStaticTableState(opts: CreateStaticTableStateOptions): {
state: ReactiveTableState;
internals: TableStateInternals;
};Composables — features
useTableSelection(state, opts?)
Wires the selection persistence policy onto an existing state. Called automatically by useTable; expose it for createStaticTableState consumers.
type SelectionPersistence = "clear" | "trim" | "persist";
function useTableSelection(state: ReactiveTableState, opts?: { mode?: SelectionPersistence }): void;useTableNavBridge()
Returns the keyboard nav bridge for binding external inputs (custom search box, command palette).
function useTableNavBridge(): TableNavBridge;
interface TableNavBridge {
onKeydown: (event: KeyboardEvent, opts?: NavKeyOptions) => void;
activeIndex: Ref<number>;
setActive: (absIndex: number) => void;
clearActive: () => void;
}useTableFilter()
Helpers for reading/writing the filter model from outside the dialog (toolbar chips, URL bridge, etc.).
useTableSearch()
Search-term composable — reads/writes state.searchTerm, applies debouncing.
useTableActions()
Returns state.actions (the full TableActionsState) from the closest <AsTableRoot> ancestor. Throws when called outside the provider tree.
function useTableActions(): TableActionsState;useTableUrlQuery(route, router, opts?)
Bridge <AsTableRoot v-model:url-query> to vue-router. Owns the whole query string. Uses type-only imports of Router / RouteLocationNormalizedLoaded — no runtime dependency on vue-router is added to @atscript/vue-table.
interface UseTableUrlQueryOptions {
/** `"replace"` (default) or `"push"`. */
mode?: "replace" | "push";
}
function useTableUrlQuery(
route: RouteLocationNormalizedLoaded,
router: Router,
opts?: UseTableUrlQueryOptions,
): WritableComputedRef<string>;<script setup>
import { useRoute, useRouter } from "vue-router";
import { useTableUrlQuery } from "@atscript/vue-table";
const urlQuery = useTableUrlQuery(useRoute(), useRouter());
</script>
<template>
<AsTableRoot v-model:url-query="urlQuery" url="/db/products" />
</template>useTableComponent(key, fallback)
Resolve a single chrome skin-slot from the injected controls map, falling back to the supplied component when the consumer left that entry unset. key is one of the TAsTableControls keys (headerCell, columnMenu, filterDialog, etc.) — this is for chrome, not cell-type dispatch.
function useTableComponent<K extends keyof TAsTableControls>(
key: K,
fallback: Component,
): Component;provideCellLocale(source) / useCellLocale()
Locale source for date / number cells. The provider takes a MaybeRefOrGetter so apps can wire it to useAppPrefs or to a global store without re-providing. The consumer side returns computed locale (falls back to navigator.language, then "en-US") and timezone (undefined lets Intl pick the browser TZ).
interface CellLocale {
language?: string;
timezone?: string;
}
function provideCellLocale(source: MaybeRefOrGetter<CellLocale | undefined>): void;
function useCellLocale(): {
locale: ComputedRef<string>;
timezone: ComputedRef<string | undefined>;
};Composables — presets
usePresets(options)
Manages preset rows, userConf, capabilities, and the apply/save plumbing. useTable instantiates this internally when preset is configured; expose it for advanced consumers.
interface UsePresetsOptions {
url: string;
app?: string;
tableKey: string;
clientFactory?: ClientFactory;
systemPresets?: SystemPresetInput[];
}
interface UsePresetsReturn {
presets: ShallowRef<AsPresetEntryRow[]>;
userConf: ShallowRef<AsPresetEntryRow | null>;
capabilities: Ref<PresetCapabilities | null>;
systemPresets: ComputedRef<SystemPreset[]>;
available: ComputedRef<boolean>;
saveActive: () => Promise<void>;
saveAs: (label: string, opts?: { aspects?: AspectMask; public?: boolean }) => Promise<string>;
rename: (id: string, label: string) => Promise<void>;
remove: (id: string) => Promise<void>;
togglePublic: (id: string) => Promise<void>;
setDefault: (id: string | null) => Promise<void>;
toggleFav: (id: string) => Promise<void>;
setFavorites: (ids: string[]) => Promise<void>;
batch: <T>(fn: () => Promise<T>) => Promise<T>;
}apply is not on usePresets — it lives on the wired state.preset surface (PresetSurface) and accepts a system id (sys:*), a stored row id, or a raw PresetSnapshot. Bypassing it and writing the underlying model arrays directly works too — the root watcher reacts either way.
useAppPrefs(options)
Manages the appConf row (app-wide user prefs: appearance, density, locale). Calls with the same (app, url) share a single underlying instance — duplicate widgets make one /query?type=appConf request total. Cross-tab sync rides BroadcastChannel; in-window sync rides a useEventBus.
interface UseAppPrefsOptions {
/** App namespace; defaults to `inject(AS_PRESETS_APP)`. */
app?: string;
/** Presets controller URL, e.g. `"/db/_presets"`. */
url: string;
clientFactory?: ClientFactory;
/** Auto-load on setup. Default `true`. */
autoLoad?: boolean;
/** Cache most recent prefs in `localStorage` keyed by app. Default `true`. */
cache?: boolean;
}
interface UseAppPrefsReturn {
/** Reactive prefs. Always non-null; defaults to `{}` until first load resolves. */
prefs: WritableComputedRef<AppConfData>;
loading: Ref<boolean>;
/** Last non-auth error, or `null`. Auth errors flip `available` instead. */
error: Ref<unknown>;
/** False on 401/403 from initial load — hide pref-bound controls. */
available: ComputedRef<boolean>;
reload(): Promise<void>;
/** Optimistic shallow-merge save; rolls back on error. */
save(patch: Partial<AppConfData>): Promise<void>;
reset(): void;
}useLocalDraft(options)
localStorage overlay manager for table preset drafts. One overlay per (app, tableKey); switching presets clears it (the caller decides when to call clear()).
interface UseLocalDraftOptions {
app: string;
tableKey: string;
enabled: Ref<boolean> | boolean;
availableAspects: readonly PresetAspect[];
debounceMs?: number;
storage?: StorageLike | null;
}
interface UseLocalDraftReturn {
/** Layer the persisted draft (if any) on top of `applied`. */
hydrate(applied: PresetSnapshot): PresetSnapshot;
/**
* Wire a debounced watcher that mirrors persisted aspects to localStorage.
* Returns the unwatch handle.
*/
watchAndPersist(
currentSnapshot: () => PresetSnapshot,
activePresetSnapshot: () => PresetSnapshot,
): () => void;
clear(): void;
readDraft(): PresetDraft | null;
}
interface StorageLike {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
}injectPresetsApp(override?) / AS_PRESETS_APP
const AS_PRESETS_APP: InjectionKey<string>;
function injectPresetsApp(override?: string): string;AS_PRESETS_APP lets a parent component declare the active "app" id (multi-app deployments). injectPresetsApp(override) returns the override when provided, else falls back to the injected value or 'default'.
Factories
createDefaultControls()
Returns a fresh TAsTableControls map pre-filled with the eight always-on Tier-2 defaults: headerCell, columnMenu, rowActions, filterInput, filterDialog, filterField, configDialog, confirmDialog. The other six slots — fieldsSelector, sortersConfig, filterValueHelp, presetPicker, presetDialog, actionFormDialog — are intentionally not seeded: the table root mounts the first five lazily on first open, and actionFormDialog is lazy-loaded so it only pulls in @atscript/vue-form when an @InputForm action is detected. Override a slot by spreading and assigning:
function createDefaultControls(): TAsTableControls;
const controls = {
...createDefaultControls(),
filterDialog: MyFilterDialog,
};createDefaultCellTypes()
Returns a pre-built TAsCellTypeComponents map with AsTableCellValue for every built-in type.
function createDefaultCellTypes(): TAsCellTypeComponents;Types
TAsTableControls
Skin-slot override map for table chrome. Set any subset — unset slots fall back to defaults.
interface TAsTableControls {
headerCell?: Component;
columnMenu?: Component;
rowActions?: Component;
filterInput?: Component;
filterDialog?: Component;
filterField?: Component;
filterValueHelp?: Component;
configDialog?: Component;
fieldsSelector?: Component;
sortersConfig?: Component;
confirmDialog?: Component;
actionFormDialog?: Component;
presetPicker?: Component;
presetDialog?: Component;
}TAsCellTypeComponents
type TAsCellTypeComponents = {
text: Component;
number: Component;
boolean: Component;
date: Component;
datetime?: Component;
relative?: Component;
array: Component;
object: Component;
enum: Component;
ref: Component;
__actions?: Component;
} & Record<string, Component>;ReactiveTableState
The full reactive state object. See the canonical definition in packages/vue-table/src/types.ts; the main slots are:
- Metadata:
tableDef,loadingMetadata,metadataError. - Columns:
columns,allColumns,columnNames,columnWidths. - Filters / sorters / search:
filters,filterFields,sorters,searchTerm. - Results:
results,windowCache,windowLoading,topIndex,viewportRowCount,totalCount,loadedCount,resultsStart. - Pagination:
pagination. - Selection:
selectedRows,selectedCount,rowValueFn,isPkSelected. - Active row / nav:
activeIndex,navMode,navViewportRowCount,hasMainActionListener,rowId,setActive,clearActive,toggleActiveSelection,requestMainAction,handleNavKey,registerMainActionListener. - Errors:
queryError,lastError,mustRefresh. - Querying flags:
querying,queryingNext. - Actions namespace:
actions: TableActionsState. - Prompt / action-form:
confirmRequest,prompt,acceptPrompt,dismissPrompt,actionFormRequest,requestActionInput,acceptActionForm,dismissActionForm. - Presets namespace:
preset: PresetSurface. - URL bridge:
applyUrlQuery.
TableActionsState
interface TableActionsState {
table: TVueTableActionInfo[];
row: TVueTableActionInfo[];
rows: TVueTableActionInfo[];
default: {
table?: TVueTableActionInfo;
row?: TVueTableActionInfo;
rows?: TVueTableActionInfo;
};
others: {
table: TVueTableActionInfo[];
row: TVueTableActionInfo[];
rows: TVueTableActionInfo[];
};
cellRow: TVueTableActionInfo[];
invoke: (
action: TVueTableActionInfo,
pk?: Record<string, unknown> | Record<string, unknown>[],
opts?: InvokeOpts,
) => Promise<ActionResult>;
invoking: ShallowRef<Set<string>>;
lastResult: ShallowRef<Map<string, ActionResult>>;
}ActionResult
type ActionResult =
| { ok: true; kind: "backend"; data: unknown; message?: string }
| { ok: true; kind: "navigate" }
| { ok: true; kind: "custom"; dispatched: true }
| { ok: true; kind: "remove"; data: TDbDeleteResult }
| { ok: false; kind: "error"; error: ClientError | Error };ColumnMenuConfig
interface ColumnMenuConfig {
sort?: boolean;
filters?: boolean;
hide?: boolean;
/** "Reset width" entry — shown only when current ≠ default. */
resetWidth?: boolean;
}RowDeleteOpt / InvokeOpts / NavKeyOptions / MainActionRequest / QueryErrorKind / TVueTableActionInfo
interface RowDeleteOpt {
label?: string;
icon?: string;
confirm?: string;
intent?: TDbActionInfo["intent"];
}
interface InvokeOpts {
suppressRefresh?: boolean;
event?: KeyboardEvent | MouseEvent;
input?: unknown;
}
interface NavKeyOptions {
enterAction?: "main-action" | "toggle-select" | "passthrough";
mode?: SelectionMode;
}
interface MainActionRequest {
row: Record<string, unknown>;
absIndex: number;
event: KeyboardEvent | MouseEvent;
}
type QueryErrorKind = "initial" | "query" | "queryNext" | "loadRange";
type TVueTableActionInfo = Omit<TDbActionInfo, "processor"> & {
processor: TDbActionProcessor | "__remove";
};Re-exports from @atscript/ui-table
ConfigTab, UrlQuerySync, AppConfData, AsPresetEntryRow, PresetAspect, PresetCapabilities, PresetData, PresetSnapshot, PresetSnapshotWire, SystemPreset, SystemPresetInput, UserConfData, PRESET_ASPECTS, STANDARD_PRESET_ID, SYSTEM_PRESET_PREFIX, isSystemPresetId, resolveSystemPresets. See @atscript/ui-table for definitions.
Re-exports from @atscript/ui
setDefaultClientFactory, getDefaultClientFactory, resetDefaultClientFactory, ClientFactory.
Utilities
function getColumnWidth(column: ColumnDef, widths: ColumnWidthsMap): string;
function getCellValue(row: Record<string, unknown>, path: string): unknown;
function formatCellValue(value: unknown, column: ColumnDef, opts?: { locale?: CellLocale }): string;
function extractIdentifier(
row: Record<string, unknown>,
preferredId: readonly string[],
): Record<string, unknown>;extractIdentifier builds the identifier object sent with action invocations and URL $1 substitution. Per @atscript/db-client invariant #11 the server rejects bare scalars — even single-field PK tables send { id: '...' }.
Cross-links
- Tables — Hello World
- Tables — Annotations Reference
- Tables — Query Function
- Tables — Filtering, Sorting
- Tables — Pagination & Virtualization
- Tables — Cells, Custom Cells
- Tables — Config Dialog
- Tables — URL State
- Tables — Presets, Server-Side Presets
- Tables — Actions & Selection
- Tables — Slot Overrides & Swaps
- @atscript/ui, @atscript/ui-table
- @atscript/moost-ui-presets