WEB フォームを開いたときに「1回だけ」処理を実行する(フォームスクリプト cfAPI.form — ページ表示ごと/新規作成時/既存文書の移行)

この記事で分かること

WEB フォームを開いたときに「1回だけ」動く処理は、consentFlow 4.4.0 から、WEB フォームに フォームスクリプト(予約キー scriptBlock のコンポーネント)を 1 つ置き、cfAPI.form にコールバックを登録するだけで実現できます。「1回」のスコープ(ページ表示ごと/新規作成時だけ/未実施の既存文書)はコールバックの種類で選び、ガード・遅延・値の復元はコアが面倒を見ます。この記事では、仕組み、設定手順、3 つのスコープの書き方、確認方法、落とし穴、そして 4.4.0 より前の環境での書き方を説明します。

対象: フォームビルダーでフォームを作る方。consentFlow 4.4.0 以降(それより前の環境は末尾の「4.4.0 より前の環境では」を参照)。

Notes/Domino から移行する方へ: Notes フォームの PostOpen や QueryOpen に書いていた「開いたときに1回だけ動く処理」は、このフォームスクリプトで置き換えられます。

前提: なぜ専用の仕組みがあるのか

WEB フォームのロジックは、イベントではなく値の再計算で動きます。「計算値」はページを開いた直後に評価され、その後も入力のたびに再評価されます。「初期値」に式を書いても、公開した時点の値が焼き込まれて新規文書では再評価されません。そのため、従来は非表示フィールドの計算値にガードを書き、ページ変数で多重実行を防ぎ、再描画で消える値を復元する、という定石が必要でした。

フォームスクリプトは、この定石をコアに取り込んだものです。開発者はコールバックの中身だけを書きます。

API いつ 1 回動くか 向いている用途
cfAPI.form.onOpen(fn) ページ表示ごと(参照モードでも)。部分更新でフォームが作り直されても再実行しない 開くたびの初期化、マスタの 1 回読み(REST)、URL パラメータの読み取り
cfAPI.form.onNew(fn) 新規作成時だけ 作成時刻・作成者・作成時点の所属のスナップショット
cfAPI.form.migrate(ver, fn) 未実施の既存文書を編集モードで開いたとき(番号の昇順に 1 回ずつ)。新規文書では実行せず、最新の番号だけ記録する 既存文書のデータ移行・正規化(バージョン番号で管理)
cfAPI.form.onReady(fn) フォームが(再)生成されるたび(部分更新ごと) 画面要素へのイベントの束縛、共通ヘルパーの公開

呼び出し順は onNew → migrate → onOpen → onReady で、いずれも保存データが適用された後です。前のコールバックが Promise を返した場合は、完了してから次へ進みます。

フォームスクリプトの 3 つのスコープ(デモ環境のキッチンシンクフォームより)

手順

  1. フォームビルダーで HTML 要素(htmlelement) コンポーネントを 1 つ置き、キーを scriptBlock にします(厳密にこの名前。1 フォームに 1 つ)
  2. 表示の設定で 非表示(hidden) をオンにします(4.4.0 より前のコアでは何もしない無害な部品になります)
  3. 本文(content)に、次のような JavaScript を書きます(HTML ではなく生の JavaScript。WYSIWYG ではなくコードのエディタか JSON で編集します)
  4. 結果を保存したい項目は、別途 非表示(Hidden)フィールドとして置き、保存する(persistent: true)にしておきます。コールバックからは ctx.set('キー', 値) で書き込みます

スクリプトの中では cfAPI、cfGlobal、form、$(jQuery)が使えます。コールバックの引数 ctx には、get(key) / set(key, value)(他の項目の読み書き)、isNewNote、editable、docid、data、component(key) が入っています。

① ページ表示ごとに 1 回(開くたびの初期化、マスタの 1 回読み)

cfAPI.form.onOpen(function (ctx) {
  ctx.set('openedAt', new Date().toLocaleTimeString('ja-JP'));
  if (ctx.isNewNote) { return; }   // REST は保存済みの文書だけ(新規時の docid は仮のもの)
  // Promise を返すと、コアは完了を待ってから自動保存の基準を取る
  return $.ajax({ url: './restDocJson.xsp/json?srv=masterdb&db=masterdb&unid=' + encodeURIComponent(ctx.docid), timeout: 30000 })
    .then(function (doc) { ctx.set('masterInfo', doc.SomeItem || ''); },
          function () { ctx.set('masterInfo', '(取得失敗)'); });
});
  • 参照モードでも動きます(結果は保存されません)
  • 部分更新(承認フロー欄の更新など)でフォームが作り直されても、onOpen は再実行されません。再実行してほしい処理は onReady に書きます

② 新規作成時に 1 回だけ(作成時のスナップショット)

cfAPI.form.onNew(function (ctx) {
  ctx.set('createdStamp', JSON.stringify({
    at: new Date().toISOString(),               // 作成時刻
    user: cfGlobal.currentUserName || '',       // 作成者
    sectionCode: cfGlobal.sectionCode || '',    // 作成時点の所属(異動後も申請時の所属を保持できる)
    sectionName: cfGlobal.sectionName || ''
  }));
});
  • 書き込み先(createdStamp)は保存する非表示フィールドにします。保存後に開き直しても再実行されません
  • 文書のコピーや子文書の作成では動きません(新規作成ではないため)
  • 項目 1 つの既定値(今日の日付など)なら、従来どおりその項目の計算値に if (typeof cfGlobal !== 'undefined' && cfGlobal.isNewNote && !value) { value = …; } と書いてかまいません。フォームスクリプトは、複数項目の初期化、REST などの副作用、移行に向いています

③ 未実施の文書を開いたとき 1 回(既存文書のデータ移行)

// 移行の内容を変えたら、既存の番号は書き換えず「新しい番号で追加」する
cfAPI.form.migrate(1, function (ctx) {
  var v = String(ctx.get('legacyField') || '');
  ctx.set('legacyField', v.replace(/[A-Za-z0-9]/g, function (ch) { return String.fromCharCode(ch.charCodeAt(0) - 0xFEE0); }));
});
cfAPI.form.migrate(2, function (ctx) {
  ctx.set('legacyField', String(ctx.get('legacyField') || '').toUpperCase());
});
  • 実施済みの番号は、コアが自動で用意する非表示フィールド cfMigrationVersion に保存されます。フォームに置く必要はありません
  • 編集モードで開いたときだけ動きます(参照モードでは保存されないため)。新規文書では実行せず、最新の番号だけが記録されます
  • 移行結果は、利用者がその文書を保存したときに確定します(ctx.set は未保存警告を出しません)

②と③の動作例(作成時スナップショットと、全角英数字の正規化)

結果の確認

  • ①: フォームを開き直すたびに実行時刻が変わり、入力欄に何か打っても、部分更新が起きても、実行時刻が変わらなければ成功です
  • ②: 新規作成時に値が入り、保存して開き直しても値が変わらなければ成功です
  • ③: 未実施の文書を編集モードで開いた直後に値が変わり、保存すると cfMigrationVersion が最新の番号になれば成功です
  • エラーが起きたときは、ブラウザのコンソールに [cfAPI.form] … と出て、cfAPI.form.errors にも残ります

デモ環境の「キッチンシンク」フォーム(ロジック・計算タブ)の「文書を開いたとき 1 回だけ実行する」パネルに、3 つのパターンが動く状態で入っています。

落とし穴

  1. キーは厳密に scriptBlock、1 フォームに 1 つ、非表示(hidden)を必ずオンにします
  2. 本文のトップレベルには登録だけを書きます。トップレベルは保存データの適用前に評価されるので、そこで値を書いても保存データで上書きされます。値の書き込みはコールバックの中(ctx.set)で行います
  3. フォームビルダーのプレビューでは評価されません(cfGlobal が未定義のため)。実際の画面で確かめます
  4. 本文に {{ を書かないでください(テンプレート式として解釈されます)
  5. データグリッドの中の項目は行付きで指定します(ctx.get('grid[0].child'))
  6. サーバー側で「保存時に 1 回」確実に行いたい処理(採番の確定、履歴の記録など)は、画面のスクリプトではなくプロコードの保存時フックの領域です

4.4.0 より前の環境では

フォームスクリプトを解釈しないコアでは、scriptBlock は何もしません。その場合は、従来の定石 「非表示(Hidden)フィールドの計算値にガードを書く」 を使います。ガードの置き場所で「1 回」のスコープが決まります。

スコープ ガード 保存する?
ページ表示ごとに 1 回 ページ変数 window.__xxxDone が未設定のときだけ実行。結果もページ変数に持ち、計算値で毎回復元する しない
新規作成時に 1 回だけ if (cfGlobal.isNewNote && !value) { value = …; } する
未実施の文書を開いたとき 1 回 var CURRENT = '2'; if (cfGlobal.editable && value !== CURRENT) { …移行…; value = CURRENT; } する

鉄則は 4 つです。(a) 副作用のガードはページ変数に持つ(インスタンスのフラグは再描画で消える)。(b) 他の項目への書き込みや REST は setTimeout(fn, 0) で次のタイミングに遅らせる。(c) 全体を try/catch で囲む(例外を投げると計算結果が捨てられる)。(d) 結果はページ変数に持ち、計算値でそこから毎回復元する。4.4.0 に更新した後も、この書き方はそのまま動きます。

関連記事

  • Notes のフォームイベントは WEB フォームのどこに書くか
  • ログインユーザーの氏名・所属・ロールを WEB フォームで使う
  • 選択肢を他の DB・ビューから取り込む
  • consentFlow フォームビルダー ライフサイクルイベント一覧