Data Access from API
Learn how to initialize, configure, and query data in Extable.
Row Identification
When accessing rows in the API, you can use two identifiers:
- Row ID (
rowId: string): A unique identifier for each row. This remains stable even when rows are filtered, sorted, or reordered. If your data has a unique key field (likeid), it's often used as the row ID. If not provided, Extable generates a unique ID internally. - Row Index (
index: number): The zero-based position of the row in the current view. This changes when rows are filtered or sorted, but provides quick access by position.
// By row ID (stable identifier)
const row = table.getRow("user-123"); // Use if you have the row's ID
// By index (position)
const row = table.getRow(0); // First row in the current view
const row = table.getRow(5); // Sixth row in the current viewMost data access methods accept either rowId or index, so choose based on what you have available.
Accessing the API
The data access API depends on your framework choice:
Access the API directly on the ExtableCore instance:
import { ExtableCore } from "@extable/core";
const table = new ExtableCore({
root: container,
schema,
defaultData,
defaultView: {},
});
// All API methods available directly
const row = table.getRow("1");
const pending = table.getPending();
const data = table.getData();Bulk Data Load
Passing Data to Constructor
Pass data to the table during initialization:
import { ExtableCore } from "@extable/core";
import type { Schema } from "@extable/core";
interface UserRow {
id: string;
name: string;
email: string;
age: number;
}
const schema = {
columns: [
{ key: "id", header: "ID", type: "string", readonly: true },
{ key: "name", header: "Name", type: "string" },
{ key: "email", header: "Email", type: "string" },
{ key: "age", header: "Age", type: "number" },
],
} satisfies Schema;
const data: UserRow[] = [
{ id: "1", name: "Alice", email: "alice@example.com", age: 30 },
{ id: "2", name: "Bob", email: "bob@example.com", age: 28 },
];
const table = new ExtableCore({
root: document.getElementById("table-root")!,
schema,
defaultData: data,
defaultView: {},
});Fetching Data on Mount
For async data loading, pass null as defaultData initially. Extable shows loading spinner for null/undefined. After getting data from server, call setData() to pass it.
const table = new ExtableCore({
root: document.getElementById("table-root")!,
schema,
defaultData: null, // Shows loading state
defaultView: {},
});
// After data is fetched, set it
const response = await fetch("/api/users");
const fetchedData = await response.json();
table.setData(fetchedData);Updating Configuration
Schema is immutable
Schema is set during initialization and cannot be changed after creation. Plan your schema carefully before creating the table.
After initialization, use the appropriate method to update data or view:
// Update data
table.setData(newData);
// Update view (column visibility, filters, sorts)
table.setView(newView);Row-Level Editing
Get a Row
Retrieve a row by its ID or array index (returns data with formula results applied):
// By row ID (string)
const row = table.getRow("1");
// By array index (number)
const rowAtIndex = table.getRow(0);
// Both return R with formula results; null if not foundEdit a Row
After user edits cells, retrieve the current state:
// Get pending edits for a specific row (returns only T values, no formulas)
const rowPending = table.getPendingForRow("1"); // or table.getPendingForRow(0)
// Get full current state (includes pending edits and formula results)
const currentRow = table.getRow("1");
// Example: compare original vs current
const originalRow = { id: "1", name: "Alice", email: "alice@example.com", age: 30 };
const delta = {
original: originalRow,
pending: rowPending,
current: currentRow,
};Insert a Row
Add a new row to the table:
// Insert at the end
const newRowId = table.insertRow({ id: "new-1", name: "Bob", email: "bob@example.com", age: 28 });
// Insert at a specific index (0 = beginning, -1 = end)
const newRowId = table.insertRow(
{ id: "new-2", name: "Charlie", email: "charlie@example.com", age: 35 },
1 // Insert at index 1
);
// Returns the generated row ID, or null if insertion failed
if (newRowId) {
console.log("Row inserted with ID:", newRowId);
}In direct mode, the new row is immediately sent to the server.
In commit mode, the insertion is queued and requires calling commit().
Delete a Row
Remove a row from the table:
// Delete by row ID
const success = table.deleteRow("1");
// Delete by row index (from current view)
const rowId = table.getRow(0)?.id; // Get ID of first row
if (rowId) {
table.deleteRow(rowId);
}
// Returns true if deletion was successful, false if row not foundIn direct mode, the deletion is immediately sent to the server.
In commit mode, the deletion is queued and requires calling commit().
Cell-Level Editing
Get a Cell Value
Retrieve individual cell values (includes pending edits and formula results):
// Type-safe access with known column keys
const name = table.getCell("1", "name"); // string | undefined
const age = table.getCell("1", "age"); // number | undefined
// Get display value (formatted string representation)
const displayName = table.getDisplayValue("1", "name");
// Check if cell has pending changes
const isPending = table.getCellPending("1", "name");Set a Cell Value
Update a cell value programmatically:
// Update by row ID (string) and column key
table.setCellValue("1", "name", "Alice");
// Update by row index (number) and column key
table.setCellValue(0, "name", "Alice");
// Use a function to compute new value based on current value
table.setCellValue("1", "age", (current) => (current ?? 0) + 1);In direct mode, the change is applied immediately to the table and sent to the server.
In commit mode, the change is queued as a pending edit and requires calling commit() to persist.
Readonly cells are silently ignored and not updated.
Bulk Cell Queries
Get all values in a column:
// Type-safe column access
const names = table.getColumnData("name"); // string[]
const ages = table.getColumnData("age"); // number[]Set Values in Selection Range
Update all cells in the current selection:
// Set all selected cells to a specific value
table.setValueToSelection("example");
// Or use a function to compute per-cell
table.setValueToSelection((current) => (current ?? 0) + 10);This respects readonly cells and edit mode (direct vs commit).
Full Data Access
Get All Data
Retrieve the entire table:
// Current state with pending edits and formula results
const allData = table.getData(); // R[]
// Original input data without edits or formulas
const rawData = table.getRawData(); // T[]Get Pending Edits
In commit mode, retrieve all pending changes:
// All pending changes (T values only, no formula results)
const pending = table.getPending(); // Map<string, Partial<T>>
// Get pending row IDs
const changedRowIds = table.getPendingRowIds(); // string[]
// Check if there are pending changes
const hasChanges = table.hasPendingChanges(); // boolean
// Count total pending cells
const cellCount = table.getPendingCellCount(); // numberCommit Return Values
commit() resolves with RowStateSnapshot<T, R>[] for both overloads:
commit(): Promise<RowStateSnapshot<T, R>[]>commit(handler): Promise<RowStateSnapshot<T, R>[]>
Each snapshot contains:
rowId: row identifierrowIndex: current index in the viewdata: computed row data (R)pending: pending raw values (commit mode only)diagnostics: active validation/diagnostic errors for the row
The list includes rows touched by the pending command batch.
Commit Mode Data Retrieval
Before Commit
const pending = table.getPending(); // Map<string, Partial<T>>
const raw = table.getRawData(); // T[]
if (pending.size > 0) {
await table.commit();
}Commit with Async Handler
Use the async handler to validate or sync with a server before applying changes. If the handler throws, the commit is aborted and the error is propagated.
const snapshots = await table.commit(async (changes) => {
await sendToServer({
user: changes.user,
commands: changes.commands,
});
});
// snapshots: RowStateSnapshot<T, R>[]After Commit
const noLongerPending = table.getPending(); // Empty or minimal
const current = table.getData(); // Current table stateCommit with Server Sync (Delta Updates)
const snapshots = await table.commit(async (changes) => {
await sendToServer({
action: "bulk-update",
commands: changes.commands,
user: changes.user,
timestamp: Date.now(),
});
});
// snapshots is RowStateSnapshot<T, R>[] - list of changed rowsExample server integration:
async function sendToServer(payload: any) {
const response = await fetch("/api/table/sync", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Server error: ${response.statusText}`);
}
return response.json();
}Utility Methods
Get Index from ID
Convert row/column IDs to array indices:
const rowIndex = table.getRowIndex("1"); // number or -1 if not found
const colIndex = table.getColumnIndex("name"); // number or -1 if not foundGet Table State
Retrieve the current table state snapshot:
const state = table.getTableState(); // Includes pending count, edit mode, etc.
const selection = table.getSelectionSnapshot(); // Current cell selectionType Safety with Formulas
When you have both input and computed row types:
interface UserRow {
id: string;
name: string;
age: number;
}
interface UserRowResult extends UserRow {
ageGroup: string; // Computed via formula
}
const table = new ExtableCore<UserRow, UserRowResult>({
root: container,
defaultData: initialUsers,
defaultView: {},
schema: {
columns: [
{ key: "name", type: "string" },
{ key: "age", type: "number" },
{ key: "ageGroup", type: "string", formula: "=IF(age<30, 'Young', 'Senior')" },
],
},
});
// Type-safe access includes formula results
const row = table.getRow("1"); // UserRowResult with ageGroup
const ageGroup = table.getCell("1", "ageGroup"); // string | undefinedSubscriptions
Monitor table changes in real-time by subscribing to events. Each subscription returns an unsubscribe function.
Subscribe to Table State Changes
Listen for changes to pending edits, undo/redo state, errors, and render mode:
const unsubscribe = table.subscribeTableState((current, previous) => {
console.log("Pending changes:", current.pendingCellCount);
console.log("Can undo:", current.undoRedo.canUndo);
console.log("Can commit:", current.canCommit);
console.log("Active errors:", current.activeErrors);
});
// Later
unsubscribe();Subscribe to Selection Changes
Listen for selection ranges and active cell state:
const unsubscribe = table.subscribeSelection((current, previous, reason) => {
console.log("Active row:", current.activeRowKey);
console.log("Active column:", current.activeColumnKey);
console.log("Change reason:", reason); // 'selection', 'edit', 'action', 'data', etc.
// Check the active cell value
if (current.activeRowKey && current.activeColumnKey) {
console.log("Active value:", current.activeValueDisplay);
}
// Button cell action payload (reason === "action")
if (reason === "action" && current.action) {
console.log("Button action:", current.action.value);
}
});
// Later
unsubscribe();When a button cell is activated (click or Space), reason is "action" and current.action contains the button payload. Link cells navigate and do not emit action payloads.
Subscribe to Row State Changes
Listen for row-level events (insert, edit, delete):
const unsubscribe = table.subscribeRowState((rowId, next, prev, reason) => {
if (reason === "delete") {
console.log(`Row ${rowId} was deleted`);
return;
}
if (reason === "new") {
console.log(`Row ${rowId} was inserted`, next?.data);
return;
}
console.log(`Row ${rowId} was edited`, { prev: prev?.data, next: next?.data });
});
// Later
unsubscribe();Examples
Detect Cell Edits
table.subscribeTableState((current, previous) => {
if (current.pendingCellCount > (previous?.pendingCellCount ?? 0)) {
console.log("A cell was edited!");
}
});
table.subscribeSelection((current, prev, reason) => {
if (reason === "edit" && prev?.activeRowKey && prev.activeColumnKey) {
const newValue = table.getCell(prev.activeRowKey, prev.activeColumnKey);
console.log("Edit confirmed:", newValue);
}
});Export Modified Data
function exportChanges() {
const pending = table.getPending();
const csv = [];
for (const [rowId, changes] of pending) {
const row = table.getRow(rowId);
csv.push({
rowId,
changes,
currentState: row,
});
}
return JSON.stringify(csv, null, 2);
}