$p.events に複数の処理を登録する
$p.events.on_editor_load などに関数を代入する方法では、同じイベントに後から代入した関数が前の関数を上書きします。 複数の処理を登録するときは、このページで配布するヘルパーを読み込み、$p.eventHandlers.Add・Remove で登録・解除します。登録方法、戻り値の扱い、既存スクリプトとの併用条件を以下にまとめます。
なぜ1つしか設定できないのか
次の2つのスクリプトをこの順に実行すると、動くのは「処理B」だけです。
$p.events.on_editor_load = function () {
console.log('処理A');
};
$p.events.on_editor_load = function () {
console.log('処理B');
};本体の _init.js の初期化では、$p.events は空のオブジェクトとして作られています。 イベント名はそのプロパティ名です。= は処理の追加ではなく、そのプロパティへの代入なので、同じ名前に代入すると値が置き換わります。スクリプトを別々に作っても、代入先は同じです。
さらに、本体の $p.execEvents は、そのプロパティの関数を直接呼びます。関数の配列を代入しても、配列は呼び出せないため解決しません。
呼び出し側が期待している値は、イベント名ごとに一つの関数です。実際の実行箇所は次のとおりです。
function exec(name) {
if ($p.events[name] !== undefined) {
return $p.events[name](args) === false ? false : true;
}
return true;
}共通名のイベントと、コントロール固有のイベントは本体で別々に呼ばれます。
$p.execEvents = function (event, args) {
var result = exec(event);
var $control = args.$control;
if ($control) {
result = exec(event + '_' + $control.attr('id')) && result;
if ($control.attr('id') !== $control.attr('data-action')) {
result = exec(event + '_' + $control.attr('data-action')) && result;
}
}exec(...) && result は、左側の exec を先に実行してから、それまでの結果と合成します。共通イベントが false でも、右辺へ渡る前に固有名のイベントが呼ばれます。ヘルパーの false で止める範囲は、一つのイベント名の一覧です。更新前チェックをまとめたい場合は、例のように同じ before_send_Update へ登録します。
$p.events[name](args) と直接呼んでいるため、関数の配列を代入する方法では動きません。また、戻り値は厳密に false かどうかで真偽値に変換されます。ヘルパーでは、外側のプロパティに呼べる関数を一つ置いたまま、その関数の内側で一覧を管理します。
公式のスクリプトの説明で基本の設定方法を確認できます。ここでは、登録・解除の管理と戻り値の扱いまで含めたヘルパーを扱います。
ヘルパーを読み込む
このヘルパーは、このページで配布するカスタマイズ用のコードです。$p.eventHandlers は本体の標準APIではありません。
$p が初期化された後、イベントを登録するスクリプトより先に、ヘルパーを一度だけ実行します。テーブルの「スクリプト」に次の全文を設定し、使用する画面に出力してください。登録側のスクリプトも同じ画面に出力し、ヘルパー→登録コードの順に実行させます。確実に順序をそろえるには、1つのスクリプト内でヘルパーの後ろに登録コードを置きます。
ヘルパーの全文
// $p の初期化後、イベントを登録するスクリプトより先に読み込む。
(function (p) {
'use strict';
if (p.eventHandlers !== undefined) {
throw new Error('$p.eventHandlers は既に定義されています。ヘルパーは一度だけ読み込んでください。');
}
const entries = new Map();
function check(name, handler) {
if (typeof name !== 'string' || !/^[a-z][a-z0-9_]*$/i.test(name)) {
throw new TypeError('イベント名には英字で始まる英数字とアンダースコアを指定してください。');
}
if (typeof handler !== 'function') {
throw new TypeError('ハンドラーには関数を指定してください。');
}
const entry = entries.get(name);
if (entry && p.events[name] !== entry.dispatch) {
throw new Error('$p.events.' + name + ' が上書きされています。直接代入を Add に変更してください。');
}
return entry;
}
p.eventHandlers = Object.freeze({
Add: function (name, handler) {
let entry = check(name, handler);
if (!entry) {
const hadOwn = Object.prototype.hasOwnProperty.call(p.events, name);
const previous = hadOwn ? p.events[name] : undefined;
if (previous !== undefined && typeof previous !== 'function') {
throw new TypeError('$p.events.' + name + ' に関数以外が設定されています。');
}
if (handler === previous) return handler;
entry = { hadOwn, previous, handlers: [] };
entry.dispatch = function () {
// 実行中の Add / Remove は次のイベント発火から反映する。
const handlers = entry.handlers.slice();
if (entry.previous) handlers.unshift(entry.previous);
for (const fn of handlers) {
const result = fn.apply(this, arguments);
if (result && typeof result.then === 'function') {
throw new TypeError('このヘルパーは Promise を返すハンドラーに対応していません。');
}
if (result === false) return false;
}
return true;
};
entries.set(name, entry);
p.events[name] = entry.dispatch;
}
if (handler !== entry.previous && !entry.handlers.includes(handler)) {
entry.handlers.push(handler);
}
return handler;
},
Remove: function (name, handler) {
const entry = check(name, handler);
if (!entry) return false;
const index = entry.handlers.indexOf(handler);
if (index < 0) return false;
entry.handlers.splice(index, 1);
if (entry.handlers.length === 0) {
if (entry.hadOwn) p.events[name] = entry.previous;
else delete p.events[name];
entries.delete(name);
}
return true;
}
});
})($p);拡張スクリプトとして共通配置する
複数のテーブルで使う場合は、ヘルパーを拡張スクリプトに配置できます。サーバー上のファイルを配置・更新できる環境で利用する方法です。
配置と反映
- このページの配布ファイル
event-handlers.jsを取得します。 - ファイル名を
00_EventHandlers.jsに変更し、プリザンターのアプリケーションディレクトリのApp_Data/Parameters/ExtendedScripts/に配置します。 - テーブルの「スクリプト」に貼り付けたヘルパー本体があれば削除し、登録コードだけを残します。同じヘルパーを別の拡張スクリプトにも置かないでください。
- プリザンターのアプリケーションを再起動して反映し、ブラウザでページを再読み込みします。IIS 環境では、公式の拡張スクリプトの手順に従い IIS を再起動します。
配置後の構成は次のとおりです。.js ファイルに入れるのは上記のヘルパー全文で、<script> タグは付けません。
App_Data/
└─ Parameters/
└─ ExtendedScripts/
└─ 00_EventHandlers.jsファイルの読み込みは Initializer.csで確認できます。この配置方法では .js だけで読み込まれ、同名の JSON 設定ファイルは不要です。同じフォルダ内のファイルは名前順に読み込まれるため、別の拡張スクリプトからも Add を使う場合は、ヘルパーを先に並べます。サブフォルダは親フォルダのファイルの後に処理されるので、異なる階層のファイルを含めた全体の名前順にはなりません。
拡張スクリプトの読み込みでは、ファイルの拡張子と同じフォルダ内での順序をここで決めています。
var files = new DirectoryInfo(path)
.GetFiles("*.js")
.OrderBy(file => file.Name);
foreach (var file in files)
{
var script = Files.Read(file.FullName);GetFiles("*.js") は JavaScript ファイル自体を列挙し、OrderBy(file => file.Name) で並べます。この配置で別の JSON 定義が不要な理由と、ヘルパーを 00_EventHandlers.js にする理由がここにあります。後続のサブフォルダ再帰は別の処理なので、フォルダをまたいだ全体のファイル名順にはなりません。
テーブル側には登録コードを書く
各テーブルの「スクリプト」には、次のように登録する処理だけを書きます。編集画面で使う場合は出力先を「編集」にします。
$p.eventHandlers.Add('on_editor_load', function () {
console.log('このテーブルの編集画面を初期化します。');
});本体の HtmlScripts.csでは、本体スクリプト→拡張スクリプト→テーブルのスクリプトの順に出力します。このため、テーブル側が Add を呼ぶ時点でヘルパーを利用できます。ヘルパー本体は DOM を参照しないので、読み込み完了を待つ関数で包まず、そのまま配置します。
ヘルパーを読み込むだけでは、テーブル固有の処理は登録されません。共通配置するのは Add・Remove の仕組みで、実行する処理と出力先は各テーブルで決めます。
読み込みを確認する
対象画面のブラウザ開発者ツールのコンソールで、次を実行します。
typeof $p.eventHandlers?.Add
// "function" ならヘルパーが読み込まれているその後、編集画面を開き直し、登録した処理のログが出ることを確認します。ヘルパーファイルを変更した場合も、アプリケーションへの反映とページの再読み込みが必要です。
登録時と実行時の処理を分ける
ヘルパーの entries は、イベント名をキーにした Map です。初回の Add は、元のプロパティが存在したかを hadOwn、元の関数を previous に保存し、追加関数用の handlers を作ります。そのうえで、p.events[name] に窓口の dispatch を一つ代入します。2 回目以降の Add は窓口を増やさず、同じ handlers に追加します。
イベントが発火すると、dispatch は handlers.slice() で今回の実行一覧を作ります。元の関数があれば unshift で先頭に入れ、一覧を順に実行します。コピーを作る理由は、処理 A が実行中に処理 B を解除しても、今回の一覧を途中で変えないためです。その発火では B も実行し、次回から B を外します。追加も同じく次回から反映します。
各関数を呼ぶ fn.apply(this, arguments) は、窓口が受け取った呼び出し時の this と引数を引き継ぎます。ヘルパー独自の this や、作り直した引数に置き換えません。戻り値が false なら直ちに窓口から false を返し、同じ一覧の後続を止めます。すべての関数がそれ以外を返せば、窓口は true を返します。
result.then が関数ならエラーにするのは、Promise の完了後に false が決まっても、本体の同期的な判定に間に合わないためです。Promise を待つ機能は追加していません。関数を呼んだ後に戻り値を検査するので、関数内で開始済みの通信などは、このエラーで取り消せません。
Add で登録する
関数を直接代入する代わりに、イベント名と関数を Add に渡します。イベント名に $p.events. は付けません。
function initializeA(args) {
console.log('処理A', args);
}
function initializeB(args) {
console.log('処理B', args);
}
$p.eventHandlers.Add('on_editor_load', initializeA);
$p.eventHandlers.Add('on_editor_load', initializeB);本体に渡す関数は1つのまま、その関数の内側で登録済みの処理を順に呼びます。args と this は、本体から受け取ったものを各処理にそのまま渡します。
図を読み込み中…
Remove で解除する
解除するときは、登録時と同じ関数オブジェクトを渡します。
$p.eventHandlers.Remove('on_editor_load', initializeA);これで、次にイベントが呼ばれたときは initializeB だけが実行されます。名前や処理内容が同じでも、新しく作った関数は別のオブジェクトなので解除できません。無名関数を登録する場合も参照を保存します。
const handler = $p.eventHandlers.Add('on_editor_load', function () {
console.log('一時的な処理');
});
// 後で解除するとき
$p.eventHandlers.Remove('on_editor_load', handler);解除は entry.handlers.indexOf(handler) で、同じ関数の参照を探します。関数名の文字列やソースの内容は比較しません。見つかれば splice で一覧から外し、追加関数がなくなったときだけ窓口を取り外します。
hadOwn が真なら previous を元のプロパティへ戻し、偽なら delete p.events[name] でプロパティ自体を削除します。単に空の窓口を残す実装ではないので、追加前の設定を復元できます。previous は追加関数の一覧に入れていないため、Remove では解除できません。
また、check は現在の p.events[name] が保存した dispatch と同じかを調べます。直接代入で窓口を上書きした後に、一覧へだけ関数を追加して「登録できたのに実行されない」状態を作らないためです。ただし、上書きの瞬間を監視する処理ではなく、次の Add・Remove で検出します。
| API | 戻り値 | 動作 |
|---|---|---|
Add(name, handler) | 渡した関数 | 登録順に追加。同じイベントに同じ関数を重複登録しても追加しない |
Remove(name, handler) | true / false | 解除できたら true。登録されていなければ false |
イベント名の存在までは検証しません。本体が呼ばない名前を登録しても実行されないため、綴りと大文字・小文字を合わせてください。
false を返す入力チェック
本体の $p.execEvents は、関数の戻り値が厳密に false のときだけ false を返します。また、_ajax.js の送信処理 は before_send が false を返すと送信を中止します。
送信を止めるのは、戻り値を受け取る Ajax 側の次の判定です。
if ($p.before_send($p.eventArgs(url, methodType, data, $control, _async)) === false) {
return false;
}ヘルパーが false を返すことと、本体がその戻り値で何を止めるかは別です。この経路は return false で送信処理を抜けますが、戻り値を判定しないイベントでは同じ効果はありません。
ヘルパーもこの戻り値を保ちます。例えば、更新前のチェックを2つ登録できます。
function checkClassA() {
if (!$p.getControl('ClassA').val()) {
window.alert('分類Aを入力してください。');
return false;
}
}
function checkNumA() {
const value = $p.getControl('NumA').val();
if (value === '' || Number(value) <= 0 || !Number.isFinite(Number(value))) {
window.alert('数値Aには0より大きい数を入力してください。');
return false;
}
}
$p.eventHandlers.Add('before_send_Update', checkClassA);
$p.eventHandlers.Add('before_send_Update', checkNumA);分類Aが空なら checkClassA が false を返し、checkNumA は実行せず更新の送信も止まります。分類Aが入力済みなら数値Aを確認します。両方のチェックを通ると、ヘルパーは true を返します。例を試すテーブルには分類Aと数値Aを用意してください。
false の扱いはイベントごとに異なる
ヘルパー内部の後続処理は false で止めますが、本体側の処理が止まるかは呼び出し元によります。on_editor_load の呼び出し元では戻り値を判定していないため、false を返しても画面の読み込みを中止する仕組みにはなりません。 また、共通の before_send が false を返しても、$p.execEvents はコントロールID・data-action に対応する別名のイベントを呼びます。ヘルパーが止めるのは、同じイベント名に登録した後続処理だけです。
既存スクリプトと併用するときの注意点
- 初回の
Addより前に同じイベントへ代入された関数がある場合は、その関数を先頭で実行します。Removeの対象はAddで追加した関数だけです。引き継いだ関数も解除したい場合は、その登録をAddに書き換えます。 - 最後の追加関数を解除すると、最初に引き継いだ設定に戻します。もともとイベントのプロパティがなければ削除します。
Add後に$p.events.イベント名 = ...を実行すると、ヘルパーの窓口が上書きされます。そのイベントへの次のAdd・Removeはエラーにします。登録側はAddに統一してください。- 実行中の
Add・Removeは次回のイベント発火から反映します。今回の実行は、発火時点の一覧を使います。 - 処理中の例外は呼び出し元へそのまま伝え、後続を実行しません。
- 同期関数専用です。本体はイベントの戻り値を
awaitしません。async関数などが返すPromiseは検出してエラーにします。ただし、既に始まった非同期処理は取り消せません。送信を止めるチェックは同期関数で書いてください。 - ヘルパーを二重に実行するとエラーにします。
$p.eventHandlersはこのヘルパーのために使い、別のコードで同じ名前を定義しないでください。