APIからのデータアクセス
Extableのデータ初期化、設定、取得の方法を解説します。
行の識別
APIで行にアクセスする際は、2種類の識別子があります。
- Row ID(
rowId: string): 各行の一意ID。フィルタ/ソート/並べ替えの影響を受けず安定します。データにidなどのユニークキーがあればそれを使用できます。未指定の場合は内部で生成されます。 - Row Index(
index: number): 現在のビューにおける0始まりの位置。フィルタ/ソートで変化しますが、位置で素早くアクセスできます。
// Row IDで取得(安定ID)
const row = table.getRow("user-123"); // 行IDが分かる場合に使用
// インデックスで取得(位置)
const row = table.getRow(0); // 現在ビューの先頭行
const row = table.getRow(5); // 現在ビューの6行目多くのAPIはrowIdまたはindexのどちらでも受け取れます。
APIへのアクセス
フレームワークに応じてアクセス方法が異なります。
ExtableCoreインスタンスから直接アクセスします。
import { ExtableCore } from "@extable/core";
const table = new ExtableCore({
root: container,
schema,
defaultData,
defaultView: {},
});
// すべてのAPIを直接呼び出せる
const row = table.getRow("1");
const pending = table.getPending();
const data = table.getData();一括データロード
コンストラクタに渡す
初期化時にデータを渡します。
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: {},
});マウント後に取得する
非同期ロード時はdefaultDataにnullを渡します。Extableはnull/undefinedでローディングスピナーを表示します。取得後にsetData()を呼びます。
const table = new ExtableCore({
root: document.getElementById("table-root")!,
schema,
defaultData: null, // ローディング表示
defaultView: {},
});
// 取得後にsetData()する
const response = await fetch("/api/users");
const fetchedData = await response.json();
table.setData(fetchedData);設定の更新
スキーマは不変
スキーマは初期化時に確定し、後から変更できません。テーブル生成前に設計しておきましょう。
初期化後は、データやビューを次のAPIで更新します。
// データを更新
table.setData(newData);
// Viewを更新(表示列/フィルター/ソート)
table.setView(newView);行レベル編集
行の取得
IDまたは配列インデックスで取得します(数式結果を含む)。
// 行IDで取得(string)
const row = table.getRow("1");
// 配列インデックスで取得(number)
const rowAtIndex = table.getRow(0);
// 数式結果を含むRを返す。見つからなければnull行の編集
編集後の状態を取得します。
// 特定行の保留編集を取得(Tのみ、数式なし)
const rowPending = table.getPendingForRow("1"); // またはtable.getPendingForRow(0)
// 現在の状態を取得(保留と数式結果を含む)
const currentRow = table.getRow("1");
// 例: 元データと現在を比較
const originalRow = { id: "1", name: "Alice", email: "alice@example.com", age: 30 };
const delta = {
original: originalRow,
pending: rowPending,
current: currentRow,
};行の追加
// 末尾に追加
const newRowId = table.insertRow({ id: "new-1", name: "Bob", email: "bob@example.com", age: 28 });
// 指定位置に追加(0=先頭, -1=末尾)
const newRowId = table.insertRow(
{ id: "new-2", name: "Charlie", email: "charlie@example.com", age: 35 },
1 // インデックス1に挿入
);
// 生成された行IDを返す(失敗時はnull)
if (newRowId) {
console.log("Row inserted with ID:", newRowId);
}directモードでは即時サーバーへ送信、commitモードではcommit()が必要です。
行の削除
// 行IDで削除
const success = table.deleteRow("1");
// ビューの行インデックスで削除
const rowId = table.getRow(0)?.id; // 先頭行のIDを取得
if (rowId) {
table.deleteRow(rowId);
}
// 削除成功でtrue、行がなければfalsedirectモードでは即時送信、commitモードではcommit()が必要です。
セルレベル編集
セル値の取得
保留変更と数式結果を含むセル値を取得します。
// 列キー指定で型安全に取得
const name = table.getCell("1", "name"); // string | undefined
const age = table.getCell("1", "age"); // number | undefined
// 表示用のフォーマット済み文字列を取得
const displayName = table.getDisplayValue("1", "name");
// セルが保留変更か判定
const isPending = table.getCellPending("1", "name");セル値の設定
// 行IDと列キーで更新
table.setCellValue("1", "name", "Alice");
// 行インデックスと列キーで更新
table.setCellValue(0, "name", "Alice");
// 現在値から新値を計算する関数を使用
table.setCellValue("1", "age", (current) => (current ?? 0) + 1);directモードは即時反映+送信、commitモードは保留状態でcommit()が必要です。readonlyセルは無視されます。
列の一括取得
// 列キーで型安全に取得
const names = table.getColumnData("name"); // string[]
const ages = table.getColumnData("age"); // number[]選択範囲への一括設定
// 選択範囲を一括で同値に設定
table.setValueToSelection("example");
// もしくはセルごとに計算
table.setValueToSelection((current) => (current ?? 0) + 10);readonlyセルと編集モードに従います。
全データアクセス
全データ取得
// 保留と数式結果を含む現在データ
const allData = table.getData(); // R[]
// 編集/数式なしの元データ
const rawData = table.getRawData(); // T[]保留変更の取得
// 保留変更一覧(Tのみ、数式なし)
const pending = table.getPending(); // Map<string, Partial<T>>
// 保留がある行ID一覧
const changedRowIds = table.getPendingRowIds(); // string[]
// 保留があるか判定
const hasChanges = table.hasPendingChanges(); // boolean
// 保留セル数を取得
const cellCount = table.getPendingCellCount(); // numberCommitの戻り値
commit()はRowStateSnapshot<T, R>[]を返します。
commit(): Promise<RowStateSnapshot<T, R>[]>commit(handler): Promise<RowStateSnapshot<T, R>[]>
各スナップショットには次が含まれます。
rowId: 行IDrowIndex: 現在のビューでの位置data: 計算後の行データ(R)pending: 保留中の生データ(commitモードのみ)diagnostics: 行の診断/バリデーションエラー
リストは今回の保留コマンドで触れた行のみです。
Commitモードのデータ取得
Commit前
const pending = table.getPending(); // Map<string, Partial<T>>
const raw = table.getRawData(); // T[]
if (pending.size > 0) {
await table.commit();
}非同期ハンドラ付きCommit
非同期ハンドラで検証やサーバー同期を行えます。ハンドラが例外を投げるとcommitは中断されます。
const snapshots = await table.commit(async (changes) => {
await sendToServer({
user: changes.user,
commands: changes.commands,
});
});
// スナップショット: RowStateSnapshot<T, R>[]Commit後
const noLongerPending = table.getPending(); // 空または最小
const current = table.getData(); // 現在のテーブル状態サーバー同期(差分更新)
const snapshots = await table.commit(async (changes) => {
await sendToServer({
action: "bulk-update",
commands: changes.commands,
user: changes.user,
timestamp: Date.now(),
});
});
// snapshotsはRowStateSnapshot<T, R>[] - 変更行一覧サーバー統合例:
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();
}便利なメソッド
IDからインデックスを取得
const rowIndex = table.getRowIndex("1"); // number(見つからなければ-1)
const colIndex = table.getColumnIndex("name"); // number(見つからなければ-1)テーブル状態の取得
const state = table.getTableState(); // 保留数や編集モードなどを含む
const selection = table.getSelectionSnapshot(); // 現在のセル選択数式の型安全
入力型と計算結果型が異なる場合:
interface UserRow {
id: string;
name: string;
age: number;
}
interface UserRowResult extends UserRow {
ageGroup: string; // 数式で計算
}
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')" },
],
},
});
// 型安全アクセスは数式結果を含む
const row = table.getRow("1"); // ageGroupを含むUserRowResult
const ageGroup = table.getCell("1", "ageGroup"); // string | undefinedサブスクリプション
テーブルの変更を購読できます。各サブスクは解除関数を返します。
テーブル状態の購読
保留数、undo/redo、エラー、描画モードなどを監視します。
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);
});
// 後で
unsubscribe();選択状態の購読
選択範囲やアクティブセルの変化を監視します。
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' など
// アクティブセルの値を確認
if (current.activeRowKey && current.activeColumnKey) {
console.log("Active value:", current.activeValueDisplay);
}
// ボタンセルのアクションpayload(reason === "action")
if (reason === "action" && current.action) {
console.log("Button action:", current.action.value);
}
});
// 後で
unsubscribe();ボタンセルの起動(クリック/Space)ではreasonが"action"になり、current.actionにpayloadが入ります。リンクセルは遷移のみでpayloadは出ません。
行状態の購読
行単位のイベント(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 });
});
// 後で
unsubscribe();例
セル編集を検知する
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);
}
});変更データのエクスポート
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);
}