Skip to content

データフォーマットガイド

Extableは多様なデータ型をサポートし、それぞれ検証ルール、スタイル、表示方法を持ちます。このガイドでは、スキーマの列定義と各型の扱い方を説明します。

列定義の構造

スキーマ内の各列は次の要素を持ちます。

typescript
{
  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として型付けされます。

typescript
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.quantityrow.unitPriceが補完されます。

データ型

String

プレーンテキスト。長さやパターン検証を追加できます。

typescript
{
  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

浮動小数点数。精度/スケールや表示オプションを設定できます。

あわせて参照: 数値フォーマットデモ →

typescript
{
  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: true1,234
  • -50 + negativeRed: true → 赤字
  • 123.456 + scale: 2123.46(丸め)
  • 1234 + format: 'scientific', precision: 41.234e+3

Integer(int / uint

安全な整数(JavaScriptのNumber.MAX_SAFE_INTEGERの範囲内)。

あわせて参照: 数値フォーマットデモ →

  • int: 符号付きの安全整数
  • uint: 非負の安全整数
typescript
{
  key: 'flags',
  header: 'Flags',
  type: 'uint',
  format: {
    format: 'hex',            // 'decimal' | 'binary' | 'octal' | 'hex'
    negativeRed: false
  },
  style: { align: 'right' }
}

Boolean

真偽値。表示形式を選べます。

typescript
{
  key: 'active',
  header: 'Active Status',
  type: 'boolean',
  // 表示バリエーション:
  format: 'checkbox'           // ☑️ / ☐
  // または
  format: ['TRUE', 'FALSE']    // テキスト表示
  // または
  format: ['Yes', 'No']        // ローカライズ表示
  // または
  format: ['真', '偽']         // 非英語
}

操作:

  • checkbox: クリック/Spaceで切替
  • テキスト: セルクリックで切替

Date

日付のみ。使用可能トークン: yyyyMMdd(リテラル可)。

プリセット(format):

PresetPatternNote
isoyyyy-MM-ddDefault (ISO)
usMM/dd/yyyyUS style
eudd.MM.yyyyEU style

例: isouseuまたはカスタムパターン。

typescript
{
  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は不要)。対応トークン: yyyyMMddHHhhmmssa、単引用リテラル。例: yyyy-MM-ddHH:mm:sshh:mm ayyyy/MM/dd HH:mm

Time

時刻のみ。使用可能トークン: HHhhmmssa(リテラル可)。

プリセット(format):

PresetPatternNote
isoHH:mm:ssDefault (24h + sec)
24hHH:mm24h, seconds hidden
12hhh:mm a12h, AM/PM
typescript
{
  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):

PresetPatternNote
isoyyyy-MM-dd'T'HH:mm:ss'Z'Default (ISO 24h)
iso-24hyyyy-MM-dd'T'HH:mm:ss'Z'Alias of iso
iso-12hyyyy-MM-dd hh:mm aISO date + 12h
usMM/dd/yyyy HH:mmUS date + 24h
us-24hMM/dd/yyyy HH:mmAlias of us
us-12hMM/dd/yyyy hh:mm aUS date + 12h
eudd.MM.yyyy HH:mmEU date + 24h
eu-24hdd.MM.yyyy HH:mmAlias of eu
eu-12hdd.MM.yyyy hh:mm aEU date + 12h
typescript
{
  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つを選ぶ単一選択です。

typescript
{
  key: 'department',
  header: 'Department',
  type: 'enum',
  enum: {
    options: ['Engineering', 'Sales', 'Marketing', 'HR']
  }
}

操作:

  • セルクリックでドロップダウン
  • options以外は無効
  • nullable: trueなら空も許可

検証:

  • options外の値はエラー
  • 大文字小文字は区別

Labeled

label(表示テキスト)とvalue(保存値)のペア。ユーザーフレンドリーな名前を表示しながら、技術識別子を内部に保存する必要がある場合に便利です。

typescript
{
  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(タグリスト)

複数選択のタグ型です。

typescript
{
  key: 'labels',
  header: 'Labels',
  type: 'tags',
  tags: {
    options: ['urgent', 'review', 'approved', 'archived'],
    allowCustom?: false  // ユーザー定義タグを禁止(推奨)
  }
}

保存:

  • 内部は配列(['urgent', 'approved']
  • 表示はカンマ区切りまたはタグチップ

操作:

  • セルクリックでマルチセレクト
  • チェックで追加/削除
  • allowCustom: trueならユーザー定義タグを許可(整合性上は非推奨)

Button

アクションセル。ボタンは常にreadonlyで編集不可です。

typescript
{
  key: 'action',
  header: 'Action',
  type: 'button',
  style: { align: 'center' }
}

値の形:

  • string → ラベル(例: "Open"
  • { label: string; command: string; commandfor: string } → アクションペイロード

commandcommandforは必ずセットで指定します。

挙動:

  • クリックまたはSpaceでセルアクションイベントを送出
  • style.disabledまたはconditionalStyle{ disabled: true }で無効化(buttonのみ)

リンクセル。リンクも常にreadonlyで編集不可です。

typescript
{
  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列や計算列など)。

typescript
{
  key: 'employeeId',
  header: 'Employee ID',
  type: 'string',
  readonly: true  // ユーザーは編集不可
}

nullable

空/Nullを許可します。

typescript
{
  key: 'middleName',
  header: 'Middle Name (optional)',
  type: 'string',
  nullable: true  // 空セル可
}

wrapText

長文の折り返しを有効化します。

typescript
{
  key: 'description',
  header: 'Description',
  type: 'string',
  wrapText: true  // 折り返しを有効化
}

format

列の見た目を調整します。詳細はスタイルガイドを参照してください。

typescript
{
  key: 'status',
  header: 'Status',
  type: 'string',
  style: {
    align: 'center',                    // 配置: 'left' | 'right' | 'center'
    textColor?: '#d32f2f',
    background?: '#fff3e0'
  }
}

conditionalStyle

行データに応じて動的にスタイルを適用します。例は条件付きスタイルを参照。

typescript
{
  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関数で計算列を定義します。詳細は数式ガイドを参照。

typescript
{
  key: 'total',
  header: 'Total',
  type: 'number',
  readonly: true,
  formula: (row) => row.price * row.quantity  // 計算結果
}

編集フック

編集フックでセルの編集方法を設定します。任意の列型に適用でき、動的な候補選択(Lookup)またはカスタムインターフェースへの編集委譲(外部エディタ)を有効にします。

Lookup

非同期に取得した候補から動的に単一選択。オートコンプリート、リモートAPI参照、マルチユーザーシナリオに対応します。Lookupは任意の列型に適用可能です。

typescript
{
  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(ユーザー入力)、rowIdcolKeysignal(AbortSignal)で呼び出し
    • { label: string; value: unknown; meta?: any }[] 配列を返す
    • 重要: queryが空のとき、すべての候補を返す(初期ドロップダウン表示用)
    • ユーザーが入力したら、クエリに基づいてフィルタ/取得
  • debounceMs (オプション、デフォルト: 250)

    • ユーザー入力後、fetchCandidatesを呼び出すまでの遅延(ミリ秒)
    • 過度なAPI呼び出しを避けるのに有用
  • recentLookup (オプション、デフォルト: true)

    • trueの場合、セル行で最後に選択した候補を候補リストの先頭に表示し、ドロップダウンで [recent] ラベルが付く
    • 列ごとに履歴が管理され、同じ値を繰り返し選択する場合に効率的
    • [recent] ラベルは表示のみで、保存される値には影響しない
  • allowFreeInput (オプション、デフォルト: false)

    • trueの場合、候補に完全一致しない値も入力・コミット可能
    • false(デフォルト)の場合、候補リストから必ず選択
    • 自動コミット: allowFreeInputfalseで候補が1つに絞られると自動確定。allowFreeInputtrueの場合は自動確定は無効化
  • toStoredValue (オプション)

    • fetchCandidatesが返す候補オブジェクトを実際に保存する値に変換
    • 例: candidate => candidate.idid フィールドを保存

ユーザー操作:

  • セルをクリックして候補ドロップダウン表示(選択モードまたはインライン編集)
  • 入力してフィルタ(デバウンス遅延付き)
  • 矢印キーで移動、Enterで選択、Escapeで閉じる
  • 自動コミットallowFreeInput === falseのみ): 候補が複数から1つに絞られると自動的に確定
  • 自由入力allowFreeInput === trueのみ): 候補を選択せずにEnterキーを押すと、入力されたテキストをそのままコミット

外部エディタ

セル編集をカスタムモーダル、フォーム、または外部インターフェイスに委譲。複雑な編集(リッチテキスト、多フィールドフォーム、コードエディタなど)に便利です。任意の列型に適用可能。

typescript
{
  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、またはカスタムコードを編集
  • ファイル/画像ピッカー: メディアをアップロードまたは選択
  • カスタムワークフロー: 追加オプション付きの日付ピッカー、アドレスジオコーディングなど

完全な例

typescript
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() → 警告/エラー

次のステップ

Apache 2.0 Licenseで公開