はじめに
プリザンターのスクリプト(クライアントサイド)を書いていると、画面の項目値を読んだり書いたりする場面が必ず出てきます。そのときに使うのが $p.getValue() や $p.set() といった $p 配下の関数です。
ところがこれらの関数、名前が似ているわりに中身の担当範囲がまったく違います。
-
$p.getValue()と$p.getControl().val()で違う値が返ってくることがある -
$p.set()と$p.setValue()はどちらも「値をセットする」ように見えるのに挙動が違う -
$p.setData()は画面を書き換えないのに、なぜか保存内容に影響する
これらは全部「その関数が内部で何を呼んでいて、最終的にどこの値を触っているか」を知ると腑に落ちます。今回は $p.get 系・$p.set 系の関数がどんな入れ子構造になっていて、どこから値を引っ張ってきているのかを、実装を追いながら図で整理してみましょう。
バージョン 1.5.7.0 を対象にしています
そもそも $p とは
$p はプリザンターがグローバルに用意しているオブジェクトです。定義そのものはとても素朴で、5 つの入れ物を持っているだけです。
window.$p = {
data: {},
events: {},
ex: {},
modal: {},
store: {}
};
ここに各スクリプトファイルが後から関数を生やしていく、という構成です。今回の主役は次の 2 つです。
| プロパティ | 役割 |
|---|---|
$p.data |
サーバへ送信する値をためておくバッファ。フォーム ID ごとに $p.data[formId][コントロールid] = 値 の形で保持する |
$p.store |
処理中の一時的な状態(現在のフォームなど)を置く場所 |
「画面(DOM)」と「送信バッファ($p.data)」は別物、という点が今回いちばん大事なポイントです。$p.get 系・$p.set 系の関数は、このどちらを触るかで性格が分かれます。
関数は 2 グループに分かれる
$p の get / set 系関数を眺めると、引数の型で綺麗に 2 つに分かれます。
| グループ | 引数 | 代表的な関数 | 想定利用者 |
|---|---|---|---|
| 項目名グループ | 項目の表示名またはカラム名(文字列) |
$p.getColumnName() / $p.getControl() / $p.getField() / $p.getValue()
|
スクリプトを書く利用者 |
| コントロールグループ |
jQuery オブジェクト($control) |
$p.set() / $p.setValue() / $p.setData() / $p.getData()
|
プリザンター本体の内部処理 |
つまり、利用者が書くスクリプトでは「項目名グループで対象のコントロールを取得し、それをコントロールグループの関数に渡す」という流れになります。
// 項目名グループで取得 → コントロールグループに渡す
$p.set($p.getControl('ClassA'), '選択肢1');
この橋渡しをしているのが $p.getControl() です。まずはここから見ていきましょう。
$p.getControl() — すべての起点
$p.getControl() は、項目名から画面上のコントロールを探し出す関数です。実装はこれだけです。
$p.getColumnName = function (name) {
var data = JSON.parse($('#Columns').val()).filter(function (column) {
return column.LabelText === name || column.ColumnName === name;
});
return data.length > 0 ? data[0].ColumnName : undefined;
};
$p.getControl = function (name) {
var columnName = $p.getColumnName(name);
return columnName !== undefined
? $('#' + $('#ReferenceType').val() + '_' + columnName)
: undefined;
};
短いコードですが、実は 2 つの hidden 項目から値を引っ張ってきています。図にすると次のようになります。
ここから読み取れるポイントは 3 つです。
-
引数には表示名とカラム名のどちらを渡してもよい。
LabelText === name || column.ColumnName === nameで両方見ています。項目名を「数値A」から「金額」に変えた場合、$p.getControl('金額')でも$p.getControl('NumA')でも引けます -
コントロールの id は「テーブル種別 + アンダースコア + カラム名」。期限付きテーブルなら
Issues_NumA、記録テーブルならResults_NumAになります -
見つからないと
undefinedが返る。$p.getControl('存在しない項目').val()と書くとundefinedに対する.val()でエラーになるため、戻り値のチェックが必要です
$p.getColumnName() は呼ばれるたびに hidden 項目の JSON を JSON.parse します。ループの中で $p.getControl() を何度も呼ぶ場合は、結果を変数に持っておくと無駄な解析を避けられます。
なお $p.getField() は同じ仕組みで末尾に Field を付けた id を返します。こちらはコントロールそのものではなく、ラベルを含む項目の外枠を取得する関数です。項目全体を非表示にしたいときなどに使います。
$p.getValue() — data-raw を最優先で返す
次に $p.getValue() です。$p.getControl() の上に乗っている関数で、コントロールから「値」を取り出します。
$p.getValue = function (name) {
let $control = $p.getControl(name);
if ($control === undefined || $control.length === 0) {
return undefined;
}
let element = $control[0];
//data-raw属性があればそれを優先的に返却
let dataRaw = element.getAttribute('data-raw');
if (dataRaw !== null) {
return dataRaw;
}
//Input要素はvalue,またはchecked、Selsect要素は$control.val() それ以外はtextContentを返す
switch (element.tagName) {
case 'INPUT':
return element.type === 'checkbox' ? element.checked : element.value;
case 'SELECT':
return $control.val();
case 'TEXTAREA':
return element.value;
default:
return element.textContent;
}
};
入れ子と分岐を図にするとこうなります。
注目すべきは data-raw を最優先で返す という分岐です。ここが $p.getValue() の性格を決めています。
data-raw はどこで付くのか
data-raw は、サーバが HTML を描画するときに付与される属性です。数値項目のテキストボックスを描画している箇所を見てみましょう。
case ControlTypes.TextBoxNumeric:
return hb.FieldTextBox(
textType: HtmlTypes.TextTypes.Normal,
fieldId: controlId + "Field",
controlId: controlId,
// ~中略~
attributes: new Dictionary<string, string>()
{
["data-raw"] = rawValue?.ToString()
?? (column.Nullable == true ? "" : "0")
},
スピナー(上下ボタン付きの数値入力)でも同様です。
.Add("data-raw",
value != null
? value.ToString()
: string.Empty)
data-raw が付く目的は「表示は書式化されるので、書式なしの生値を別に持っておく」ことです。数値項目は「1,000 円」のように桁区切りや単位が付いた形で表示されることがあり、表示文字列をそのまま読んでも計算に使えません。読み取り専用の項目(SPAN で描画される)でも、textContent は表示テキストなので生値が別途必要になります。
data-raw は画面操作では更新されない
ここが重要なところです。クライアント側のスクリプトを検索しても、data-raw を書き換えている処理は存在しません。読んでいるのは $p.getValue() の 1 か所だけです。
つまり data-raw は、サーバが HTML を描画した瞬間の値のまま固定されます。更新されるのは保存などでサーバから HTML が返ってきて再描画されたときだけです。
この挙動が実際に問題になるのが、プロセス機能のボタンから発火したスクリプトで、保存前の数値項目を読みたいケースです。
| 項目の種類 | data-raw |
$p.getValue() が返す値 |
|---|---|---|
数値項目(NumA など) |
付く | 描画時点の値(保存前の入力は反映されない) |
分類項目(ClassA など) |
付かない | 画面の現在値 |
| タイトル・説明項目 | 付かない | 画面の現在値 |
読み取り専用項目(SPAN) |
付く | 描画時点の生値 |
分類項目やタイトルで「変更後の値が取れる」のに数値項目で取れないのは、この差によるものです。
保存前の数値項目の入力値を読みたい場合は、$p.getValue() ではなく $p.getControl('NumA').val() を使います。ただし戻り値は文字列なので、計算に使うときは Number() などで数値化してください。
var $control = $p.getControl('NumA');
var num = $control !== undefined ? Number($control.val()) : 0;
$p.getData() — 送信バッファを取り出す
ここからはコントロールグループです。まずは読み取り側の $p.getData() を見てみましょう。
$p.getData = function ($control) {
$p.store.formId = $p.getFormId($control);
if (!($p.store.formId in $p.data)) {
$p.data[$p.store.formId] = {};
}
return $p.data[$p.store.formId];
};
$p.getFormId() は、渡されたコントロールから一番近い form を遡って id を取る関数です。
$p.getFormId = function ($control) {
return $control.closest('form').attr('id');
};
戻り値はオブジェクトへの参照なので、受け取った側が data.Xxx = 1 と書けば $p.data 本体が書き換わります。本体のコードでもこの性質を使って、直接プロパティを足している箇所が多くあります。
$p.getData($('.main-form')) と書けば、いま送信対象になっている値の一覧をまとめて確認できます。デバッグのときに便利です。
$p.setData() — 画面の値を送信バッファへ写す
$p.setData() は、渡されたコントロールの現在値を読み取って $p.data に格納する関数です。画面は書き換えません。あくまで「画面 → バッファ」の一方通行です。
$p.setData = function ($control, data) {
var controlId = $control.attr('id');
if (!$control.hasClass('not-send')) {
if (data === undefined) {
data = $p.getData($control);
}
$p.setGridTimestamp($control, data);
switch ($control.prop('type')) {
case 'checkbox':
data[controlId] = $control.prop('checked');
break;
// ~中略~
default:
switch ($control.prop('tagName')) {
case 'SPAN':
data[controlId] =
$control.attr('data-value') !== undefined
? $control.attr('data-value')
: $control.text();
break;
// ~中略~
default:
data[controlId] = $control.val();
break;
}
break;
}
}
};
ここで注目したいのが、$p.setData() は data-raw を見ていないことです。$control.val() を読むので、画面で入力した直後の値がそのままバッファに入ります。数値項目を画面で変更したあと保存すると、ちゃんと変更後の値が保存されるのはこのためです。
$p.setData() はコントロールの change イベントから自動的に呼ばれています。
$(document).on('change', '[class^="control-"]:not(select[multiple])', function (e) {
var $control = $(this);
// ~中略(スピナーの範囲補正)~
$p.setData($control);
e.preventDefault();
});
スクリプトから jQuery で直接 val() を書き換えると change イベントが発火しないため、$p.data にも入りません。値を書き換えたのに保存されない、という現象の典型的な原因がこれです。
$p.set() — DOM とバッファの両方を更新する
$p.set() は、これまで見てきた関数を束ねた「利用者が使うべき」書き込み関数です。
$p.set = function ($control, val) {
if ($control.length === 1) {
switch ($control.prop('type')) {
case 'checkbox':
$control.prop('checked', val);
break;
case 'textarea':
$control.val(val);
// ~中略(マークダウンのビューア切り替え)~
break;
default:
switch ($control.prop('tagName')) {
case 'SELECT':
// ~中略(検索付きドロップダウンなら Ajax で候補を取得)~
if ($control.attr('multiple')) {
$p.selectMultiSelect($control, val);
} else {
$control.val(val);
$control.change();
}
break;
default:
// ~中略(ラジオ・アンカーの個別処理)~
$control.val(val);
break;
}
break;
}
$p.setData($control);
}
};
入れ子を図にすると、$p.setData() → $p.getData() → $p.getFormId() と 3 段になっているのが分かります。
$p.set() の要点は次の 3 つです。
- DOM とバッファの両方を更新するので、これ 1 回で「画面にも見えるし、保存もされる」状態になります
-
$control.length === 1が条件なので、コントロールが見つからなかったときは黙って何もしません。エラーにならない代わりに気付きにくい点に注意してください -
選択肢の項目では
change()を発火させるため、その項目に紐づく他の処理(自動ポストバックや連動絞り込み)も動きます
$p.setValue() — DOM だけを更新する
名前が紛らわしいのですが、$p.setValue() は $p.set() とはまったく別物です。
$p.setValue = function ($control, value) {
switch ($control.prop('type')) {
case 'checkbox':
$control.prop('checked', value);
break;
case 'radio':
$control.val([value]);
break;
// ~中略~
default:
switch ($control.prop('tagName')) {
case 'SELECT':
// ~中略~
case 'SPAN':
$control.html(value);
break;
case 'TIME':
$control.html(value);
$control.attr('datetime', value);
break;
default:
// ~中略~
$control.val(value);
break;
}
}
};
見ての通り、末尾に $p.setData() の呼び出しがありません。DOM を書き換えるだけで、送信バッファには一切触りません。
この関数はサーバからのレスポンスを反映するために使われています。サーバスクリプトなどで context.AddResponse() を組み立てたときの、SetValue メソッドの受け口がここです。
case 'SetValue':
$p.setValue($(target), value);
$p.hideField(target, options);
break;
サーバ側の C# では、この 2 つは別のメソッドとして用意されています。
public ResponseCollection Set(
string target,
object value,
bool _using = true)
{
return _using
? Add(
method: "Set",
target: target,
value: value)
: this;
}
public ResponseCollection Val(
string target,
object value,
string options = null,
bool _using = true)
{
return _using
? Add(
method: "SetValue",
target: target,
value: value,
options: options)
: this;
}
C# の Val() が JavaScript の SetValue に対応している、というねじれた対応関係になっている点は覚えておくと混乱しません。
送信されるまでの流れ
最後に、バッファに入った値がどうやってサーバへ届くのかを確認しておきましょう。ボタンなどをクリックしたときに走る $p.send() の中で、バッファがそのまま送信データになります。
$p.setMustData() があるおかげで、新規作成時は画面のすべてのコントロールが送信対象になります。逆に更新時はバッファに入っている項目しか送られません。スクリプトから値を変えたのに更新されない場合は、$p.set() を使わずに val() だけで書き換えていないか確認してみてください。
全体像と使い分け
ここまでの関係をまとめると次のようになります。
関数ごとの守備範囲を表にすると次のとおりです。
| 関数 | 引数 | DOM を更新 |
$p.data を更新 |
主な用途 |
|---|---|---|---|---|
$p.getColumnName(name) |
項目名 | — | — | 表示名からカラム名を解決する |
$p.getControl(name) |
項目名 | — | — | コントロールを取得する |
$p.getField(name) |
項目名 | — | — | 項目の外枠を取得する |
$p.getValue(name) |
項目名 | — | — | 生値(data-raw 優先)を読む |
$p.getData($control) |
jQuery | — | — | 送信バッファを参照する |
$p.set($control, v) |
jQuery | する | する | 値を書き換える(通常はこれ) |
$p.setValue($control, v) |
jQuery | する | しない | 表示だけ差し替える |
$p.setData($control) |
jQuery | しない | する | 画面の値をバッファへ写す |
用途別に選ぶなら次のようになります。
| やりたいこと | 使う関数 |
|---|---|
| 保存済みの生値を読みたい | $p.getValue(name) |
| 画面で入力中の値を読みたい | $p.getControl(name).val() |
| 値を変更して保存もしたい | $p.set($p.getControl(name), value) |
| 表示だけ変えて保存はしたくない | $p.setValue($p.getControl(name), value) |
val() で直接書き換えた値を保存対象にしたい |
$p.setData($p.getControl(name)) |
まとめ
$p の get / set 系関数を追いかけてみました。要点は以下のとおりです。
-
関数は 2 グループある — 項目名を受け取るグループ(
$p.getValueなど)と、jQuery オブジェクトを受け取るグループ($p.setなど)に分かれる -
$p.getControl()がすべての起点 — hidden 項目のColumnsとReferenceTypeからテーブル種別_カラム名の id を組み立てている -
$p.getValue()はdata-rawを最優先で返す — 数値項目や読み取り専用項目にはdata-rawが付くため、画面で入力しただけの値は取得できない -
data-rawはクライアント側では一切更新されない — 更新されるのはサーバが HTML を再描画したときだけ -
画面(DOM)と送信バッファ(
$p.data)は別物 —$p.set()は両方、$p.setValue()は DOM だけ、$p.setData()はバッファだけを更新する -
スクリプトから値を変えるときは
$p.set()を使う —val()だけで書き換えると$p.dataに載らず、更新時に送信されない
名前が似ている関数でも、内部で何を呼んでいるかを追うと役割がはっきり分かれています。スクリプトが思ったとおりに動かないときは、「いま触っているのは DOM なのかバッファなのか」を切り分けてみると原因にたどり着きやすくなります。