データフォーマットガイド
Extableは多様なデータ型をサポートし、それぞれ検証ルール、スタイル、表示方法を持ちます。このガイドでは、スキーマの列定義と各型の扱い方を説明します。
列定義の構造
スキーマ内の各列は次の要素を持ちます。
{
key: 'columnName', // 列の一意キー
header: 'Display Label', // 表示ラベル
type: 'string' | 'number' | 'int' | 'uint' | 'boolean' | 'date' | 'time' | 'datetime' | 'enum' | 'tags' | 'labeled' | 'button' | 'link',
width?: number, // 任意: 列幅(px)
readonly?: boolean, // 任意: 編集禁止
nullable?: boolean, // 任意: 空/Null可
wrapText?: boolean, // 任意: 折り返し有効化
style?: { /* type-specific */ },
conditionalStyle?: (row) => StyleObject | null, // 任意: 動的スタイル
formula?: (row) => value | [value, Error], // 任意: 計算列
// ... 型ごとのプロパティ
}defineSchemaによる型安全なスキーマ
defineSchema<T>()を使うと、formula/conditionalStyle内のrowが行モデルTとして型付けされます。
import { defineSchema } from '@extable/core';
type Row = {
id: number;
quantity: number;
unitPrice: number;
};
const schema = defineSchema<Row>({
columns: [
{ key: 'id', header: 'ID', type: 'number', readonly: true },
{ key: 'quantity', header: 'Qty', type: 'number' },
{
key: 'unitPrice',
header: 'Unit Price',
type: 'number'
},
{
key: 'total',
header: 'Total',
type: 'number',
formula: (row) => row.quantity * row.unitPrice, // rowはRow
conditionalStyle: (row) =>
row.quantity * row.unitPrice > 1000 ? { backgroundColor: '#fff3cd' } : null
}
],
row: {
conditionalStyle: (row) => (row.quantity === 0 ? { textColor: '#999' } : null)
}
});defineSchema<Row>(...)によりrow.quantityやrow.unitPriceが補完されます。
データ型
String
プレーンテキスト。長さやパターン検証を追加できます。
{
key: 'email',
header: 'Email Address',
type: 'string',
nullable: false,
// 文字列固有のオプション:
string: {
length?: { min: 0, max: 255 }, // 文字数制限
pattern?: /^[a-z0-9]+@[a-z]+\.[a-z]+$/, // 正規表現で検証
allowMultiline?: false // デフォルト: 単一行
}
}特徴:
- デフォルト表示: プレーンテキスト
- 折り返し:
wrapTextで制御 - 検証: 文字数と正規表現
- Null扱い:
nullable: trueなら空セル表示
Number
浮動小数点数。精度/スケールや表示オプションを設定できます。
あわせて参照: 数値フォーマットデモ →
{
key: 'salary',
header: 'Annual Salary',
type: 'number',
format: {
precision?: 10, // 有効桁数(scientific表示で使用)
scale?: 2, // 小数桁(decimal表示)
signed?: true, // falseの場合、負数は無効
thousandSeparator?: true, // カンマ区切りを表示(1,234.56)
negativeRed?: true, // 負数は赤
format?: 'decimal' | 'scientific'
},
style: { align: 'right' } // 数値は右寄せ
}表示例:
1234+thousandSeparator: true→1,234-50+negativeRed: true→ 赤字123.456+scale: 2→123.46(丸め)1234+format: 'scientific', precision: 4→1.234e+3
Integer(int / uint)
安全な整数(JavaScriptのNumber.MAX_SAFE_INTEGERの範囲内)。
あわせて参照: 数値フォーマットデモ →
int: 符号付きの安全整数uint: 非負の安全整数
{
key: 'flags',
header: 'Flags',
type: 'uint',
format: {
format: 'hex', // 'decimal' | 'binary' | 'octal' | 'hex'
negativeRed: false
},
style: { align: 'right' }
}Boolean
真偽値。表示形式を選べます。
{
key: 'active',
header: 'Active Status',
type: 'boolean',
// 表示バリエーション:
format: 'checkbox' // ☑️ / ☐
// または
format: ['TRUE', 'FALSE'] // テキスト表示
// または
format: ['Yes', 'No'] // ローカライズ表示
// または
format: ['真', '偽'] // 非英語
}操作:
- checkbox: クリック/Spaceで切替
- テキスト: セルクリックで切替
Date
日付のみ。使用可能トークン: yyyy、MM、dd(リテラル可)。
プリセット(format):
| Preset | Pattern | Note |
|---|---|---|
| iso | yyyy-MM-dd | Default (ISO) |
| us | MM/dd/yyyy | US style |
| eu | dd.MM.yyyy | EU style |
例: iso、us、euまたはカスタムパターン。
{
key: 'joinDate',
header: 'Join Date',
type: 'date',
format?: 'yyyy-MM-dd' // ISO標準(デフォルト)
// その他の一般的な形式:
// 'yyyy/MM/dd' // スラッシュ形式
// 'MM/dd/yyyy' // 米国形式
// 'dd.MM.yyyy' // 欧州形式
}保存:
- 内部はJavaScriptの
Dateオブジェクト - 入出力はISO 8601(
YYYY-MM-DD) - Nullは空セル
- フォーマットエンジンは軽量内蔵(date-fnsは不要)。対応トークン:
yyyy、MM、dd、HH、hh、mm、ss、a、単引用リテラル。例:yyyy-MM-dd、HH:mm:ss、hh:mm a、yyyy/MM/dd HH:mm。
Time
時刻のみ。使用可能トークン: HH、hh、mm、ss、a(リテラル可)。
プリセット(format):
| Preset | Pattern | Note |
|---|---|---|
| iso | HH:mm:ss | Default (24h + sec) |
| 24h | HH:mm | 24h, seconds hidden |
| 12h | hh:mm a | 12h, AM/PM |
{
key: 'startTime',
header: 'Start Time',
type: 'time',
format?: 'HH:mm:ss' // 24時間+秒(デフォルト)
// その他の一般的な形式:
// 'HH:mm' // 24時間(秒なし)
// 'hh:mm a' // 12時間(AM/PM)
// 'HH:mm:ss' // 24時間(秒付き)
}保存:
- 内部はJavaScriptの
Dateオブジェクト(date部分は無視) - 入出力はISO 8601時刻(
HH:mm:ss) - フォーマットエンジンはDateと同じ軽量実装
DateTime
日付+時刻。Date/Timeトークンの集合を使用します。
プリセット(format):
| Preset | Pattern | Note |
|---|---|---|
| iso | yyyy-MM-dd'T'HH:mm:ss'Z' | Default (ISO 24h) |
| iso-24h | yyyy-MM-dd'T'HH:mm:ss'Z' | Alias of iso |
| iso-12h | yyyy-MM-dd hh:mm a | ISO date + 12h |
| us | MM/dd/yyyy HH:mm | US date + 24h |
| us-24h | MM/dd/yyyy HH:mm | Alias of us |
| us-12h | MM/dd/yyyy hh:mm a | US date + 12h |
| eu | dd.MM.yyyy HH:mm | EU date + 24h |
| eu-24h | dd.MM.yyyy HH:mm | Alias of eu |
| eu-12h | dd.MM.yyyy hh:mm a | EU date + 12h |
{
key: 'createdAt',
header: 'Created At',
type: 'datetime',
format?: "yyyy-MM-dd'T'HH:mm:ss'Z'" // ISO 8601(デフォルト)
// その他の一般的な形式:
// 'yyyy/MM/dd HH:mm' // 日時(秒なし)
// 'MM/dd/yyyy hh:mm a' // 米国形式(AM/PM)
}保存:
- 内部はJavaScriptの
Dateオブジェクト - 入出力はISO 8601(
YYYY-MM-DDTHH:mm:ssZ) - タイムゾーンはバックエンドと整合を取る
- フォーマットエンジンは同じ軽量実装
Enum
選択肢から1つを選ぶ単一選択です。
{
key: 'department',
header: 'Department',
type: 'enum',
enum: {
options: ['Engineering', 'Sales', 'Marketing', 'HR']
}
}操作:
- セルクリックでドロップダウン
options以外は無効nullable: trueなら空も許可
検証:
options外の値はエラー- 大文字小文字は区別
Labeled
label(表示テキスト)とvalue(保存値)のペア。ユーザーフレンドリーな名前を表示しながら、技術識別子を内部に保存する必要がある場合に便利です。
{
key: 'assignee',
header: '担当者',
type: 'labeled',
edit: {
lookup: {
fetchCandidates: async ({ query, rowId, colKey, signal }) => [
{ label: 'Alice Smith', value: 'user_123' },
{ label: 'Bob Jones', value: 'user_456' },
{ label: 'Carol White', value: 'user_789' }
]
}
}
}保存形式:
- 内部では
{ label: string; value: unknown }として保存 - 例:
{ label: 'Alice Smith', value: 'user_123' } - 表示:
labelのみユーザーに表示されます
操作:
- セルをクリックして候補を表示・選択
- ドロップダウンではラベルと値を表示(ラベルで表示)
- ユーザーはラベルを見て、値は内部保存
Tags(タグリスト)
複数選択のタグ型です。
{
key: 'labels',
header: 'Labels',
type: 'tags',
tags: {
options: ['urgent', 'review', 'approved', 'archived'],
allowCustom?: false // ユーザー定義タグを禁止(推奨)
}
}保存:
- 内部は配列(
['urgent', 'approved']) - 表示はカンマ区切りまたはタグチップ
操作:
- セルクリックでマルチセレクト
- チェックで追加/削除
allowCustom: trueならユーザー定義タグを許可(整合性上は非推奨)
Button
アクションセル。ボタンは常にreadonlyで編集不可です。
{
key: 'action',
header: 'Action',
type: 'button',
style: { align: 'center' }
}値の形:
string→ ラベル(例:"Open"){ label: string; command: string; commandfor: string }→ アクションペイロード
commandとcommandforは必ずセットで指定します。
挙動:
- クリックまたはSpaceでセルアクションイベントを送出
style.disabledまたはconditionalStyle→{ disabled: true }で無効化(buttonのみ)
Link
リンクセル。リンクも常にreadonlyで編集不可です。
{
key: 'docs',
header: 'Docs',
type: 'link'
}値の形:
string→ URL(ラベルとhref){ label: string; href: string; target?: string }→ ラベル付きリンク
targetのデフォルトは_selfです。
挙動:
- クリックまたはSpaceで遷移
style.disabledまたはconditionalStyle→{ disabled: true }で無効化(linkのみ)
共通プロパティ
readonly
ユーザー編集を禁止します(ID列や計算列など)。
{
key: 'employeeId',
header: 'Employee ID',
type: 'string',
readonly: true // ユーザーは編集不可
}nullable
空/Nullを許可します。
{
key: 'middleName',
header: 'Middle Name (optional)',
type: 'string',
nullable: true // 空セル可
}wrapText
長文の折り返しを有効化します。
{
key: 'description',
header: 'Description',
type: 'string',
wrapText: true // 折り返しを有効化
}format
列の見た目を調整します。詳細はスタイルガイドを参照してください。
{
key: 'status',
header: 'Status',
type: 'string',
style: {
align: 'center', // 配置: 'left' | 'right' | 'center'
textColor?: '#d32f2f',
background?: '#fff3e0'
}
}conditionalStyle
行データに応じて動的にスタイルを適用します。例は条件付きスタイルを参照。
{
key: 'score',
header: 'Score',
type: 'number',
conditionalStyle: (row) => {
if (row.score >= 90) return { backgroundColor: '#c8e6c9' }; // 緑
if (row.score < 50) return { backgroundColor: '#ffcdd2' }; // 赤
return null; // デフォルトスタイル
}
}formula
JavaScript関数で計算列を定義します。詳細は数式ガイドを参照。
{
key: 'total',
header: 'Total',
type: 'number',
readonly: true,
formula: (row) => row.price * row.quantity // 計算結果
}編集フック
編集フックでセルの編集方法を設定します。任意の列型に適用でき、動的な候補選択(Lookup)またはカスタムインターフェースへの編集委譲(外部エディタ)を有効にします。
Lookup
非同期に取得した候補から動的に単一選択。オートコンプリート、リモートAPI参照、マルチユーザーシナリオに対応します。Lookupは任意の列型に適用可能です。
{
key: 'assignee',
header: '担当者',
type: 'string', // Lookupは任意の型で使用可能
edit: {
lookup: {
fetchCandidates: async ({ query, rowId, colKey, signal }) => {
// リモートAPI、データベース、またはメモリ内リストから取得
const res = await fetch(
`/api/users?search=${encodeURIComponent(query)}`,
{ signal }
);
return res.json();
// 想定: { label: string; value: unknown; meta?: any }[]
},
debounceMs: 250, // オプション: 取得までの遅延(デフォルト: 250ms)
recentLookup: true, // オプション: 最近選択した値を先頭に表示(デフォルト: true)
allowFreeInput: false, // オプション: 候補以外の自由入力を許可(デフォルト: false)
toStoredValue: (candidate) => candidate.value // オプション: 候補を保存値に変換
}
}
}オプション:
fetchCandidates (必須)
query(ユーザー入力)、rowId、colKey、signal(AbortSignal)で呼び出し{ label: string; value: unknown; meta?: any }[]配列を返す- 重要:
queryが空のとき、すべての候補を返す(初期ドロップダウン表示用) - ユーザーが入力したら、クエリに基づいてフィルタ/取得
debounceMs (オプション、デフォルト: 250)
- ユーザー入力後、
fetchCandidatesを呼び出すまでの遅延(ミリ秒) - 過度なAPI呼び出しを避けるのに有用
- ユーザー入力後、
recentLookup (オプション、デフォルト: true)
trueの場合、セル行で最後に選択した候補を候補リストの先頭に表示し、ドロップダウンで[recent]ラベルが付く- 列ごとに履歴が管理され、同じ値を繰り返し選択する場合に効率的
[recent]ラベルは表示のみで、保存される値には影響しない
allowFreeInput (オプション、デフォルト: false)
trueの場合、候補に完全一致しない値も入力・コミット可能false(デフォルト)の場合、候補リストから必ず選択- 自動コミット:
allowFreeInputがfalseで候補が1つに絞られると自動確定。allowFreeInputがtrueの場合は自動確定は無効化
toStoredValue (オプション)
fetchCandidatesが返す候補オブジェクトを実際に保存する値に変換- 例:
candidate => candidate.idでidフィールドを保存
ユーザー操作:
- セルをクリックして候補ドロップダウン表示(選択モードまたはインライン編集)
- 入力してフィルタ(デバウンス遅延付き)
- 矢印キーで移動、Enterで選択、Escapeで閉じる
- 自動コミット (
allowFreeInput === falseのみ): 候補が複数から1つに絞られると自動的に確定 - 自由入力 (
allowFreeInput === trueのみ): 候補を選択せずにEnterキーを押すと、入力されたテキストをそのままコミット
外部エディタ
セル編集をカスタムモーダル、フォーム、または外部インターフェイスに委譲。複雑な編集(リッチテキスト、多フィールドフォーム、コードエディタなど)に便利です。任意の列型に適用可能。
{
key: 'description',
header: '説明',
type: 'string',
edit: {
externalEditor: {
open: async ({ rowId, colKey, currentValue, signal }) => {
// カスタムUI(モーダル、ダイアログ、外部ウィンドウなど)を開く
const newValue = await showCustomEditor({
title: '説明を編集',
initialValue: currentValue,
signal
});
// 結果を返す
return {
kind: 'commit', // または 'cancel'
value: newValue
};
}
}
}
}動作:
- セルクリックではインラインエディタを開かず、
open()関数をトリガー - 関数は現在値を受け取り、
{ kind, value }を返す kind: 'commit'→valueをセルに書き込みkind: 'cancel'→ 変更を破棄、元の値を保持- 外部UIは選択モードのまま、その後は通常のナビゲーションに戻る
ユースケース:
- リッチテキストエディタ: WYSIWYG エディタ(Quill、TipTapなど)をHTMLコンテンツ用に開く
- マルチフィールドフォーム: 複雑なオブジェクトを複数フィールドで編集
- コードエディタ: JSON、SQL、またはカスタムコードを編集
- ファイル/画像ピッカー: メディアをアップロードまたは選択
- カスタムワークフロー: 追加オプション付きの日付ピッカー、アドレスジオコーディングなど
完全な例
const schema = {
columns: [
// ID列 - readonly
{
key: 'id',
header: '#',
type: 'number',
readonly: true,
width: 50
},
// Name - 必須文字列(折り返し)
{
key: 'name',
header: 'Employee Name',
type: 'string',
nullable: false,
wrapText: true,
width: 180
},
// Email - パターン検証付き
{
key: 'email',
header: 'Email',
type: 'string',
string: { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
width: 200
},
// Department - enum
{
key: 'department',
header: 'Department',
type: 'enum',
enum: { options: ['Engineering', 'Sales', 'Marketing', 'HR'] },
width: 140
},
// Salary - フォーマット済み数値
{
key: 'salary',
header: 'Salary',
type: 'number',
format: { scale: 2, thousandSeparator: true, negativeRed: true },
style: { align: 'right' },
width: 120
},
// Active - booleanチェックボックス
{
key: 'active',
header: 'Active',
type: 'boolean',
format: 'checkbox',
width: 80
},
// Join Date - フォーマット済み日付
{
key: 'joinDate',
header: 'Join Date',
type: 'date',
format: 'yyyy/MM/dd',
width: 130
},
// Skills - タグリスト
{
key: 'skills',
header: 'Skills',
type: 'tags',
tags: { options: ['JavaScript', 'TypeScript', 'React', 'Python', 'SQL'] },
width: 160
},
// Annual Compensation - 計算済みreadonly
{
key: 'annualComp',
header: 'Annual Comp',
type: 'number',
readonly: true,
format: { scale: 0, thousandSeparator: true },
style: { align: 'right' },
formula: (row) => row.salary * 1.25, // 給与 + 25%の福利
width: 140
},
// Notes - プレーン列
{
key: 'notes',
header: 'Notes',
type: 'string',
wrapText: true,
nullable: true,
width: 220
}
]
};バリデーションとエラー処理
スキーマ制約に合わないデータはエラー表示になります。
- 赤いセル枠: 無効データ(型違い、正規表現失敗、enum不一致)
- 警告アイコン: 非致命(数式のフォールバックなど)
- エラーアイコン: 致命的(数式が例外を投げた)
例:
- 数値列に
'abc'→ エラー pattern: /^\d+$/の文字列に'abc'→ エラー- enum列に
options外の値 → エラー - 数式が
new Error()→ 警告/エラー