# data-binding — Form.io key と Notes アイテムのマッピング、隠しフィールドの正しい型

**Scope:** Form.io コンポーネント `key` から Notes アイテムへの保存経路、および隠しフィールド (setValue 用 / calculate 用) の型選択。レイアウト用の `panel` vs `container` の判断は [layout-styling.md](/docs/layout-styling.md) を参照。

## 1. Form.io ↔ Notes フィールド命名規則

- Form.io コンポーネントの `key` ↔ Notes アイテム `<prefix>{key}` の **1:1 マッピング**。`<prefix>` は **consentFlow アプリ側 (WEB フォーム設定の Notes フィールド接頭語) で設定される**もので、WEB フォーム側 (`key`) は接頭語の有無に依存しない
  - **デフォルトは `CF_`** (未設定時はコアが `CF_` を適用)。例: `key: "ringiTitle"` → `CF_ringiTitle`
  - **空文字も正式サポート (schema-driven mode)**。既存 Notes アプリのモダナイズで既存フィールド名 (例: `RingiTitle`) をそのまま使う場合は接頭語を空に設定する。このとき `key: "RingiTitle"` → Notes 側 `RingiTitle` (接頭語なし)
  - 任意の別接頭語も設定可能
- **WEB フォーム JSON 側は接頭語を一切ハードコードしない**。`key` は接頭語抜きの素の名前で書き、接頭語の付与/解決は consentFlow エンジンが `wfConfig` 設定に従って行う。`CF_` を前提にした条件分岐や命名を webform 側に書くと、空接頭語のモダナイズ案件で破綻する
- 例外:
  - `submit` / `signature` 型コンポーネントは同期対象外
  - `file` 型は別経路 (クラウドストレージ連携経由で Google Drive / Notes ストレージ文書にて管理)。Notes 文書本体には値ではなく `storageUnid` 系の参照を保持する
- **値の型はそのまま 1 アイテムに入る**:
  - スカラー (textfield / number / checkbox / radio / `dataSrc:"values"` の select / `valueProperty` を設定した select / datetime …) → そのまま `<prefix>key`
  - **オブジェクト** (`valueProperty:""` で選択肢オブジェクト全体を値に持つ json / url / custom select、selectboxes、survey、container、address、REST 行オブジェクト) → `<prefix>key` に **JSON 文字列 1 アイテム** (`CF_relationshipB` = `{"label":"妻","value":"妻"}`、`CF_contactMethods` = `{"email":true,"sms":false}`)。子キーには分解されない。空オブジェクトは `""`
  - グリッド行は列ごとに `<prefix><列key><postfix>N`。列の値がオブジェクトならその列アイテムに JSON 文字列 (`CF_relationshipC__1`)
  - **Notes ビュー・メールトークン `{{key}}`・SQL / Excel エクスポート・searchApps 列でスカラーが要る select は `valueProperty` を設定する** (Form.io がスカラーを渡すので `CF_key`="妻" になる)。JSON 文字列のままでは列表示・検索に向かない
  - **`multiple:true` の select は `valueProperty` 必須**。オブジェクト配列はグリッド行と区別できず `CF_label__1` … に行展開されて他コンポーネントと衝突する
  - **`persistent` は設定依存**: Form.io の定義は Server (`true`) = 送信して保存 / None (`false`) = 送信するが**バックエンドは保存しない** / Client-only = 送信もしない。consentFlow の既定 (アプリ設定「persistent:false / Client-only の項目を保存しない」= いいえ) では**この設定を見ずに全キーを保存する** (button の押下値 `CF_btnXxx="true"` まで)。設定が はい のときだけ Form.io サーバーと同じく `persistent:false` / `client-only` / button を `FormIOFieldJson` と `<prefix>key` の両方から外す (再表示では初期値に戻り、計算値は再計算される)。サンプルやラベルの「保存されません」はこの設定が はい のときの話。どちらの環境でも動くフォームにするなら「非永続の値を後で読む」設計を避け、残したい値は `persistent:true` の項目へ `calculateValue` で写す
  - 行オブジェクト ピッカー (`dataType:"object"` + restViewJson) は **設定が いいえ (既定) なら `persistent:false` でも Notes に保存される** (文書画面は Form.io の生 data をそのまま送る)。ピッカー自体は `<prefix>key` に行 JSON として残る前提で、必要な列は `calculateValue` でスカラーの別コンポーネントに転記する
  - 経緯: 旧コアはオブジェクト値を子キーだけで `CF_label` / `CF_value` / `CF_<選択肢>` / `CF_<列名>` に平坦化していたため、同形のコンポーネントが 2 個以上あると後勝ち上書き + 読込時の逆同期で全部同じ値に化けた (実例: 続柄 select が 2 つあるフォームで、保存し直すと全員の続柄が同じ値になった)。選択肢名が通常フィールドの key と同じ (selectboxes の `email` と email 欄) だと通常フィールドの値も失われた

### 1.1 ⚠ `valueProperty` / `dataType` を**後から**変えると、既存文書の値が照合できなくなる

運用中のフォームで select の `valueProperty` を `""` → `"value"` に変えると、**変更前に保存された文書が壊れる**。

- 変更前の保存値は選択肢オブジェクト `{"label":"妻","value":"妻"}`、変更後の選択肢はスカラー `"妻"`
- Form.io の照合は保存値がオブジェクトのとき `isObject(normalizeSingleValue(option))` を要求するため、選択肢がスカラーになった時点で **必ず不一致 → コントロールがブランク**
- **EditGrid では「一覧には出るのに行エディタだけブランク」になる。** 行テンプレートは `getView(component, row[key])` で、select の `getView` は `template` (`<span>{{ item.label }}</span>`) をそのまま描画するので、オブジェクトなら `.label` が解決して値が見える
- **再保存しても自然治癒しない。** select が値を解決できない＝新しい値を書かないので、ユーザーが開いて保存し直してもオブジェクトのまま残る
- 同じことが `dataType` にも起きる (`dataType:"number"` と文字列の選択肢 `value:"1"` など、型が食い違うと照合に失敗する)

**How to apply — 変える側の責任は 2 つある**:

1. **既存文書の移行経路**。文書を直接書き換えずに済ませるなら、既存の空 `calculateValue` を上書きして**読込時に正規化**する (`allowCalculateOverride:false` のまま。冪等なので新規文書に影響しない):

   ```js
   if (value && typeof value === 'object') {
     value = ('value' in value) ? value.value : '';
   }
   ```

   あわせて `defaultValue: {}` が残っていれば `""` にする。`normalizeSingleValue` は空オブジェクトを素通しするため描画が崩れる

2. **その値を読んでいる JS 全部**。`row.<key>.value` / `data.<grid>[i].<key>.value` の裸参照はスカラーでは `undefined` になり、条件分岐・バリデーション・ロジックが**無言で死ぬ**。移行期間は両対応にする:

   ```js
   var v = row.<key>;
   if (v && typeof v === 'object') { v = v.value; }
   v = v || "";
   ```

**片方だけ直すと新旧どちらかの文書が必ず壊れる。** 実例: 運用中フォームで設定だけ変えて JS を直さなかった結果、旧文書は表示が壊れ、新文書は条件分岐が死に、さらに最初からスカラー前提だった SQL 生成には旧文書から `[object Object]` が流れていた。

## 2. 「裏で持つだけの値」は `type: "textfield" + hidden:true` ではなく `type: "hidden"` を使う

ボタン JS / calculate / picker 連動などで **`setValue()` で動的に書き込んで送信時にサーバーへ届けたい** だけの隠しフィールドは、`type: "hidden"` (Form.io ネイティブ hidden コンポーネント) を使う。`type: "textfield"` + `hidden: true` だと **値がサーバーに保存されない**罠がある。

### 何が起きるか

```jsonc
// ❌ NG: textfield を hidden:true で隠す
{
  "type": "textfield",
  "key": "w_num2",
  "hidden": true,
  "clearOnHide": true,   // ← デフォルト true
  "input": true,
  "persistent": true
}
```

この設定で picker の `custom` JS から `form.getComponent('w_num2').setValue('0100')` してフォーム送信すると、サーバー側には `CF_w_num2` が **空のまま** 保存される。`form.data.w_num2` はクライアント側では一瞬入っているが、送信前ライフサイクルで Form.io が「`hidden:true` なフィールドは `clearOnHide:true` の対象 → 値をクリア」と判定するため。

### 正しい設定

```jsonc
// ✅ OK: ネイティブ hidden コンポーネント
{
  "type": "hidden",
  "key": "w_num2",
  "input": true,
  "persistent": true,
  "id": "ewn2"
}
```

`type: "hidden"` は `<input type="hidden">` としてレンダリングされ、`hidden:true` プロパティとは **別経路** (UI の表示制御ではない) なので `clearOnHide` ライフサイクルに巻き込まれない。`setValue()` で書いた値はそのまま送信される。

### `type: "hidden"` で省略してよい / 入れない方がよいプロパティ

| プロパティ | 不要な理由 |
|---|---|
| `hidden: true` | type=hidden の時点で常時非表示 |
| `clearOnHide` | hidden ライフサイクルに乗らないので無意味 |
| `tableView` / `label` / `widget` / `validateOn` / `inputType` / `labelPosition` | textfield 用設定で type=hidden では参照されない |

最小構成は `type` / `key` / `input:true` / `persistent:true` / `id` の 5 プロパティだけで OK。

### 既存フォームを点検したいとき

JSON 内で `"type": "textfield"` と `"hidden": true` を同時に持つコンポーネントを探す (静的チェックのルール CF004 が検出する)。

ヒットしたうち、JS から `setValue()` で値をセットしているフィールドは `type:"hidden"` への置換候補。`calculateValue` / `defaultValue` で値が決まるものは textfield + hidden:true のままでも動く (Form.io は `calculateValue` の戻り値で `clearOnHide` を上書きするため)。

### 参考実装 (実運用フォームの実例)

- 担当者ピッカーの裏フィールド `w_num1..17` / `w_name1..17` (ピッカー選択から職員番号・メールアドレスを裏に保持)
- 評価記録フォームの `Inp_1..` (計算結果保持用)

## 3. `input: false` のコンポーネントはデータ系ライフサイクルを skip する

`input: false` を指定した Form.io コンポーネントは `customDefaultValue` / `calculateValue` 等のデータライフサイクルが走らない (Form.io が「表示専用」と判断するため)。`customConditional` は描画系判定なので走る。

**Why:** 「隠し helper フィールド」を計算結果保持用や `window.*` 関数登録用に用意したい場合、`input:false` だと初期化処理が一切呼ばれず、関数が undefined のままで silent fail する。`htmlelement` / `content` のような純粋表示コンポーネントとは区別する。

**How to apply:** bootstrap 用の隠し helper は以下で構成する:

- `input: true` (必須 — customDefaultValue を走らせるため)
- `persistent: false` (Form.io の「保存しない」宣言。consentFlow で実際に Notes アイテムが作られなくなるのはアプリ設定「persistent:false / Client-only の項目を保存しない」が はい のときだけ。既定では保存される — §1)
- `hidden: true` (DOM には残るが非表示)
- `tableView: false`

そもそも DRY 化のために helper bootstrap を使うより、各ボタンに JS をインライン化する方がロード順依存・ライフサイクル依存がなくなり確実なケースも多い。

## 4. 32,000 バイト超の文字列値は黙って RichText アイテムになりビュー列から消える

Form.io フィールド値 (文字列) が **32,000 バイトを超えると**、consentFlow の保存処理は通常の Text アイテムではなく **RichText アイテムとして保存する** (Notes の Summary 制限回避のためのフォールバック)。エラーも警告も出ない。

**Why:** RichText アイテムは**ビュー列に値を出せない**ため、`restViewJson` の列でその値を読む設計 (例: EditGrid/グリッドの内容を hidden に JSON 直列化してビュー1列で読む「serialized JSON カラム」パターン) が、データが育った日に**静かに空になる**。restDocJson でも RichText は HTML 変換されて返るため生 JSON としては読めない。

**How to apply:**
- serialized JSON hidden (例: `scheduleJson` / `subjectsJson`) には `validate.custom` で **30,000 バイト上限**を張り、フォーム側 UI にもバイトカウンタを出す
- JSON キーは短縮形にし (`{"d":"2026-08-03","s":"08:30",...}`)、文書を月×人などでシャーディングして 1 アイテムを小さく保つ
- 実例: あるスケジュール管理アプリの `scheduleJson` (30,000 byte 上限 + 月×人でシャーディングして 1 文書)

## Red Flags

- ❌ 裏で `setValue()` する隠しフィールドに `"type": "textfield"` + `"hidden": true` を使う → 送信時にクリアされる。`"type": "hidden"` を使え (§2)
- ❌ 計算 helper に `"input": false` を使う → `calculateValue` / `customDefaultValue` が走らない。`input:true + persistent:false + hidden:true` (§3)
- ❌ `persistent:false` にしたから Notes に保存されないと思い込む → 既定のアプリ設定では**保存される** (button の値も)。保存されないのは設定「persistent:false / Client-only の項目を保存しない」が はい の環境だけ。逆に はい の環境では非永続項目の値をビュー・メール・SQL・サーバー側スクリプトから読めない (§1)
- ❌ `file` 型コンポーネントを通常の値アイテム経由で保存しようとする → クラウドストレージ連携経路 (`storageUnid`) で管理される (§1)
- ❌ webform JSON 側に `CF_` 接頭語をハードコードする (key を `CF_xxx` にする / JS で `CF_` 付き名を直接参照する) → 接頭語は `wfConfig` 設定値 (デフォルト `CF_`、モダナイズ案件では空文字もあり)。webform は接頭語非依存で書く (§1)
- ❌ ビュー列で読む前提の serialized JSON hidden を上限ガードなしで育てる → 32,000 バイト超で黙って RichText 化しビュー列から消える。validate.custom で 30,000 バイト上限 (§4)
