@affino/datagrid-vue
v0.2.1
Published
Vue adapter and UI layer for @affino/datagrid-core (bootstrap fork from .tmp/ui-table/vue)
Readme
@affino/datagrid-vue
Vue adapter surface for @affino/datagrid-core.
For normal Vue usage, install and import only @affino/datagrid-vue.
@affino/datagrid-core and @affino/datagrid-orchestration are internal dependencies of this adapter.
Stable API (@affino/datagrid-vue)
createDataGridVueRuntimeuseDataGridRuntimeuseAffinoDataGriduseAffinoDataGridUiAffinoDataGridSimpleDataGriduseDataGridSettingsStorecreateDataGridSettingsAdapterbuildDataGridOverlayTransformbuildDataGridOverlayTransformFromSnapshotmapDataGridA11yGridAttributesmapDataGridA11yCellAttributesuseDataGridContextMenuDATA_GRID_SELECTORSDATA_GRID_DATA_ATTRSdataGridCellSelectordataGridHeaderCellSelectordataGridResizeHandleSelector
Stable selector contract (for parity tests/integration-safe querying):
import {
DATA_GRID_SELECTORS,
dataGridCellSelector,
} from "@affino/datagrid-vue"
const viewportSelector = DATA_GRID_SELECTORS.viewport
const ownerCellSelector = dataGridCellSelector("owner")Advanced API (@affino/datagrid-vue/advanced)
Compatibility entrypoint. New demo/workbench wiring should import @affino/datagrid-vue/internal.
useDataGridViewportBridgeuseDataGridHeaderOrchestrationcreateDataGridHeaderBindingsuseDataGridCellPointerDownRouteruseDataGridCellPointerHoverRouteruseDataGridDragSelectionLifecycleuseDataGridDragPointerSelectionuseDataGridFillSelectionLifecycleuseDataGridFillHandleStartuseDataGridRangeMoveLifecycleuseDataGridRangeMoveStartuseDataGridSelectionMoveHandleuseDataGridTabTargetResolveruseDataGridCellNavigationuseDataGridClipboardValuePolicyuseDataGridCellDatasetResolveruseDataGridCellRangeHelpersuseDataGridNavigationPrimitivesuseDataGridMutationSnapshotuseDataGridCellVisualStatePredicatesuseDataGridRangeMutationEngineuseDataGridA11yCellIdsuseDataGridColumnUiPolicyuseDataGridEditableValuePolicyuseDataGridMoveMutationPolicyuseDataGridInlineEditorSchemauseDataGridInlineEditOrchestrationuseDataGridInlineEditorTargetNavigationuseDataGridInlineEditorKeyRouteruseDataGridHeaderContextActionsuseDataGridCopyRangeHelpersuseDataGridHeaderSortOrchestrationuseDataGridHeaderResizeOrchestrationuseDataGridHeaderInteractionRouteruseDataGridColumnFilterOrchestrationuseDataGridEnumTriggeruseDataGridGroupValueLabelResolveruseDataGridGroupMetaOrchestrationuseDataGridGroupBadgeuseDataGridGroupingSortOrchestrationuseDataGridViewportMeasureScheduleruseDataGridVisibleRowsSyncScheduleruseDataGridColumnLayoutOrchestrationuseDataGridSelectionOverlayOrchestrationuseDataGridRowsProjectionuseDataGridRowSelectionOrchestrationuseDataGridRowSelectionInputHandlersuseDataGridVirtualRangeMetricsuseDataGridContextMenuAnchoruseDataGridContextMenuActionRouteruseDataGridViewportContextMenuRouteruseDataGridViewportBlurHandleruseDataGridViewportScrollLifecycleuseDataGridLinkedPaneScrollSyncuseDataGridResizeClickGuarduseDataGridInitialViewportRecoveryuseDataGridManagedWheelScrolluseDataGridClearSelectionLifecycleuseDataGridGlobalPointerLifecycleuseDataGridPointerAutoScrolluseDataGridPointerPreviewRouteruseDataGridPointerCellCoordResolveruseDataGridAxisAutoScrollDeltauseDataGridCellVisibilityScrolleruseDataGridGlobalMouseDownContextMenuCloseruseDataGridKeyboardCommandRouteruseDataGridQuickFilterActionsuseDataGridCellCoordNormalizeruseDataGridSelectionComparatorsuseDataGridRowSelectionModeluseDataGridPointerModifierPolicyuseDataGridHistoryActionRunneruseDataGridInlineEditorFocususeDataGridRowSelectionFacadeuseDataGridFindReplaceFacadeuseDataGridClipboardBridgeuseDataGridClipboardMutationsuseDataGridIntentHistory
Quick start
import { ref } from "vue"
import { useDataGridRuntime } from "@affino/datagrid-vue"
const rows = ref([])
const columns = ref([
{ key: "service", label: "Service", width: 220 },
])
const { api, columnSnapshot } = useDataGridRuntime({
rows,
columns,
})useDataGridRuntime also exposes patchRows(updates, options?) for partial row updates without mandatory sort/filter/group recompute on every cell change:
const runtime = useDataGridRuntime({ rows, columns })
runtime.patchRows(
[{ rowId: "r-42", data: { tested_at: "2026-02-21T10:15:00Z" } }],
{ recomputeSort: false, recomputeFilter: false, recomputeGroup: false },
)Use setRows for full data replacement; use patchRows for interactive/streaming cell updates when you want to avoid immediate projection jumps.
In no-recompute mode (recomputeSort/filter/group = false), row order/filter/group visibility can remain temporarily stale by design until a recompute pass is requested.
For app-level editing flows, prefer the higher-level applyEdits() + reapplyView() API:
const runtime = useDataGridRuntime({ rows, columns })
// default Excel-style behavior: update values, keep current view stable
runtime.applyEdits([{ rowId: "r-42", data: { tested_at: "2026-02-21T10:15:00Z" } }])
// explicit reapply when the user clicks "Reapply" or leaves edit mode
runtime.reapplyView()
// optional live-reapply mode
runtime.autoReapply.value = trueManaged wheel scroll (advanced)
Use useDataGridManagedWheelScroll when you want deterministic wheel ownership (axis lock, preventDefault policy, and consistent header/body horizontal sync).
import { useDataGridManagedWheelScroll } from "@affino/datagrid-vue/advanced"
const managedWheelScroll = useDataGridManagedWheelScroll({
resolveWheelMode: () => "managed", // "managed" | "native"
resolveWheelAxisLockMode: () => "dominant",
resolvePreventDefaultWhenHandled: () => true,
resolveBodyViewport: () => viewportRef.value,
resolveMainViewport: () => viewportRef.value
? {
scrollLeft: viewportRef.value.scrollLeft,
scrollWidth: viewportRef.value.scrollWidth,
clientWidth: viewportRef.value.clientWidth,
}
: null,
setHandledScrollTop: (nextTop) => {
if (viewportRef.value) {
viewportRef.value.scrollTop = nextTop
}
},
setHandledScrollLeft: (nextLeft) => {
if (viewportRef.value) {
viewportRef.value.scrollLeft = nextLeft
}
},
})
function onViewportWheel(event: WheelEvent) {
managedWheelScroll.onBodyViewportWheel(event)
}<div ref="viewportRef" @wheel="onViewportWheel" @scroll="onViewportScroll" />- Call
managedWheelScroll.reset()on unmount. - Keep DOM reads/writes in adapter/demo/UI layer; do not move wheel DOM handling into core.
Orchestration-heavy viewport integration
Use orchestration primitives from @affino/datagrid-vue/advanced to keep component code thin while preserving high-fidelity interaction behavior.
import {
useDataGridLinkedPaneScrollSync,
useDataGridResizeClickGuard,
useDataGridInitialViewportRecovery,
useDataGridRowSelectionModel,
} from "@affino/datagrid-vue/advanced"
const linkedPaneSync = useDataGridLinkedPaneScrollSync({
resolveSourceScrollTop: () => viewportRef.value?.scrollTop ?? 0,
mode: "css-var",
resolveCssVarHost: () => gridRootRef.value,
})
const resizeGuard = useDataGridResizeClickGuard()
const viewportRecovery = useDataGridInitialViewportRecovery({
resolveShouldRecover: () => totalRows.value > 1 && renderedRowsCount.value <= 1,
runRecoveryStep: () => syncViewportMetrics(),
})
const rowSelectionModel = useDataGridRowSelectionModel({
resolveFilteredRows: () => filteredRows.value,
resolveRowId: row => String(row.rowId),
resolveAllRows: () => allRows.value,
})Recommended ownership:
@affino/datagrid-core: deterministic data/runtime contracts.@affino/datagrid-orchestration: interaction policies and state orchestration.@affino/datagrid-vue: refs/template wiring and DOM lifecycle integration.
60-second integration (junior-friendly)
import { ref } from "vue"
import { AffinoDataGridSimple } from "@affino/datagrid-vue/components"
const rows = ref([
{ rowId: "1", service: "edge-gateway", owner: "NOC" },
{ rowId: "2", service: "billing-api", owner: "Payments" },
])
const columns = [
{ key: "service", label: "Service", width: 220 },
{ key: "owner", label: "Owner", width: 180 },
]<AffinoDataGridSimple
v-model:rows="rows"
:columns="columns"
:features="{ selection: true, clipboard: true, editing: true }"
/>- Includes pre-wired sort, row-selection, context-menu, clipboard and inline edit.
- Emits
update:rows,update:status, andactionfor app-level integration.
High-level sugar API
import { ref } from "vue"
import { useAffinoDataGrid } from "@affino/datagrid-vue"
const rows = ref([
{ rowId: "1", service: "edge-gateway", owner: "NOC" },
{ rowId: "2", service: "billing-api", owner: "Payments" },
])
const columns = ref([
{ key: "service", label: "Service", width: 220 },
{ key: "owner", label: "Owner", width: 180 },
])
const grid = useAffinoDataGrid({
rows,
columns,
features: {
selection: true,
clipboard: true,
editing: {
mode: "cell",
enum: true,
},
filtering: {
enabled: true,
initialFilterModel: {
columnFilters: {},
advancedFilters: {},
},
},
summary: {
enabled: true,
columns: [
{ key: "owner", aggregations: ["countDistinct"] },
],
},
visibility: {
enabled: true,
hiddenColumnKeys: [],
},
tree: {
enabled: true,
initialGroupBy: {
fields: ["owner"],
expandedByDefault: true,
},
},
interactions: {
enabled: true,
range: { enabled: true, fill: true, move: true },
},
headerFilters: {
enabled: true,
maxUniqueValues: 300,
},
feedback: {
enabled: true,
maxEvents: 120,
},
statusBar: {
enabled: true,
},
keyboardNavigation: true,
},
})
type Grid = ReturnType<typeof useAffinoDataGrid>
// grid is fully typed and safe to destructure.Row identity contract (required):
- Each row must expose a stable non-empty
rowId(orid/key), or - Provide
features.selection.resolveRowKey(row, index). - Index-based fallback keys are intentionally not used.
<th v-for="column in columns" :key="column.key" v-bind="grid.bindings.headerCell(column.key)">
{{ column.label }}
</th>
<td
v-for="column in columns"
:key="column.key"
v-bind="grid.bindings.dataCell({ row, rowIndex, columnKey: column.key, value: row[column.key] })"
>
<input
v-if="grid.bindings.isCellEditing(String(row.rowId), column.key)"
v-bind="grid.bindings.inlineEditor({ rowKey: String(row.rowId), columnKey: column.key })"
/>
<span v-else>{{ row[column.key] }}</span>
</td>grid.componentPropscan be passed into<DataGrid v-bind="grid.componentProps" />.grid.bindingsprovides ready wiring helpers:grid.bindings.headerSort(column.key)for sortable header handlers/ARIA.grid.bindings.rowSelection(row, rowIndex)for row selection click/keyboard behavior.grid.bindings.editableCell({ row, rowIndex, columnKey })+grid.bindings.inlineEditor(...)for inline edit flows.grid.bindings.headerCell(column.key)andgrid.bindings.dataCell(...)for pre-wired sort/edit + context-menu behavior.grid.bindings.contextMenuRoot()+grid.bindings.contextMenuAction(action.id)for menu keyboard/click wiring.grid.bindings.actionButton("copy" | "cut" | "paste" | ...)for toolbar-level actions.
grid.actionsprovides no-router commands for common flows:runAction("copy" | "cut" | "paste" | "clear" | "sort-asc" | "sort-desc" | "sort-clear")copySelectedRows(),cutSelectedRows(),pasteRowsAppend(),clearSelectedRows(),selectAllRows()- Mutating clipboard flows (
clear/cut/paste) are intent-transaction backed in sugar path (undo/redo-capable when history controls are wired).
grid.contextMenuwraps menu state + keyboard support + action execution:open(x, y, { zone, columnKey?, rowId? })runAction(actionId)(maps directly intogrid.actions)
grid.features.filteringexposesmodel,setModel,setAdvancedExpression, andclear.grid.features.filtering.helpersprovides typed advanced-filter helpers:setText,setNumber,setDate,setSet,apply,clearByKey- merge modes:
replace | merge-and | merge-or - set value modes:
replace | append | remove
grid.features.summary.selectedreturns deterministic aggregates for current selection scope.grid.features.visibilityexposessetColumnVisible,toggleColumnVisible,setHiddenColumnKeys,reset.grid.features.treeexposesgroupBy,groupExpansion,setGroupBy,toggleGroup,expandAll,collapseAll.grid.features.rowHeightexposessetMode("fixed" | "auto"),setBase(height),measureVisible().features.keyboardNavigation: trueenables out-of-the-box shortcuts and cell navigation:Cmd/Ctrl+C,Cmd/Ctrl+X,Cmd/Ctrl+V,Delete/BackspaceCmd/Ctrl+Z,Cmd/Ctrl+Shift+Z,Cmd/Ctrl+Y- arrows/home/end/page/tab/enter range navigation on focused grid cells
grid.paginationexposes first/prev/next/last + snapshot wrappers.grid.columnStateexposescapture/applyand point updates (setOrder,setVisibility,setWidth,setPin).grid.historyexposessupported/canUndo/canRedo/undo/redo.grid.rowReorderexposes guarded client-side reorder (moveByIndex,moveByKey).grid.cellSelectionexposes anchor/focus/range model (setCellByKey,isCellSelected,clear).grid.cellRangeexposes range clipboard/fill/move (copy,cut,paste,clear,applyFillPreview,applyRangeMove).grid.features.interactions.rangeprovides declarative fill/move lifecycle flags.grid.bindings.rangeHandle+grid.bindings.rangeSurfacewire fill/move without page-local pointer logic.grid.bindings.columnResizeHandle+grid.bindings.rowResizeHandleprovide drag/keyboard/double-click autosize.grid.features.headerFiltersprovides Excel-style popover model (open/toggle/query/operators/unique-values/select-only/select-all).grid.feedbackexposes unified event stream for action/context/history/range/header-filter flows.grid.contextMenuparity helpers:openForActiveCell,openForHeader,groupedActions, disabled reason helpers.grid.features.editing.enumEditorexposes Affino enum-editor contract (primitive,resolveOptions).grid.layoutProfilesexposes save/apply/remove/clear for sort/filter/group/column snapshots.grid.statusBarexposes built-in metrics model and summary accessors for status bar UIs.
Complete integration playbook
For end-to-end integration (tree rendering contract, advanced-filter AST cookbook, interaction/hotkey contract, and full-page setup), use:
/Users/anton/Projects/affinio/docs/datagrid-vue-sugar-playbook.md/Users/anton/Projects/affinio/docs/datagrid-sheets-user-interactions-and-integrator-api.md
Junior-first UI wrapper
import { ref } from "vue"
import { useAffinoDataGridUi } from "@affino/datagrid-vue"
const status = ref("Ready")
const grid = useAffinoDataGridUi({
rows,
columns,
status,
features: {
selection: true,
clipboard: true,
editing: true,
},
})<button v-bind="grid.ui.bindToolbarAction('copy')">Copy</button>
<th v-bind="grid.ui.bindHeaderCell(column.key)">{{ column.label }}</th>
<td v-bind="grid.ui.bindDataCell({ row, rowIndex, columnKey: column.key, value: row[column.key] })">
<input
v-if="grid.ui.isCellEditing(String(row.rowId), column.key)"
v-bind="grid.ui.bindInlineEditor({ rowKey: String(row.rowId), columnKey: column.key })"
/>
</td>- Demo-level orchestration can be imported from
@affino/datagrid-vue/internal.
