Chapter 6
JavaScriptでのプログラミング
この章は、INTER-Mediator Ver.10をもとに記載しました。
INTER-Mediatorはアプリケーションの基本的な動作を宣言的な記述、つまりページファイルと定義ファイルの作成で行います。その上で、さまざまな要求に応えるために、プログラミングが可能になっています。この章では、クライアントサイドで稼働するJavaScriptのプログラミングについて、INTER-Mediatorが持つAPIを中心に説明します。アプリケーション開発で使うことを意図したメソッドやプロパティについては、マニュアルのサイトにある『クライアント側でのJavaScriptの処理』にまとめてあります。JavaScriptの記述方法は、『2-3 JavaScriptプログラムの記述』に説明があります。この章では、その内容を受けて、実際のニーズに関わる内容を説明します。
6-1再合成を利用した検索ページ
コンテキストの検索条件やソート条件は、JavaScriptのプログラムにより追加することができることを、『2-5 検索と並べ替えに関する設定』の『JavaScriptで検索条件を付加する方法』でも説明しました。このセクションでは、その機能や、URLからのパラメーター取得の機能、ページの再合成を行利用することで、検索ページを作成します。検索のユーザーインターフェースについては『2-5 検索と並べ替えに関する設定』でも説明しましたが、検索条件をJavaScriptのプログラムで与えることで、より柔軟に要求へ応えることができます。
コンテキストの検索条件およびソート条件
『2-5 検索と並べ替えに関する設定』での説明の通り、コンテキスト定義には、queryおよびsortキーによって、検索条件とソート条件を付加することができます。これらの設定をもとに、単にデータベースのテーブルにアクセスするのではなく、必要なレコードに絞り込み、望む順序に並べたリレーションを得ることができます。この設定は、定義ファイルのコンテキストの中に行えば、固定的な設定となり、そのコンテキストでクエリーすなわちデータベースからの読み出しを行うときには常に適用されます。加えて、JavaScriptによって条件を追加することもできます。
JavaScriptによる条件の追加は、INTERMediator変数のオブジェクトに対するadditionalConditionおよびadditionalSortKeyプロパティへの追加によって実現されますが、これらのプロパティはセッター/ゲッターで実装されています。プロパティの変更時に、ローカルストレージやクッキーに確実に残すためにそのような実装になっています。しかしながら、条件の追加はプロパティの直接の変更ではなく、そのために用意されたメソッドを利用する方が確実です。
パラメーターを受け取るページ
URLの?の後に指定するパラメーターを追加して、リンク先のページで特定のデータだけを表示するという方法はよく使われます。『5-1 マスター/ディテール形式のナビゲーション』で説明したように、nav-controlキーを利用して、マスター/ディテール形式のユーザーインターフェースは宣言的な記述だけで作成できます。
このようなユーザーインターフェースを、nav-controlの仕組みを使わないで作りたい場合、パラメーターの受け渡しを利用することで実現します。一覧ページがlist.html、詳細ページがdatail.htmlであったとします。一覧ページから、特定のレコードの詳細を表示するボタンは、detail.html?id=35のようなURLにしておきます。ここで、idの後の数値は、コンテキストのもとになるテーブルの主キー値です。list.htmlでは、リスト6-1-1のような詳細リンク、あるいは詳細ボタンが各行に登場するように、リピーターの内部を作成しておきます。SPANタグやDIVタグで詳細ボタンを作りたい場合も、BUTTONタグと同様にonclick属性に別のページに移動する1行のJavaScriptのプログラムを作成しておきます。
<a href="detail.html?id=" data-im="context@id@#href">詳細</a>
<button onclick="location.href='detail.html?id=$';"
data-im="context@id@$onclick">詳細</button>
最初のAタグの場合、idの値が「12」なら「detail.html?id=12」へリンクが生成されます。BUTTONタグの場合だとonclick属性のプログラムは「location.href='detail.html?id=12';」となります。ターゲット指定の#や$については、『4-1 ターゲット指定』で説明しています。
詳細を表示するdetail.htmlでは、JavaScriptの標準機能でパラメーターは簡単に取得できますが、INTER-MediatorではJavaScriptのオブジェクトとして得られるメソッドINTERMediatorOnPage.getURLParametersAsArrayを用意しています。そのオブジェクトへの参照がparamだとすると(例えば、「params = INTERMediatorOnPage.getURLParametersAsArray()」)、リスト6-1-1のようなリンクでページに移動したら、param["id"] でidフィールドの値が得られるので、その値を追加の検索条件として付加すればいいでしょう。
ページの再合成
『4-2 ページを合成するときのルール』で説明したように、INTER-Mediatorによるページファイルをベースにしたデータベースの内容との合成(ページ合成)を、通常はページを表示したときに行います。しかしながら、このページ合成は、INTERMediator.constructメソッドを明示的に呼び出すことで実施されるようにしているのは、合成の処理を任意のタイミングで実施できるようにするためです。また、本コースのこれまでの部分は、ページ全体を合成することを基本としていましたが、ここでは、任意のコンテキストに関わる部分だけを再合成する手法で、「検索条件として与えたデータをもとに、新たにページを合成する」ということを行い、「検索ができるページ」を作成します。
演習プログラムで条件を指定する検索機能を持つページ(MySQL)
テキストフィールドに入れた文字列を、JavaScriptのインターフェースで指定する検索条件として与える形式のページを作成します。この演習は、MySQLとFileMakerで細かな点で異なるため、同一の演習をそれぞれのデータベース向けに別々に記述します。MySQLで演習をされる場合には、このまま進んでください。FileMakerで演習される方は、この演習の後にあるFileMaker向けの演習手順『プログラムで条件を指定する検索機能を持つページ(FileMaker)』で進めてください。
定義ファイルにデータベースアクセスに必要な設定を行う
ページファイルの作成
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def16.php"></script>
<script type="text/javascript">
INTERMediatorOnPage.doBeforeConstruct = function () {
const params = INTERMediatorOnPage.getURLParametersAsArray();
INTERMediator.clearCondition("postalcode");
if (params["q"]) {
INTERMediator.addCondition("postalcode",
{field: "f3", operator: "LIKE", value: params["q"]});
}
}
</script>
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table>
<thead>
<tr><th>郵便番号</th><th>住所</th></tr>
</thead>
<tbody>
<tr>
<td data-im="postalcode@f3"></td>
<td>
<span data-im="postalcode@f7"></span>
<span data-im="postalcode@f8"></span>
<span data-im="postalcode@f9"></span>
</td>
</tr>
</tbody>
</table>
</body>
</html>
ページの表示と結果の確認
SELECT * FROM postalcode WHERE (`f3` LIKE '1%') ORDER BY `f3` ASC LIMIT 10 OFFSET 0
http://localhost:9080/page16.html?q=16%25
SELECT * FROM postalcode WHERE (`f3` LIKE '1%') AND (`f3` LIKE '16%') ORDER BY `f3` ASC LIMIT 10 OFFSET 0
検索のためのユーザーインターフェースと連動する
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def16.php"></script>
<script type="text/javascript">
/* INTERMediatorOnPage.doBeforeConstruct = function () {
var params = INTERMediatorOnPage.getURLParametersAsArray();
INTERMediator.clearCondition("postalcode");
if (params["q"]) {
INTERMediator.addCondition("postalcode",
{field: "f3", operator: "LIKE", value: params["q"]});
}
} */
function search() {
const node = document.getElementById("criteria");
INTERMediator.clearCondition("postalcode");
if (node.value) {
const str = "%" + node.value + "%";
let param = {field: "__operation__", operator: "ex"};
INTERMediator.addCondition("postalcode", param);
param = {field: "f7", operator: "LIKE", value: str};
INTERMediator.addCondition("postalcode", param);
param = {field: "f8", operator: "LIKE", value: str};
INTERMediator.addCondition("postalcode", param);
param = {field: "f9", operator: "LIKE", value: str};
INTERMediator.addCondition("postalcode", param);
}
const context = IMLibContextPool.contextFromName("postalcode");
INTERMediator.construct(context);
}
</script>
</head>
<body>
<input type="text" id="criteria"/>
<button onclick="search()">検索</button>
<div id="IM_NAVIGATOR"></div>
<table>
<thead>
<tr><th>郵便番号</th><th>住所</th></tr>
</thead>
<tbody>
<tr>
<td data-im="postalcode@f3"></td>
<td>
<span data-im="postalcode@f7"></span>
<span data-im="postalcode@f8"></span>
<span data-im="postalcode@f9"></span>
</td>
</tr>
</tbody>
</table>
</body>
</html>
SELECT * FROM postalcode WHERE (`f3` LIKE '1%') AND (`f7` LIKE '%北%' OR `f8` LIKE '%北%' OR `f9` LIKE '%北%') ORDER BY `f3` ASC LIMIT 10 OFFSET 0
演習のまとめ
- JavaScriptのプログラムを利用すれば検索条件の追加が可能で、ページ合成前や、ボタンを押してページ合成ができます。後者の利用方は、「検索ページ」を構成するひとつの方法です。
- 検索条件を与えて検索をさせる場合、デバッグ情報に見えるSQLステートメントをよく確認して、意図した条件設定になるかを確認しましょう。
演習プログラムで条件を指定する検索機能を持つページ(FileMaker)
テキストフィールドに入れた文字列を、JavaScriptのインターフェースで指定する検索条件として与える形式のページを作成します。この演習は、MySQLとFileMakerで細かな点で異なるため、同一の演習をそれぞれのデータベース向けに別々に記述します。FileMakerで演習をされる場合には、このまま進んでください。MySQLで演習される方は、この演習の前にあるMySQL向けの演習手順『プログラムで条件を指定する検索機能を持つページ(MySQL)』で進めてください。
定義ファイルにデータベースアクセスに必要な設定を行う
ページファイルの作成
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def17.php"></script>
<script type="text/javascript">
INTERMediatorOnPage.doBeforeConstruct = function () {
var params = INTERMediatorOnPage.getURLParametersAsArray();
INTERMediator.clearCondition("postalcode");
if (params["q"]) {
INTERMediator.addCondition("postalcode",
{field: "f8", operator: "cn", value: params["q"]});
}
}
</script>
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table>
<thead>
<tr><th>郵便番号</th><th>住所</th></tr>
</thead>
<tbody>
<tr>
<td data-im="postalcode@f3"></td>
<td>
<span data-im="postalcode@f7"></span>
<span data-im="postalcode@f8"></span>
<span data-im="postalcode@f9"></span>
</td>
</tr>
</tbody>
</table>
</body>
</html>
ページの表示と結果の確認
https://gateway.docker.internal:443/fmi/data/vLatest/databases/TestDB/layouts/postalcode/_find {"sort":[{"fieldName":"f3","sortOrder":"ascend"}],"offset":"1","limit":"10","portal":[],"query":[{"f3":"*\u5317*"}]}
http://localhost:9080/page17.html?q=北
https://gateway.docker.internal:443/fmi/data/vLatest/databases/TestDB/layouts/postalcode/_find {"sort":[{"fieldName":"f3","sortOrder":"ascend"}],"offset":"1","limit":"10","portal":[],"query":[{"f3":"*\u5317*","f8":"*\u5317*"}]}
検索のためのユーザーインターフェースと連動する
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def17.php"></script>
<script type="text/javascript">
/* INTERMediatorOnPage.doBeforeConstruct = function () {
var params = INTERMediatorOnPage.getURLParametersAsArray();
INTERMediator.clearCondition("postalcode");
if (params["q"]) {
INTERMediator.addCondition("postalcode",
{field: "f8", operator: "cn", value: params["q"]});
}
}*/
function search() {
const node = document.getElementById("criteria");
INTERMediator.clearCondition("postalcode");
if (node.value) {
let param = {field: "__operation__", operator: "ex"};
INTERMediator.addCondition("postalcode",param);
param = {field: "f7", operator: "cn", value: node.value};
INTERMediator.addCondition("postalcode",param);
param = {field: "f8", operator: "cn", value: node.value};
INTERMediator.addCondition("postalcode",param);
param = {field: "f9", operator: "cn", value: node.value};
INTERMediator.addCondition("postalcode",param);
}
const context = IMLibContextPool.contextFromName("postalcode");
INTERMediator.construct(context);
}
</script>
</head>
<body>
<input type="text" id="criteria"/>
<button onclick="search()">検索</button>
<div id="IM_NAVIGATOR"></div>
<table>
<thead>
<tr><th>郵便番号</th><th>住所</th></tr>
</thead>
<tbody>
<tr>
<td data-im="postalcode@f3"></td>
<td>
<span data-im="postalcode@f7"></span>
<span data-im="postalcode@f8"></span>
<span data-im="postalcode@f9"></span>
</td>
</tr>
</tbody>
</table>
</body>
</html>
https://gateway.docker.internal:443/fmi/data/vLatest/databases/TestDB/layouts/postalcode/_find {"sort":[{"fieldName":"f3","sortOrder":"ascend"}],"offset":"1","limit":"10","portal":[],"query":[{"f9":"*\u5317*"},{"f8":"*\u5317*"},{"f7":"*\u5317*"}]}
演習のまとめ
- JavaScriptのプログラムを利用すれば検索条件の追加が可能で、ページ合成前や、ボタンを押してページ合成ができます。後者の利用方は、「検索ページ」を構成するひとつの方法です。
- 検索条件を与えて検索をさせる場合、デバッグ情報に表示されるXML共有のURLの一部をよく確認して、意図した条件設定になるかを確認しましょう。
ページ合成およびブラウザー判定に使用したAPI
このセクションのプログラムで使用したAPIや関連するAPIについて、まとめておきます。
INTERMediator.construct(context, recordset)
ページ全体あるいは部分の合成を行います。データベースの内容を表示するには、必ずこのメソッドは呼び出す必要がありますが、ページをロードしたときには自動的にこのメソッドが呼び出されます。Ver.5.4-devの途中まではonloadイベントでこのメソッドの呼び出しが記述される必要がありましたが、現在は記述の必要はありません。記述の必要があるのは、ページ表示後に表示内容の更新を意図的に行うような場合です。(返り値はなし)
引数 | 指定内容 |
---|---|
context | trueあるいは省略ならページ全てを合成する。contextを指定すると、そのコンテキストのみを再合成するが、その場合はIMLIbContext変数をクラスとしたコンテキストへの参照を指定する |
recordset | ページ全体の合成では省略する。ここにオブジェクトの配列の形式でレコードを指定すると、そのレコードに関連したレコードを、contextで指定したコンテキストに対して再合成する |
INTERMediatorOnPage.INTERMediatorCheckBrowser(deleteId)
定義ファイルの設定、あるいはparams.phpを参照して、サポートしているブラウザーなのかどうかを判定します。サポートしていない場合には既定のエラーメッセージのみを画面に表示します。なお、このメソッドは、ページをロードするときに自動的に呼びだされるため、通常は使用することはないと思われます。
引数 | 指定内容 |
---|---|
deleteId | 判定の後、非対応ブラウザーであれば削除されるBODY要素内の要素のid属性の値 |
[返り値] | 対応ブラウザーならtrue、そうでなければfalse |
INTERMediatorOnPage.doBeforeConstruct() = function() {...}
ページ合成が行われる直前で呼び出されるメソッドで、アプリケーションはこれを呼び出すのではなく、アプリケーション側で定義しておくことで、INTER-Mediatorによって呼び出されます。引数および返り値はありません。
INTERMediatorOnPage.doAfterConstruct() = function() {...}
ページ合成が終わったときに呼び出されるメソッドで、アプリケーションはこれを呼び出すのではなく、アプリケーション側で定義しておくことで、INTER-Mediatorによって呼び出されます。引数および返り値はありません。
INTERMediatorOnPage.isAutoConstruct
ページをロードしたときの自動的なページ合成を行うかどうかを指定します。既定値はtrueです。ページの自動合成をさせたくないような場合、これをfalseとします。例えば、doBeforeConstructメソッドの中で特定の条件が成り立てばこのプロパティにfalseを代入して、ページ合成処理をさせないようにできます。このプロパティの値に関係なくdoBeforeConstructメソッドは定義されていれば実行されますが、doAfterConstructメソッドはこのプロパティがtrueの時のみ実行されます。
コンテキストの検索条件を追加指定するAPI
INTERMediatorOnPage.getURLParametersAsArray()
自分自身のページのURLに含まれるパラメーターを、オブジェクトとして返します。
引数 | 指定内容 |
---|---|
[返り値] | URLのパラメーターにある「キー=値」のそれぞれのセットについて、キーをプロパティ名、値をそのプロパティに対する値として持つオブジェクト |
INTERMediator.clearCondition(contextName)
コンテキストに対して追加される検索条件を、指定したコンテキストに対して消去します。(返り値なし)
引数 | 指定内容 |
---|---|
contextName | コンテキスト名、すなわち定義ファイルのコンテキスト定義にあるnameキーの値 |
INTERMediator.addCondition(contextName, criteria)
コンテキストに対する検索条件を追加します。(返り値なし)
引数 | 指定内容 |
---|---|
contextName | コンテキスト名、すなわち定義ファイルのコンテキスト定義にあるnameキーの値 |
criteria | 検索条件を示すオブジェクト。プロパティはfield、operator、valueで、それぞれ定義ファイルでのqueryキーの配列におけるキーと対応している |
addConditionやaddSortKeyメソッドを利用するときの注意点があります。これらのメソッドで登録した条件は、クライアントのブラウザーのセッションストレージに記録されます。その結果、以前にそのページで設定していた検索条件やソート対象フィールドを、改めてページ表示するときに自動的に適用されることになります。そのため、検索条件の設定時にはINTERMediator.clearCondition()メソッドで、条件をクリアをしないと、既存の条件に追加されてしまうことになります。clearConditionにコンテキスト名の引数をつければ、指定したコンテキストの検索条件をクリアします。また、引数を省略すると、追加の検索条件すなわちaddtionalConditonプロパティが何も設定されていない状態になります。
なお、ページを閉じた後、検索条件がセッションストレージに保持され、そのページを再度開いた場合に復元されてしまうと思わぬ副作用ももたらします。あるコンテキスト定義が複数の箇所に流用されているような場合には要注意です。ページを開いた後、追加条件なしでコンテキストを利用し、その後、条件を追加してコンテキストを利用したとします。そして、改めてそのページを開くと、最初のコンテキスト利用では後の利用での条件が適用されてしまい、意図しない検索条件が付与されることになります。この場合、ページ移動間での検索条件の保持が不要なら、INTERMediatorOnPage.doBeforeConstructメソッドの最初に「INTERMediator.clearCondition();」を呼び出して、ページ開始時には常に検索条件がクリアされているようにすることで回避はできます。しかしながら、設計上はこうしたコンテキストの使い回しは行わないで、コンテキスト定義自体を分離して別々のコンテキスト定義を行いそれぞれを利用するのが適切です。
このセクションのまとめ
コンテキストに対する検索条件は、定義ファイルのコンテキスト定義に追加できるだけでなく、『2-5 検索と並べ替えに関する設定』の『ユーザーインターフェースの定義だけで検索条件を付与する』では、宣言的な記述だけで検索処理をページに組み込む方法を説明しました。これらの方法に加えてJavaScriptでの追加も可能です。JavaScriptを利用すれば、プログラムでデータに対するさまざまな処理が可能です。検索で指定する文字列とデータベースのフィールドに入っているデータに直接関係がないような場合でも対処できます。例えば、日付データがひとつのフィールドに入っていて、そこへの検索において、年月日を別々にテキストフィールドやポップアップメニューで指定したいような場合が相当します。
6-2コンテキストオブジェクト
データベースから取り出したデータは単にノードに合成するだけでなく、データそのものも残してあります。さらに、単に残すだけでなく、ページ上に展開したデータや要素などの情報を保持して、同一フィールドの要素の情報を連動させるなど、クライアントサイドの「モデル」としての機能が組み込まれています。このような、コンテキスト定義を現実のデータや要素と連動できるようにするオブジェクトを「コンテキストオブジェクト」と呼びます。エンクロージャーがひとつあれば、コンテキストオブジェクトもひとつが作られます。
コンテキストオブジェクト
INTER-Mediator上でプログラムを作る場合、コンテキストオブジェクトの存在を知っておくことで、ページ上のデータのやりとりが非常に効率的になります。定義ファイルに定義する「コンテキスト」については、「コンテキスト定義」と呼ぶことにします。
コンテキストオブジェクトは、コンテキスト定義をもとにして、データベースから取り出されたデータなどを保持しているオブジェクトであり、INTER-Mediatorは自動的に作成されます(図6-2-1)。基本的には、エンクロージャーの数だけ、コンテキストオブジェクトの実体が作られます。したがって、ひとつのコンテキスト定義から、ひとつのコンテキストオブジェクトの場合もあれば、複数存在することもあります。APIを利用すれば、コンテキスト名からコンテキストオブジェクトを参照できます。また、nav-controlでのマスターおよびディテールのコンテキストオブジェクトを直接取り出すメソッドもあります。
コンテキストオブジェクトとそのプロパティ
IMLibContext変数が参照するオブジェクトをクラスとして生成するコンテキストオブジェクトには、データベースから取り出し、ページ上のいずれかのノードに展開したデータが保持されています。ページ展開で得られたデータを取り出すには、このコンテキストオブジェクトに保持された値を利用するのがひとつの方法です。表6-2-1には、コンテキストオブジェクトで利用することがありそうなプロパティをまとめておきました。
プロパティ | 内容 |
---|---|
contextName | コンテキスト名 |
enclosureNode | エンクロージャーのノードへの参照 |
repeaterNodes | 展開前に初期状態として保持したリピーターで、ノードへの参照の配列 |
store | データベースから取得し、ページに展開したデータ |
storeCaptured | ページ展開直後のページに展開したデータ(Control+Shift+Zによる復帰をサポートするため) |
表6-2-1で、ページに展開したデータは、storeプロパティに保持されています。storeプロパティは若干複雑なオブジェクト構成になっています。レコードを示すキー値として、「主キーフィールド名=フィールドの値」の形式を持ちます。主キーフィールド名は、コンテキスト定義のkeyキーに対する値です。keyキーの値が「id」だった場合、例えば、「id=3」などがレコードを示すキーになります。そしてひとつのレコードは、フィールド名がプロパティになり、その値がデータベースから得られた値になります。例えば、表6-2-2のようなリレーションが得られた場合、3つのフィールドがいずれもページ上に展開されれば、storeプロパティの値はリスト6-2-1のような形式になります。
id | text1 | num1 |
---|---|---|
1 | suger | 4314 |
2 | salt | 2983 |
3 | saurce | 9223 |
{
"id=1": {
"id": "1",
"text1": "suger",
"num1": "4314"
},
"id=2": {
"id": "2",
"text1": "salt",
"num1": "2983"
},
"id=3": {
"id": "3",
"text1": "saurce",
"num1": "9223"
}
}
storeプロパティを利用すれば、例えば、ページに展開したデータについて、複数のレコードの同一のフィールドの値を串刺しで取り出したり、同じレコードの特定のフィールドのデータを取り出すことができます。データはページ上に見えていますが、その値を取り出すにはDOMモデルに従って複雑なプログラムを書かざるを得ません。しかしながら、コンテキストオブジェクトであれば、データをページ上の要素とは独立して取り出すことができます。なお、ページ上に展開していないとstoreプロパティには保持されないので、表示は不要だがコンテキストオブジェクトに必要なフィールドは、type属性がhiddenのINPUTタグ等で、ページ内への展開を記述しておく必要があります。
コンテキストのデータの書き込み処理
データの取り出しは、storeプロパティを探るのが一番効率的ですが、コンテキストへのデータの設定は、用意されているメソッドを利用するのが良いでしょう。基本的に、コンテキストの特定のレコードの特定のフィールドに値を設定すると、そのフィールドとバインドしているページ上の要素でも設定した値が見えます。また、その値をデータベースへ書き込む処理も行うメソッドもあります。どういうメソッドがあるかは、この後の演習と記事を参照してください。
演習フィールドを更新するボタンを設置する
承認ワークフローを実装するようなアプリケーションにおいて、「承認した」ということを記録するために、承認日時とログイン名を記録するのがひとつの方法としてあります。その時、日時や名前などを手入力はしたくはないと考えるところでしょう。そこで、「ボタンを押すと、指定のフィールドに現在の日時が入力される」というプログラムをボタンで呼び出せば、承認ボタンに対するもっとも重要な要求が満たされます。もちろん、ユーザーに応じて異なる認証権限を与えるアクセス権設定や、承認のキャンセルをどうすればいいかなど、ワークフローに関わるアプリケーションは状態の遷移に伴って多数の要件が絡みます。この演習では、その処理の一部だけを紹介するものとなります。
定義ファイルにデータベースアクセスに必要な設定を行う
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
ページファイルの作成
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def18.php"></script>
<script type="text/javascript">
// 次のステップでここにプログラムを追加
</script>
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table>
<thead>
<tr><th>案件番号</th><th>承認日時</th><th></th><th></th></tr>
</thead>
<tbody>
<tr>
<td data-im="testtable@id"></td>
<td><input type="text" data-im="testtable@dt2"/></td>
<td><span data-im="testtable@dt2"></span></td>
<td>
<button onclick="approval($)"
data-im="testtable@id@$onclick">承認</button>
</td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
function approval(id) {
const currentDT = INTERMediatorLib.dateTimeStringISO();
const context = IMLibContextPool.contextFromName("testtable");
context.setDataWithKey(id, "dt2", currentDT)
}
ページ上のプログラムを実行してみる
演習のまとめ
- JavaScriptのプログラム内で、データベースに対するCRUDおよびコピーの処理が可能です。
- 特定のフィールドに直接値を設定することなどができますが、その後に対応するコンテキストを再描画することで、変更した結果がページに反映されます。
コンテキストオブジェクトへのデータの設定と取り出し
IMLibContext変数によって作られるオブジェクトでは、以下のメソッドも利用できます。IMLibContextの部分は、例えば、IMLibContextPool.contextFromName("...")を使い引数に指定したコンテキスト名より得られたコンテキストオブジェクトの変数を指定します。いくつかのプログラム例を、メソッドの説明の後に示します。
IMLibContext.setValue(recKey, key, value, nodeId, target)
コンテキスト内の指定したレコードの指定したフィールドに値を設定する。ページ合成時に、INTER-Mediatorは自動的にこのメソッドを呼び出して、コンテキストとページ内の要素との対応情報を保持する。ページ合成後に、nodeIdとtargetを省略してこのメソッドを呼び出すと、値を保存すると同時に、他のコンテキストの同じテーブルの同じレコードの同じフィールドとバインディングしている値も更新するので、結果として各要素に表示する値も更新される。ただし、データベース処理は行わず、ローカルのコンテキストの値を更新するのみである。
引数 | 指定内容 |
---|---|
recKey | storeプロパティのオブジェクトのプロパティ(id=1などのキー)を指定する |
key | フィールド名 |
value | 値 |
nodeId | [省略可能]コンテキストのこの値とバインディングした要素のid属性値 |
target | [省略可能]バインディングした要素のターゲット。ターゲットなしは "" を指定 |
[返り値] | 更新された要素のid属性値の配列 |
IMLibContext.setDataWithKey(pkValue, key, value)
コンテキスト内の指定したレコードの指定したフィールドに値を設定し、バインディングされている他の要素への更新を行うとともに、データベースへの更新を行う。
引数 | 指定内容 |
---|---|
pkValue | 主キー(コンテキスト定義のkeyキーで指定したフィールド)の値のみで対象レコードを指定 |
key | フィールド名 |
value | 値 |
[返り値] | 更新された要素のid属性値の配列 |
IMLibContext.setDataAtLastRecord(key, value)
コンテキストの最後のレコードにある指定したフィールドに値を設定し、バインディングされている他の要素への更新を行うとともに、データベースへの更新を行う。
引数 | 指定内容 |
---|---|
key | フィールド名 |
value | 値 |
[返り値] | (なし) |
IMLibContext.getValue(recKey, key)
コンテキスト内の指定したレコードの指定したフィールドの値を得る。なお、マスター/ディテール形式のユーザーインターフェースにおいて、keyに "_im_button_master_id" を指定すると、「詳細」ボタンの要素に設定されているid属性値を得られるので、プログラムでクリック操作をしたい時には利用できる。
引数 | 指定内容 |
---|---|
recKey | storeプロパティのオブジェクトのプロパティ(id=1などのキー)を指定する |
key | フィールド名 |
[返り値] | (なし) |
IMLibContext.getDataAtLastRecord(key)
コンテキストの最後のレコードにある指定したフィールドの値を得る。
引数 | 指定内容 |
---|---|
key | フィールド名 |
[返り値] | 最後のレコードの指定したフィールドの値 |
コンテキストの情報取得
コンテキストオブジェクトからコンテキスト定義を得るなどの情報取得のためのメソッドとして以下のようなものが利用できます。
IMLibContext.getContextDef()
定義ファイルに記述したコンテキスト定義を得る。返り値はひとつのコンテキスト定義を示すオブジェクト。
IMLibContextPool.contextFromName(contextName)
引数に指定したコンテキスト名に対するコンテキストオブジェクトをひとつだけ返します。定義ファイルのコンテキスト定義よりひとつのコンテキストしか生成していない場合に利用します。
引数 | 指定内容 |
---|---|
contextName | コンテキスト名、すなわち定義ファイルのコンテキスト定義にあるnameキーの値 |
[返り値] | コンテキストオブジェクトへの参照 |
IMLibContextPool.getContextFromName(contextName)
引数に指定したコンテキスト名に対するコンテキストオブジェクトの配列を返します。定義ファイルのコンテキスト定義から複数のコンテキストを生成している場合に利用します。
引数 | 指定内容 |
---|---|
contextName | コンテキスト名、すなわち定義ファイルのコンテキスト定義にあるnameキーの値 |
[返り値] | コンテキストオブジェクトへの参照の配列 |
IMLibContext.getContextInfo(nodeId, target)
要素のid属性値とターゲットから、コンテキスト情報を得る。
引数 | 指定内容 |
---|---|
nodeId | 要素のid属性値 |
target | 要素のターゲット。ターゲットなしは "" を指定 |
[返り値] | 要素とバインディングしているコンテキスト情報({context: this, record: recKey, field: key}形式のオブジェクト) |
IMLibContext.getContextValue(nodeId, target)
要素のid属性値とターゲットから、コンテキストの値を得る。
引数 | 指定内容 |
---|---|
nodeId | 要素のid属性値 |
target | 要素のターゲット。ターゲットなしは "" を指定 |
[返り値] | 引数で指定した要素とターゲットにバインディングしている値 |
コンテキストを利用したサンプルプログラム
プログラムの簡単なサンプルを示します(リスト6-2-2)。INTER-Mediatorで作成したアプリケーションでは、ひとつのコンテキスト定義をもとにしたページ上のコンテキストはひとつだけという場合がよくあります。その時、コンテキストオブジェクトを参照するには、IMLibContextPool.contextFromName(...)を利用できます。引数はコンテキストの "name" キーの値、つまりコンテキスト名を指定します。もし、コンテキストが複数ある場合には、IMLibContextPool.getContextFromName(...) を使用して、該当するコンテキストを返された配列から取り出さなければなければなりません。
ひとつのコンテキストにレコードがひとつだけという場合はよくあります。つまり、コンテキスト定義の "records" キーの値を1にしているような場合です。コンテキストのメソッドの中に「最後のレコード」に対応するものが用意されていますが、もちろん複数のレコードの最後のレコードに適用できると同時に、1レコードしかない場合には、確実にその1レコードに対して処理をするメソッドとしても利用できます。getDataAtLastRecordメソッドで引数にフィールド名を指定すれば、データベースの値を取得できます。また、setDataAtLastRecordメソッドを利用すれば、コンテキストの値を更新してバインディングしている他の要素の値も更新するとともに、データベースの該当フィールドを更新します。
var context = IMLibContextPool.contextFromName("contextName");
var idValue = context.getDataAtLastRecord("id");
context.setDataAtLastRecord("price", 350);
もし、複数レコードがあるようなコンテキストを変数contextで参照していたとしたら、Object.keys(context.store)で、レコードを指定するキーが配列で得られます。必要であれば、そのキーをもとに順番に処理をしたり、あるはfor..inを利用するなどして、コンテキストの各レコードに対して処理を行うことができます(リスト6-2-3)。
var context = IMLibContextPool.contextFromName("productList");
for (var recKey in context.store) {
var unitPrice = context.getValue(recKey, "unitprice");
if (unitPrice > 1000) {
context.setDataWithKey(recKey, "unitprice", unitPrice * 1.05);
}
}
このように、クライアントサイドのプログラムによる値の変更は、コンテキストを中心に考えれば、プログラムはシンプルに作成できます。
内部では、コンテキストオブジェクトを使ってさまざまな作業を行います。例えば、同一のフィールドを2つのテキストフィールドにバインドしたとき、一方のデータを変更してデータベースへの更新を行うとともに、もう一方に表示するテキストも更新されます。こうした動作の基礎になっているのがコンテキストオブジェクトです。
JavaScriptのプログラムを作る上で一番便利なのは、ページ上の要素にバインドした値が保持されていることです。もちろん、要素から値を取り出すAPIもありますが、要素の種類に応じてプログラムをつくり分ける必要があります。コンテキストオブジェクトは単に値だけがあるので、要素の種類を気にかける必要はありません。また、特定のコンテキストだけを再合成する仕組みもあります。実際の使用方法は、この章で紹介するプログラムとともに示します。
ローカルコンテキスト
ブラウザーでページ合成を行えば、ローカルコンテキストというオブジェクトがひとつ生成されます。このコンテキストは、データベースとは連動しておらず、クライアントで独立して利用できます。その値はローカルストレージあるいはクッキーに値を保持することで、一度コンテキスト内に作った値を、ページを閉じて開いた後にも復帰させることができます。
ローカルコンテキストは、レコードという構造は持たないキー/バリュー形式のストレージです。ページファイル内の要素とバインドでき、要素のdata-im属性に例えば、リスト6-2-4のように記述します。
<input id="myVal1" data-im="_@myvalue1" />
data-im属性において、コンテキスト名に「_」(半角のアンダーライン)を指定します。その後に、任意のフィールド名を与えます。これにより、ローカルコンテキストのmyvalue1フィールドとテキストフィールドがバインドされます。テキストフィールド内の値を変更すると、ローカルコンテキストのmyvalue1フィールドが更新されます。そして、ページを閉じてもローカルストレージ等にmyvalue1フィールドの値を残し、再度ページを開くと、ローカルコンテキストのmyvalue1フィールドは以前の値になります。そして、バインドしているテキストフィールドにも、以前の値が見えるようになります。ローカルコンテキストのAPIを利用すれば、値の取得や設定が可能です。
なお、『2-5 検索と並べ替えに関する設定』の『ユーザーインターフェースの定義だけで検索条件を付与する』で説明した、検索ページを宣言的な記述だけで作成する手法は、ローカルコンテキストを応用したものです。
ローカルコンテキストの値の設定や取り出しをJavaScriptのプログラムで行う場合、以下のメソッドを利用できます。ローカルコンテキストは、変数名IMLibLocalContextで参照できるオブジェクトです。
IMLibLocalContext.getValue(key)
ローカルコンテキスト内の引数keyに指定したキーに対する値を返す。
IMLibLocalContext.setValue(key, value)
引数keyに指定したキーに対する値valueをローカルコンテキストに設定する。
ローカルコンテキストの初期値
ローカルコンテキストの値は、例えばテキストフィールドにバインドしているのであれば、テキストフィールドに値を入力したときに設定されます。加えて、Ver.5.4-devより、定義ファイルへの設定を行えば、ローカルコンテキストの値の初期値を設定することができます。リスト6-2-5のように、IM_Entry関数の2つ目の引数(オプション指定)に、local-contextキーで配列を指定します。リストでは、ターゲット指定で、「_@pageTitle」で参照されるコンテキストに対して、初期値として、「IM Samples」という文字列を設定しています。複数のキーに値を与えるには、keyキーとvalueキーを持つ連想配列を書き並べます。
IM_Entry(
array( /* コンテキスト */ ),
array(
"local-context" => array(
array("key" => "pageTitle", "value" => "IM Samples"),
),
),
array( /* DB接続定義 */ ),
false
);
この定義ファイルを読み込んでいるページファイルにおいて、例えば、ヘッダー部のTITLEタグで、リスト6-2-6のようにローカルコンテキスト参照をしたとします。すると、ページのタイトルには「IM Samples」という文字列が設定されます。データベースからのデータを手軽にバインドできる一方、なんでもデータベースに記録しなければならないのなら、データベースに多様なデータが保存されることになります。ビジネスに直結したデータは当然ながらデータベースに保存するとしても、ちょっとした見栄えを良くするためだけに使う付随的なデータもビジネスロジックを交えて管理するのはかえって大変です。ここではページタイトルを例に出しますが、例えばページタイトルはカスタマイズしたいけれども、全部のページは同じということであれば、このようにコンテキストで与えておくか、あるいは『2-6 設定ファイルparams.php』で説明するようにparams.phpファイルに記述しておくことでも対処ができます。
<html>
<head>
<title data-im="_@pageTitle"></title>
<script type="text/javascript" src="context_of_above.php"></script>
</head>
:
このセクションのまとめ
コンテキストオブジェクトは、INTER-Mediatorの稼働においては重要な位置を占めますが、プログラミングを行う段階にならないと、その存在は意識することはまずないでしょう。データ処理を絡めたプログラムの作成が必要になる場合には、コンテキストオブジェクトの存在を忘れないようにしましょう。
6-3データベースへの書き込みを直接行う
テキストフィールドなどは「バインド」という手法で、自動的にデータベースと結合されているので、読み書きのためのプログラムは一切追加しないでも、データベースとページ上の表示は同期されます。しかしながら、バインドとは別にデータベースとのやりとりをしたい場合があります。表示に関しては、計算プロパティ(『4-4 計算プロパティの設定』)を利用する方法などがありますが、逆に更新ではJavaScriptのプログラムを利用して、データベースへの直接の書き込みが可能です。
データベース処理メソッドの利用
INTER-Mediatorのクライアント側のプログラムには、データベースの4つの基本操作であるCRUD(Create Read Update Delete)、そしてレコードのコピーに対応するメソッドがあり、フレームワークの処理に利用されています。他に、ファイルのアップロードや、パスワードの変更のメソッド等も利用できますが、アプリケーションで利用したいメソッドは、CRUDおよびコピーのメソッドと思われます。それぞれ、パラメーターをどのように指定するのかということで説明は終わってしまいますが、個別のメソッドの利用方法は演習の後にまとめておきます。
なお、基本的なデータベース処理については、前の節で説明したコンテキストに対するsetDataWithKeyメソッドや、コンテキストのstoreプロパティの参照でも可能です。この節で紹介するデータベース処理は、それらコンテキストでうまくできないような処理や一括処理をやりたいような場合に利用することになるでしょう。
データベースへの直接的な処理に利用できるAPI
データベース処理を行うメソッドの引数には、通信終了後に呼び出される関数を記述するcompletion引数があります。この関数は引数をひとつ設定します。呼び出すときに引数に設定する値については、関数ごとに違いますので、以下の記述の表の中を参照してください。
なお、各メソッドについては、返り値はありません。成功時と失敗時にクロージャーの呼び出しがあります。失敗時のクロージャーは引数はありませんが、成功時のクロージャーにはひとつだけ引数が設定され、その引数に与えられるオブジェクトは、リスト6-3-1のような形式になります。つまり、左側のキーを利用すれば、右側に記載した内容のデータが得られます。なお、キーに対する値は操作によってはあったりなかったりします。
{
dbresult: /* クエリー結果、レコードに対応するオブジェクトの配列 */,
resultCount: /* 検索して得られたレコード数 */,
totalCount: /* 条件に合致するレコード数 */,
newRecordKeyValue: /* 新規レコードの主キー値 */,
newPasswordResult: /* パスワード変更の結果 */,
registeredId: /* クライアント間連携で使うコード */,
nullAcceptable: /* null値を使うかどうか */
}
INTERMediator_DBAdapter.db_createRecord_async(args, successProc, failedProc)
指定したコンテキストに新たなレコードを作成します。既定値には引数で指定するものの他に、コンテキストに定義したdefault-valuesおよび、INTERMediator.additionalFieldValueOnNewRecordプロパティに設定した既定値についても追加されます。
引数 | 指定内容 |
---|---|
args | 以下のname, datasetプロパティを持つオブジェクト |
→name | 処理対象のコンテキスト名を文字列で指定する |
→dataset | {field:xx, value:xx}の形式のオブジェクトの配列。新しいレコードに対して指定フィールドに値を設定する |
completion, successProc | データベース処理が成功した後に呼び出されるクロージャー |
failedProc | データベース処理が失敗した後に呼び出されるクロージャー |
[返り値] | 作成したレコードのキーフィールドの値(引数completionの関数呼び出しの引数に設定される値も同様) |
INTERMediator_DBAdapter.db_query_async(args, successProc, failedProc)
指定したコンテキストに対して検索を行います。検索条件およびソート条件として、引数だけでなく、コンテキスト定義のquery、sortキーによる指定、INTERMediator.additionalConditionおよびINTERMediator.additionalSortKeyプロパティの定義についても適用されます。さらに、ローカルコンテキストにあるターゲット指定が "_@condition:....", "_@valueofaddorder:....", "_@limitnumber:...." の指定についても反映されます。
引数 | 指定内容 |
---|---|
args | 以下のname〜primaryKeyOnlyプロパティを持つオブジェクト |
→name | 処理対象のコンテキスト名を文字列で指定する |
→records | 取り出すレコード数。省略するとコンテキスト定義の値。uselimitも参照 |
→fields | 取り出すフィールド一覧(省略可能)。Ver.5.3現在この情報は未使用 |
→parentkeyvalue | 関連レコードの検索時に利用(省略可能) |
→conditions | 追加の検索条件で、{field: xxx, operator: xxx, value: xxxx}形式のオブジェクトの配列(省略可能) |
→useoffset | この引数がtrueでINTERMediator.startFromプロパティが設定されていれば、オフセットを指定する(省略可能) |
→uselimit | 取り出すレコード数としてtrueならINTERMediator.pagedSize、falseあるいは省略ならこのオブジェクトのrecordsプロパティが指定される |
→primaryKeyOnly | trueなら検索条件は主キーフィールドのみを指定してデータベースアクセスする(省略可能) |
completion, successProc | データベース処理が成功した後に呼び出されるクロージャー |
failedProc | データベース処理が失敗した後に呼び出されるクロージャー |
[返り値] | 以下のプロパティを持ったオブジェクト(引数completionの関数呼び出しの引数に設定される値も同様) |
→recordset | 検索して得られたレコード。1レコードはフィールドをキーとしたオブジェクトで、その配列がこのプロパティの値 |
→totalCount | 検索条件に合致したレコードの総数 |
→count | 実際に取り出したレコード数 |
→registeredid | このコンテキストの登録ID値。マルチユーザー利用での同期のときに利用される |
→nullAcceptable | データベースがnull値をサポートすればtrue。Ver.5.3現在、積極的な利用はされていない情報 |
INTERMediator_DBAdapter.db_update_async(args, successProc, failedProc)
指定したコンテキストに対して既存のレコードの更新を行います。検索条件として、引数だけでなく、コンテキスト定義のqueryキーによる指定、INTERMediator.additionalFieldValueOnUpdateプロパティの設定も適用されます。
引数 | 指定内容 |
---|---|
args | 以下のname, conditions, datasetプロパティを持つオブジェクト |
→name | 処理対象のコンテキスト名を文字列で指定する |
→conditions | 検索条件で、{field: xx, operator: xx, value: xx}の形式のオブジェクトの配列。 |
→dataset | {field:xx, value:xx}の形式のオブジェクトの配列。指定フィールドに値を設定する |
completion, successProc | データベース処理が成功した後に呼び出されるクロージャー |
failedProc | データベース処理が失敗した後に呼び出されるクロージャー |
[返り値] | 更新したレコード。1レコードはフィールドをキーとしたオブジェクトで、その配列がこのプロパティの値(引数completionの関数呼び出しの引数に設定される値も同様) |
INTERMediator_DBAdapter.db_delete_async(args, successProc, failedProc)
指定したコンテキストのレコードを削除します。検索条件として、引数だけでなく、コンテキスト定義のqueryキーによる指定、INTERMediator.additionalFieldValueOnDeleteプロパティの設定も適用されます。
引数 | 指定内容 |
---|---|
args | 以下のname, conditionsプロパティを持つオブジェクト |
→name | 処理対象のコンテキスト名を文字列で指定する |
→conditions | 検索条件で、{field: xx, operator: xx, value: xx}の形式のオブジェクトの配列。 |
completion, successProc | データベース処理が成功した後に呼び出されるクロージャー |
failedProc | データベース処理が失敗した後に呼び出されるクロージャー |
[返り値] | オブジェクトが返るが、アプリケーションにとって有用な情報はない |
INTERMediator_DBAdapter.db_copy_async(args, successProc, failedProc)
指定したコンテキストに対して、検索結果のレコードに対する複製レコードを作成します。検索条件として、引数だけでなく、コンテキスト定義のqueryキーによる指定も適用されます。
引数 | 指定内容 |
---|---|
args | 以下のname, conditions, associatedプロパティを持つオブジェクト |
→name | 処理対象のコンテキスト名を文字列で指定する |
→conditions | 検索条件で、{field: xx, operator: xx, value: xx}の形式のオブジェクトの配列。 |
→associated | 複製する関連レコードの指定で、{name: xx, field: xx, value: xx}の形式のオブジェクトの配列。nameはコンテキスト名、fieldは外部キーフィールド名、value外部キーフィールドの値を指定する |
completion, successProc | データベース処理が成功した後に呼び出されるクロージャー |
failedProc | データベース処理が失敗した後に呼び出されるクロージャー |
[返り値] | {newKeyValue: xxx, recordset: xxx}形式のオブジェクトで、前者が新規に作成されたレコードの主キー値、後者は新規に作成されたレコードのオブジェクトで、フィールド名がプロパティ(引数completionの関数呼び出しの引数に設定される値も同様) |
このAPIを使ったサンプルプログラムとしては以下のようなものです。おおむね、前の節の演習と同様な処理になります。INTERMediator_DBAdapter.db_update_asyncは非同期通信処理を行うので、原則的にはこの呼び出し後には何も記述がないのが一般的でしょう。通信後の処理は、引数にクロージャーとして記述します。通信成功時には、コンテキストの再描画を行い、書き込んだ結果を反映させています場合によっては変数resultの結果からデータベース処理結果を取り出すこともできます。失敗時には単にアラートを出すだけになっています。
function approval(id) {
const currentDT = INTERMediatorLib.dateTimeStringISO();
const args = {
name: "testtable", // 更新対象となるコンテキスト名
conditions:[{field: "id", operator: "=", value: id}], // 更新対象のレコード検索条件
dataset:[{field: "dt2", value: currentDT}] // 更新するフィールドとデータ
}
INTERMediator_DBAdapter.db_update_async(args,
(result) => { // 通信成功時に呼び出される
const context = IMLibContextPool.contextFromName("testtable");
INTERMediator.construct(context);
},
() => { // 通信失敗時に呼び出される
alert("Error");
}
);
}
サーバーとの通信を直列化する
INTER-Mediatorは、Ver.5.7現在、ページ合成のためのサーバーからの読み出し処理は、非同期通信で行っているため、通信処理は直列化されています。しかしながら、データ更新の処理は同期処理での通信を利用するため、直列化はされていません。状況によってはそれでも稼働しますが、認証を行う場合、チャレンジ取得とデータ処理の通信が対応している必要があるため、並列的に通信処理を行うと、認証が切れるなどの問題が発生します。そこで、キューを使って通信処理を直列化しています。キューは、変数IMLibQueueのオブジェクトとして用意されています。独自に作成するJavaScriptのプログラムでも、直列化しないと問題が発生する場合がありますが、その時には、以下のメソッドを使ってください。このメソッドは先入れ先出しの動作となります。
IMLibQueue.setTask(func, true, lowPriority)
キューに引数のクロージャーを登録し、キューは順番にしたがって処理をし、クロージャーを実行する。クロージャーはひとつの引数を持つが、この引数のクロージャーを実行することで、次のキューに移行できる。2つ目の引数は必ずtrueを指定する。3つ目の引数がfalseなら通常の優先度、trueなら低い優先順位のキューに投入する。低い優先順位のタスクは、通常の優先順位のタスクが全て終わらないと実行されない。なお、2番目以降の引数は省略でき、その場合は通常の優先順位のキューとなる。
実際に、更新処理を直列化するプログラム例を見てください。setTask関数の引数は、ローカル変数にdb_update_asyncへの引数をキャプチャしたクロージャーを返します。クロージャーは引数completeTaskがあり、クロージャー内での処理が終わると「completeTask();」の形式でキューの最後であることを示すようにします。そうしないと、次のキューに移行しません。キューの処理は非同期でも構いませんが、どこかで必ず引数の処理を実行します。
IMLibQueue.setTask((() => {
const arcsCapt = args;
return (completeTask) => {
INTERMediator_DBAdapter.db_update_async(arcsCapt,
(result) => {
//更新成功時の処理
completeTask(); // 引数の変数に()をつけてクロージャーを実行
},
() => {
//更新失敗時の処理
completeTask(); // 引数の変数に()をつけてクロージャーを実行
}
);
};
})());
INTER-Mediator内部でも、以下のメソッドで同様にIMLibQueue.setTaskメソッドを利用しています。例えば、テキストフィールドの値を変更すると、IMLibUI.valueChangeメソッドが呼び出され、実際にデータベース更新が必要な処理はいきなり実行するのではなく、IMLibQueue.setTaskメソッドでキューに入れて、順次実行されるようになっています。これらのメソッドを利用する場合には、逆にsetTaskを使う必要はありません。
- IMLibUI.valueChange
- IMLibUI.copyButton
- IMLibUI.deleteButton
- IMLibUI.insertButton
- IMLibContext.prototype.setDataAtLastRecord
- IMLibContext.prototype.setDataWithKey
- IMLibPageNavigation.copyRecordFromNavi
- IMLibPageNavigation.deleteRecordFromNavi
- IMLibPageNavigation.insertRecordFromNavi
日付時刻の文字列生成のメソッド
JavaScriptの中ではDateオブジェクトで日付や時刻を扱います。しかしながら、データベースの日付や日付時刻型のフィールドに設定する値は、適切な書式の文字列である必要があります。以下のように、ISO8601形式の日付時刻、あるいはFileMaker Server向けの日付時刻の文字列を得るためのメソッドを用意しましたので、Dateオブジェクトからの変換ではこれらのメソッドを使えばOKです。なお、MySQL、PostgreSQL、SQLite、SQL Serverは、ISO8601形式で受け付けます。いずれのメソッドも、引数はDateオブジェクトで、返り値は文字列です。引数を省略すると、現在の日時の文字列を生成します。
NTERMediatorLib.dateTimeStringISO(dt)
引数のDateクラスの値を、ISO8601形式(2015-06-21 00:00:00)に変換します。引数を省略すると、現在の日付を返します。日時はブラウザーのローカル時刻に応じたものです。
INTERMediatorLib.dateTimeStringFileMaker(dt)
引数のDateクラスの値を、FileMaker Serverのタイムスタンプ型フィールドが受け付ける形式(06/21/2015 00:00:00)に変換します。引数を省略すると、現在の日付を返します。日時はブラウザーのローカル時刻に応じたものです。
INTERMediatorLib.dateStringISO(dt)
引数のDateクラスの値を、ISO8601形式(2015-06-21)に変換します。引数を省略すると、現在の日付を返します。日時はブラウザーのローカル時刻に応じたものです。
INTERMediatorLib.dateStringFileMaker(dt)
引数のDateクラスの値を、FileMaker Serverが受け付ける形式(06/21/2015)に変換します。引数を省略すると、現在の日付を返します。日時はブラウザーのローカル時刻に応じたものです。
INTERMediatorLib.timeString(dt)
引数のDateクラスの値を、時刻の形式(12:34:56)に変換します。引数を省略すると、現在の日付を返します。日時はブラウザーのローカル時刻に応じたものです。このメソッドは、FileMaker ServerおよびSQLデータベースのどちらにも共通で利用できます。
まとめて更新する処理
『3-1 更新可能なテキストフィールド』『まとめて更新処理を行う』での説明の通り、IM_Entryの2つ目の引数の配列内に、キーがtransactionで値が「none」の要素を追加することで、テキストフィールドなどを更新するごとにデータベースに書き込みをするのではなく、後からまとめて更新することができました。ナビゲーションバーが表示されていれば「保存」ボタンで保存ができましたが、プログラムで保存処理を呼び出したい場合には、以下のメソッドを利用します。「IMLibPageNavigation」はINTER-Mediatorで定義されているオブジェクトを参照する変数で、プログラムではそのまま記述します。
IMLibPageNavigation.saveRecordFromNavi(dontUpdate)
更新処理が必要なフィールドを、まとめて更新します。引数にtrueを設定すると、更新処理を行った後にページ全体を「INTERMediator.constructMain(true)」で再度描画し直します。
このセクションのまとめ
データベースとバインドしたユーザーインターフェースだけでなく、ボタンなどによるアプリケーション特有の処理を組み込む場合、データベース処理のニーズが発生します。特に、データベースの更新処理の利用が一般的でしょう。その場合、JavaScriptで利用できるデータベースとの直接のやりとりが可能なAPIを利用することで、必要な機能を組み込むことができます。
6-4ページ合成に割り込む処理の追加
INTER-Mediatorではページ合成を行い、ページのテンプレートとデータベースのデータが統合されます。その処理に割り込むことにより、合成処理途中に自分で作成したプログラムを実行させることができます。難易度は高くなりますが、高度な機能も組み込めます。演習では、小計を表示するといったことを行ってみます。
ページ合成に割り込む処理
ページ合成の手法については、『4-2 ページを合成するときのルール』ですでに解説をしてありますが、本セクションは、合成の流れを理解していないと何を意味するのかまったく分からないと思います。もし、『4-2 ページを合成するときのルール』を読んでいないのであれば、先にそちらを読み進めてください。
INTER-Mediatorはページ合成時に、エンクロージャーとリピーターのセットを発見すると、リピーター内部のターゲット指定の状況を見てコンテキストを決定して、データベースアクセスを行います。そして、1レコード分を取り置いてあるリピーターの複製にマージして、エンクロージャーの子要素として追加をします。まず、この追加直後に、指定したメソッドの実行ができます。このメソッドは、例えば、10レコード分が得られていれば、10回呼び出されることになります。そして、エンクロージャーに最後のリピーターが追加された後にも、別のメソッドを呼び出すことができます。こちらはレコード数に関係なく、ひとつのエンクロジャーとリピーターのセットの合成に対して1回呼び出されます。
これらのメソッドは、定義ファイルのコンテキスト定義の中で指定します。リピーター追加ごとに呼び出されるメソッドはpost-repeater、エンクロージャーが完成したときに呼び出されるメソッドはpost-enclosureキーで、メソッド名を指定します。コンテキストに依存しないで、リピーター追加ごとあるいはエンクロージャー完成時に呼び出されるメソッドの定義もありますが、こちらは開発初期の時代のもので、現在はコンテキストごとに指定する方法を利用することで、ニーズは十分に満たせるでしょう。なお、これらのキーの値は、定義ファイルエディターでは、Show Allボタンをクリックして全項目を表示しないとページ上には見えてきません。
post-repeaterで指定したメソッドはひとつの引数を持ち、その引数には追加したばかりのリピーターのノードに対する配列が設定されて呼び出されます。post-enclosureで指定したメソッドもひとつの引数を持ち、その引数には完成したエンクロジャーのノードへの参照が設定されています。つまり、そのノードより下位のノードとして、実際にデータベースのデータをマージした要素や、あるいはその他の要素が存在します。ターゲット指定がある要素については、INTERMediatorOnPage.getNodeIdsHavingTargetFromNodeメソッド(詳細は演習の後に記載)を利用して、指定したターゲット指定の要素のid属性あるいはそのノードへの参照を得ることで、後はDOMに関するさまざまなメソッドやプロパティを利用して、新たにデータを追加したり、スタイルシート設定を行うことなどが可能です。ターゲット指定でないものは、class属性を指定しておき、DOMのAPIであるgetElementsByClassNameメソッドを使うことで、そのノードへの参照を得ることができます。なお、getElementsByClassNameはInternet Explorerで使用できないメソッドとして有名ですが、Ver.9以降は利用できます。INTER-Mediator Ver.5.0でInternet Explorer Ver.8はサポート対象としなくなっているので、getElementsByClassNameを利用する上での互換性の問題は基本的にはありません。
演習合計や小計を表示する
ページ合成の途中に割り込むメソッドを利用して、リストの合計を表示したり、あるいはレコードのグループごとの小計を求める方法を説明します。合計に関しては、一覧部分の全体を含むようなコンテキストを定義して、そのコンテキスト側に計算プロパティを設定することでも実現しますが、小計についてはJavaScriptを組む方法でしか実現しません。なお、この演習は、INTER-Mediator Ver.5.3以降を利用して行ってください。http://localhost:9080」に接続したページの最初に、稼働しているINTER-Mediatorのバージョンが記載されているので、それを手掛かりにしてください。演習環境の更新方法は、『1-2 演習を行うための準備』にある『演習環境内のINTER-Mediatorのアップデート』を参照してください。
定義ファイルにデータベースアクセスに必要な設定を行う
viewを「item_display」にします。
viewを「item」にします。
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
ページファイルの作成と初期データの確認
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def19.php"></script>
</head>
<body>
<table>
<thead>
<tr>
<th>product_id</th><th>name</th><th>unitprice</th>
<th>qty</th><th>amount</th><th></th>
</tr>
</thead>
<tbody>
<tr>
<td data-im="item@product_id"></td>
<td data-im="item@name"></td>
<td data-im="item@unitprice"></td>
<td data-im="item@qty"></td>
<td data-im="item@amount"></td>
<td></td>
</tr>
</tbody>
</table>
</body>
</html>
合計金額を表示できるようにする
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def19.php"></script>
次のステップで、プログラムをここに追加
</head>
<body>
<table>
<thead>
<tr>
<th>product_id</th><th>name</th><th>unitprice</th>
<th>qty</th><th>amount</th><th></th>
</tr>
</thead>
<tbody>
<tr>
<td data-im="item@product_id"></td>
<td data-im="item@name"></td>
<td data-im="item@unitprice"></td>
<td data-im="item@qty"></td>
<td data-im="item@amount"></td>
<td></td>
</tr>
</tbody>
<tfoot>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td id="total"></td>
<td></td>
</tr>
</tfoot>
</table>
</body>
</html>
<script type="text/javascript">
INTERMediatorOnPage.afterItemList = function(target) {
const context = IMLibContextPool.contextFromName("item");
const keys = Object.keys(context.store);
let s = 0;
for (let i = 0 ; i < keys.length ; i++ ) {
const value = context.store[keys[i]]["amount"];
s += parseFloat(value);
}
const fmtValue = IMLibFormat.numberFormat(s, 0);
const node = document.getElementById("total");
const tNode = document.createTextNode(fmtValue);
node.appendChild(tNode);
}
</script>
金額の小計を表示できるようにする
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def19.php"></script>
<script type="text/javascript">
INTERMediatorOnPage.afterItemList = function(target) {
addSubTotalLine(null, false);
const context = IMLibContextPool.contextFromName("item");
const keys = Object.keys(context.store);
let s = 0;
for (let i = 0 ; i < keys.length ; i++ ) {
const value = context.store[keys[i]]["amount"];
s += parseFloat(value);
}
const fmtValue = IMLibFormat.numberFormat(s, 0);
const node = document.getElementById("total");
const tNode = document.createTextNode(fmtValue);
node.appendChild(tNode);
}
INTERMediatorOnPage.afterItemRecord = function(target) {
let nodes = INTERMediatorOnPage.getNodeIdsHavingTargetFromNode(
target, "item@product_id");
if (nodes.length > 0) {
const node = document.getElementById(nodes[0]);
const groupId = parseFloat(node.innerHTML);
nodes = INTERMediatorOnPage.getNodeIdsHavingTargetFromNode(
target, "item@amount");
if (nodes.length > 0) {
const node = document.getElementById(nodes[0]);
const value = parseFloat(node.innerHTML);
if (prevId != groupId && prevId != -1) {
addSubTotalLine(target, true);
subtotal = 0;
}
subtotal += value;
}
prevId = groupId;
}
}
function addSubTotalLine(target, isBefore) {
const context = IMLibContextPool.contextFromName("item");
const insertRepeater = context.repeaterNodes[0].cloneNode(true);
const nodes = INTERMediatorOnPage.getNodeIdsHavingTargetFromNode(
insertRepeater, "item@amount");
if (nodes.length > 0) {
if (isBefore) {
context.enclosureNode.insertBefore(insertRepeater, target[0]);
} else {
context.enclosureNode.appendChild(insertRepeater);
}
const fmtValue = IMLibFormat.numberFormat(subtotal, 0);
const tNode = document.createTextNode(fmtValue);
nodes[0].style.backgroundColor = "white";
nodes[0].appendChild(tNode);
}
}
let prevId = -1, subtotal = 0;
</script>
</head>
<body>
:
</body>
</html>
この演習のまとめ
- ひとつのリピーターの処理後、あるいはひとつのエンクロージャーの処理後に呼び出されるメソッドを定義できます。
- その中では、特定のターゲット指定を持つノードを検索し、そのidフィールド値を返すメソッドを利用して、処理対象のノードを探すこことができます。
- データベースから得られたデータは、コンテキストオブジェクトのstoreプロパティに残っており、そこから表示された値を得ることができます。
ページ合成処理に割り込むメソッド
ページ合成に割り込むメソッドを、アプリケーション側で定義することができます。詳細はこのセクションの最初の部分でも解説しています。post-repeaterキーの値をメソッド名にした場合、メソッドを呼び出すときの引数は、追加したリピーターを参照しています。post-enclosureキーの値のメソッドの場合は、引数はエンクロージャーのノードです。いずれのメソッドも、返り値は不要です。
INTERMediatorOnPage.《post-repeaterキーの値》 = function(target) {...}
INTERMediatorOnPage.《post-enclosureキーの値》 = function(target) {...}
ノード検索のためのメソッド
あるノードから、引数に指定したターゲット指定を持つノードを返すメソッドがいくつかあります。演習で使用したものは、最初のひとつだけですが、いくつか異なるバリエーションのメソッドを用意してあります。
INTERMediatorOnPage.getNodeIdsHavingTargetFromNode(nodes, targetDef)
指定したノードの配列の子要素の中で、引数に指定したターゲット指定を持つノードのid属性値、あるいはそのノードへの参照を配列で返します。
引数 | 指定内容 |
---|---|
nodes | ルートとなるノードあるいはノードの配列 |
targetDef | 検索するターゲット指定 |
[返り値] | 該当するノードがid属性があればそのid値、id属性がなければそのノードへの参照を返す。指定したターゲット指定のノードが複数あることもあるので、返り値は文字列ないしはノードへの参照の配列になる。 |
INTERMediatorOnPage.getNodeIdsHavingTargetFromRepeater(fromNode, targetDef)
INTERMediatorOnPage.getNodeIdsHavingTargetFromEnclosure(fromNode, targetDef)
指定したノードより上位階層にさかのぼり、最初に見つけたリピーターあるいはエンクロージャーに含まれる子要素で、引数に指定したターゲット指定を持つノードのid属性値、あるいはそのノードへの参照を配列で返します。
引数 | 指定内容 |
---|---|
fromNode | 基準となるノード |
targetDef | 検索するターゲット指定 |
[返り値] | 該当するノードがid属性があればそのid値、id属性がなければそのノードへの参照を返す。指定したターゲット指定のノードが複数あることもあるので、返り値は文字列ないしはノードへの参照の配列になる。 |
書式設定のためのメソッド
書式設定や数値化のためのメソッドとして以下のようなものが用意されています。もちろん、自分で作ったり、他のライブラリを利用して書式を整えてもかまいません。
IMLibFormat.numberFormat(value, digits)
数値を3桁ごとのカンマ付きおよび小数点以下の桁数指定をして書式化した文字列を返す。
引数 | 指定内容 |
---|---|
value | 書式化の対象となる値 |
digits | 少数以下の桁数 |
[返り値] | 書式化した文字列 |
このセクションのまとめ
ページ合成の処理に割り込むメソッドを利用することで、ページ合成の結果をダイナミックに変化させることができます。DOM関連のプログラムはコンテキストオブジェクトの使いこなしなどが必要になりますが、複雑な要求に応えるにはここまでの処理が必要になるかもしれません。
6-5Post Onlyモードと連動した処理
データベースへの新規レコードを作成するPost Onlyモードは、アンケート入力などを簡単に作成できるのですが、新規入力に連動する処理が必要なアプリケーションもあります。そのため、レコード作成の前後にプログラムを追加できるようになっています。このセクションには演習が2つあり、異なるテーマでのWebページ作成を行います。
Post Onlyモードの動作とカスタマイズ
Post Onlyモードのページのカスタマイズ可能な箇所は、data-im-control属性が「post」のボタンをクリックしたときの処理です。ボタンを押した後、実際にデータベースにレコードを作成し、テキストフィールドに入力した文字列などをフィールドの初期値とします。このデータベースを処理する前と後に、定義したメソッドを実行できます。データベース処理前のメソッドは、例えば、入力したデータに不正がないかを調べたりもできます。ただし、この処理はバリデーションで実施してもいいのですが、複数のフィールドにまたがるような判断、例えばパスワードのフィールドが2つあって同じものかどうかを判定したいような場合には、データベースの処理の前に判断する方がページ自体は作りやすいでしょう。データベースの処理前のメソッドは論理値を返す必要があり、falseを返せばデータベース処理はキャンセルされて、ページはそのままの状態で止まります。もちろん、データに何か問題が見つかればfalseを返し、問題なければtrueを返せばよいのです。なお、この後の演習では、もう少し複雑な状況での例を出します。演習は、「状況に応じて、フィールドの初期値をプログラムで定義する」といったテーマになります。
一方、データベース処理の後に呼び出されるメソッドは、データベース処理が成功したときにだけ呼び出され、作成されたレコードそのものや、あるいはレコードの主キー値を得ることができます。つまり、新規に作成されたレコードの情報を得た上で、別の処理に引き継ぐことができます。こちらの処理は、ページを遷移したりすることが前提になります。このメソッドがなくても、コンテキスト定義により別のページに移動しますが、メソッド側でページを移動すると、それ以降はPost Onlyモードのページ側の処理はすべてキャンセルされます。
以上はデータベースを書き込むという意味では、Post Onlyモードのページの最終段階で利用できる機能です。Post Onlyモードのページでも、通常のINTER-Mediatorベースのページと同様に、ページ構築時の機能は使用できます。しかしながら、通常の検索結果を表示するページと異なり、Post Onlyモードの場合は、id属性は元のページファイルの状態をキープします。また、ローカルコンテキストは構成しません。したがって、HTMLの段階で後から処理をしたいテキストフィールドに自由にid属性を設定しておき、そのid値を利用してオブジェクトへのアクセスができます。
Post Onlyモードでも、INTERMediator.construct()メソッドを呼び出して、ページ合成の処理は必要です。その前にプログラムを追加したり、INTERMediatorOnPage.doAfterConstructメソッド(『8-5 ブラウザーを判断するページ』を参照)を定義して、ページ合成直後に行う処理を記述することもできます。これらの仕組み利用して、Post Onlyモードのページでの拡張を行うことができます。
演習Post Onlyモードを利用して関連レコードを追加する
Post Onlyモードで単一のレコードの作成は簡単にできるのですが、伝票における明細のレコードをPost Onlyモードで作成しようとした場合、単に明細側のコンテキストに対するHTMLを記述すればいいだけではありません。この場合、外部キーに相当するフィールドに、適切な値を入れて、リレーションシップに基づく関連付けをしなければいけません。このような場合は追加で自動的に特定のフィールドに値を与えることが必要になります。このことをPostOnlyモードで実現する方法を紹介します。なお、この演習では、『4-4 計算プロパティの設定』で作成したものの計算式を設定していないものを作成します。ページファイルの記述の一部は、コピー&ペーストで入力してもいいでしょう。
定義ファイルにデータベースアクセスに必要な設定を行う
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
ページファイルの作成と初期状態の確認
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def20.php"></script>
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table>
<tr><th>id</th><td data-im="product@id"></td></tr>
<tr><th>acknowledgement</th>
<td data-im="product@acknowledgement"></td>
</tr>
<tr><th>name</th>
<td><input type="text" data-im="product@name"/></td>
</tr>
<tr><th>unitprice</th>
<td><input type="text" data-im="product@unitprice"/></td>
</tr>
<tr><td colspan="2">
<table>
<thead>
<tr><th>invoice_id</th><th>qty</th>
<th>unitprice</th><th>price</th></tr>
</thead>
<tbody>
<tr>
<td data-im="item@invoice_id"></td>
<td><input type="text" data-im="item@qty"/></td>
<td><input type="text" data-im="item@product_unitprice"/></td>
<td></td>
</tr>
</tbody>
</table>
</td></tr>
</table>
<table>
<thead>
<tr><th>qty</th><th>unitprice</th><th></th></tr>
</thead>
<tbody data-im-control="post">
<tr>
<td><input type="text" data-im="item@qty"/></td>
<td><input type="text" data-im="item@product_unitprice"/></td>
<td><button data-im-control="post">追加</button></td>
</tr>
</tbody>
</table>
</body>
</html>
INSERT INTO item (qty,unitprice) VALUES ('555','666)
[FileMakerの場合]
http://192.168.56.1:80/fmi/xml/FMPXMLRESULT.xml?-db=TestDB&-lay=item&-max=1&qty=555&unitprice=666&-new
外部キーのフィールドにも適切な値を入力する
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def20.php"></script>
<script style="text/javascript">
INTERMediatorOnPage.processingBeforePostOnlyContext
= function(targetNode) {
const context = IMLibContextPool.contextFromName("product");
const keys = Object.keys(context.store);
const pid = context.store[keys[0]]["id"];
INTERMediator.additionalFieldValueOnNewRecord["item"]
= {field:"product_id", value:pid};
return true;
}
</script>
</head>
INSERT INTO item (product_id,qty,unitprice) VALUES ('1','44','789)
[FileMakerの場合]
http://192.168.56.1:80/fmi/xml/FMPXMLRESULT.xml?-db=TestDB&-lay=item&-max=1&product_id=1&qty=44&unitprice=789&-new
入力後にコンテキストを更新する
<script style="text/javascript">
INTERMediatorOnPage.processingBeforePostOnlyContext
= function(targetNode) {
const context = IMLibContextPool.contextFromName("product");
const keys = Object.keys(context.store);
const pid = context.store[keys[0]]["id"];
INTERMediator.additionalFieldValueOnNewRecord["item"]
= {field:"product_id", value:pid};
return true;
}
INTERMediatorOnPage.processingAfterPostOnlyContext
= function(targetNode, returnValue) {
const context = IMLibContextPool.contextFromName("item");
INTERMediator.construct(context);
}
</script>
演習のまとめ
- Post Onlyモードにおいて、テキストフィールド等に指定されていないフィールドに新たな値を追加するには、INTERMediator.additionalFieldValueOnNewRecordプロパティを利用することができます。
- 上記のプロパティに値を設定するタイミングとして、「追加」のボタン(data-im-control属性が「post」のボタン)を押した直後に呼び出されるメソッド内を利用できます。
- Post Onlyモードによる新規レコードを作成直後に画面更新をするには、データベースに新規レコードを作成した直後に呼び出されるメソッドを定義して、そこに画面更新のプログラムを記述します。
演習Post Onlyモードのページに引き続いてページを表示する
『3-4 入力専用のPost Onlyモード』で、Post Onlyモードのページに確認のページが不要な点を説明しましたが、一方で、入力後に処理をした結果を確認するという構成も欲しくなります。この場合、入力と確認のページを同一ページに作り、表示と非表示をうまく行う方法を、『3-4 入力専用のPost Onlyモード』で説明しました。一方、入力ページはPost Onlyモードで作成し、確認のためのページを別途作り、そちらは新たに作られたレコードの内容を表示するようにするという方法もあります。こうすれば、サーバー側あるいはデータベース上で複雑な処理を記述できます。こうしたページの作成は、例えばシンプルな通販サイトなどで見られ、合計金額や地域に応じた送料の適用などの応用例が考えらます。その一方で、さまざまなビジネスロジックを組み込むと複雑なシステムになります。そこで、この演習では、ビジネスロジックが極端に簡単な通販サイトを想定することにします。Post Onlyモードのページと、確認のページをどのように「つなぐ」ということを行うのかということを示すためのサンプルであり、通販に必要なロジックをすべて実装したものではありません。
演習を行う前に、どのような動作を行うのかをまず確認しておきます。図6-5-1は、最初に表示されるPost Onlyモードのページです。購入者名と、購入品目をチェックボックスで用意します。金額は意図的にページ上に書いていませんが、これは次のページで計算する合計金額がPost Onlyモードページ内にある金額値を取り込んで計算しているかのような誤解がないようにするためのものです。使い勝手としては悪くなりますが、組み込む機能をより明確にしたいので、あえて表示しないでおきます。金額は、引き続くページのコンテキストに定義した計算式に織り込むようにしました。そして、「購入」ボタンを押すと、図6-5-2のページにジャンプします。ここで購入品目と、それに合わせた合計金額が表示されています。ロジック的なものは、このページのコンテキストに定義した計算式により、選択した品目に応じた合計金額が表示されるということです。この状態からの発展については、ページを作成する作業の後に説明します。
最初のページ用の定義ファイルに必要な設定を行う
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
最初のページのページファイルの作成
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def21.php"></script>
<script type="text/javascript">
INTERMediatorOnPage.processingAfterPostOnlyContext
= function(targetNode, returnValue){
location.href = "page22.html?id="
+ encodeURIComponent(returnValue);
}
</script>
</head>
<body>
<table>
<tbody data-im-control="post">
<tr>
<th>お名前</th>
<td><input type="text" data-im="testtable@text1"/></td>
</tr>
<tr>
<th>購入品目</th>
<td>
<input type="checkbox" value="1" data-im="testtable@num1"/>なべ
<input type="checkbox" value="1" data-im="testtable@num2"/>やかん
<input type="checkbox" value="1" data-im="testtable@num3"/>フライパン
</td>
</tr>
<tr>
<td><button data-im-control="post">購入</button></td>
</tr>
</tbody>
</table>
</body>
</html>
2番目のページの定義ファイルを設定する
field : item1, expression : if ( num1 == 1, 'inline', 'none' )
field : item2, expression : if ( num2 == 1, 'inline', 'none' )
field : item3, expression : if ( num3 == 1, 'inline', 'none' )
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
2番目のページのページファイルの作成
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<script type="text/javascript" src="def22.php"></script>
<script type="text/javascript">
INTERMediatorOnPage.doBeforeConstruct = function () {
const params = INTERMediatorOnPage.getURLParametersAsArray();
if (params["id"]) {
INTERMediator.clearCondition("testtable");
INTERMediator.addCondition("testtable",
{field: "id", operator: "=", value: params["id"]});
}
}
</script>
</head>
<body>
<table>
<tbody>
<tr>
<th>お名前</th>
<td data-im="testtable@text1"></td>
</tr>
<tr>
<th>購入品目</th>
<td>
<span data-im="testtable@item1@style.display">なべ </span>
<span data-im="testtable@item2@style.display">やかん </span>
<span data-im="testtable@item3@style.display">フライパン</span>
</td>
</tr>
<tr>
<th>合計金額</th>
<td data-im="testtable@total"></td>
</tr>
<tr>
<td><button onclick="">決定</button></td>
</tr>
</tbody>
</table>
</body>
</html>
実際にページの移動を確認する
演習のまとめ
- Post Onlyページでのレコード作成後に呼び出されるメソッドを利用すれば、新規に作成されたレコードの主キー値が得られます。
- 主キー値を元に別のページで、今作成したばかりのレコードを表示することができるので、データベースやあるいはコンテキストなどにロジックを組み込むことで入力データを元にしたデータ処理は可能です。
- なお、実用的なサイトにするには考慮すべきことは多々あります。それについては、この演習の後に解説します。
入力と確認をより確実に行わせるために
直前まで行っていた『Post Onlyモードのページに引き続いてページを表示する』については、もっとも根幹的な部分だけの作成になりました。現実のアプリケーションではここからさまざまな仕組みの実装が必要になります。それらのヒントをここではまとめておきます。
セキュリティ面での考慮
このままでは、他人の注文ページを参照できてしまうことは明白です。page22.html?id=1などとアドレスバーに入れて、1から順番に見ていけばいいのです。これを排除する方法のひとつは、すべて認証した状態で注文をするようにすることです。加えて、レコード単位のアクセス権の設定を行い、Post Onlyモードで作成したレコードは、作成者しか見えないようなコンテキスト定義をdef22.phpで行うというのがもっとも確実な方法です。認証とアクセス権については、『Chapter 7 セキュリティと認証・アクセス権』で説明します。
もし、これを認証なしに行いたい場合、他人に見られる確率は0ではありませんが、限りなく0に近くできる手法はあります。Post Onlyモードでレコード作成をしたとき、レコード作成日時を適当な日付時刻型フィールドに保存しておきます。データベースエンジンで設定する方法、あるいは、INTERMediator.additionalFieldValueOnNewRecordプロパティを使う方法などがあります。そして、ここでのpage22.htmlにおいて、主キー値だけでなく、レコード作成日時と現在の日時の時間を見て、一定範囲内であれば表示するようにします。もちろん、数秒としたいところですが、ネットワークが遅いことを考えれば、30秒程度の範囲くらいが妥当なところと思われます。なお、page22.htmlは「更新」ができなくはなりますが、それは「セキュリティ上の理由」で納得してもらえるのではないかと思います。
しかし、それでも、主キー値が連番なら、自分のときが245だったら、そのうち300番が来るのは明白なので、10秒ごとにアクセスしてみるというアタックも可能かもしれません。それを防ぐには、主キー値(あるいは主キーフィールドとは別にフィールドを設ける)をランダムかつ長い値にします。数値10桁くらいにしておくと、予測はほとんどつきません。もちろん、偶然当たることもあるかもしれませんが、この完全に0ではないものの極めて低い確率をどのように評価するかは、隠匿したい情報の内容や、発注者の要求レベル、あるいは公開されてもリスクが発生しない業務形態など、個々の案件に依存すると思われます。認証をさせるのが手堅いのですが、認証をしないのは単にリスクをある意味意図的に増やしているということでもあります。コンピューターはなんでもできる万能機械ではないことを理解していただき、落とし所を探りましょう。
発注状況の記録
もうひとつ大きな問題があります。こうして蓄積されたレコードは、全部が「発注した」ものかというと、そうではありません。ここでは、「決定」ボタンをクリックしたら発注が確定したということにしたいわけです。このようなステータス管理を行うには、一般にはステータスを記録するフィールドをひとつ用意して、1なら確定、空白なら未確定、9ならキャンセルといったような値を割り当てておく方法がひとつあります。別の方法としては、特定の状態へ移行した日時を記録する方法があります。これら、どちらがいいというわけではなく、他の用途と密接に関連します。例えば、会計と出荷の処理のように、事象確定のタイミングが異なるような場合であれば、さらにテーブルを分けるべきかもしれません。
いずれもして、一番手軽な方法は、「決定」ボタンをクリックすることにより、あるフィールドに現在の日時を設定する方法です。その日時フィールドが空欄なら確定していない発注であり、空欄でなければ確定した発注です。「発注一覧」の検索条件にそのフィールドがnullでないことを追加すればおおむね大丈夫です。
明細を別テーブルに分けたい
このための手法として、Post Onlyモードを1対多での運用はできません。そこで、Post Onlyモードで使用するテーブルには、明細行のためのフィールドを、10個あるいは15個くらい用意しておきます。つまり、product_idN、qtyN(N=1〜10)のようなフィールドを用意しておきます。場合によってフィールドの種類も増えるでしょう。第一正規形が崩れますがこれは一時的なものとして許容します。そして、どこかで、product_idNとqtyNを別のテーブルに新規レコードおよび転記を行って、1対多の関係に展開します。
どこでやればいいのかという点についてはいろいろな場面が考えられますが、Post Onlyモードによる新規レコード作成の直後がベストではないかと思います。そうすれば、次のページの処理では、1対多に展開した状態で、さまざまなロジックを組み込むことができるからです。JavaScriptではINTERMediatorOnPage.processingAfterPostOnlyContextメソッドに、多のテーブル側への追加のプログラムを書くことができるでしょう。あるいは『Chapter 8 サーバーサイドでのプログラミング』で説明する方法を使えばサーバーサイドで別テーブルへの追加部分を記述してもいいかもしれません。また、FileMakerを使っている場合には、コンテキストのscriptキーを使って、データベース上に定義したスクリプトを利用して、明細のレコードを作るということもできるでしょう。
Post Onlyモードで利用できるAPI
Post Onlyモードだけで利用できるAPIを紹介します。以下のメソッドは定義すれば、ページ内のすべてのコンテキストで呼ばれます。しかしながら、通常Post Onlyモードのコンテキストが複数あるようなページはあまり作られないと思われます。どうしてもコンテキストが複数あってそれらを区別したい場合は、引数で参照したエンクロージャーにid属性あるいはclass属性を指定しておき、その値で区別する方法があるでしょう。
INTERMediatorOnPage.processingBeforePostOnlyContext = function(targetNode) {...}
メソッドを定義すれば、バリデーションが完了した後で、データベースへの書き込み前に呼び出されます。このメソッドがfalseを返すとデータベースの書き込み処理はキャンセルされます。引数targetにはPost Onlyモードのエンクロージャーへの参照が設定されます。
INTERMediatorOnPage.processingAfterPostOnlyContext = function(targetNode, idValue) {...}
メソッドを定義すれば、新規レコードを作成した直後に呼び出されます。引数targetにはPost Onlyモードのエンクロージャーへの参照が設定されます。引数idValueは新たに作成されたレコードの主キー値が設定されます。引数に値が設定されていない場合は、レコード作成ができなかったことをしますがデータベースにエラーがある場合にはこのメソッドの呼び出しは行われません。
このセクションのまとめ
Post Onlyモードは、データベースへのレコード作成の前後に、プログラムを追加することができます。演習で示したように、関連レコードのコンテキストにレコードを追加することが可能なPost Onlyモードのコンテキストとページ要素を定義したり、あるいは、新規レコードを作成後にそのレコードを引き継いで新たなページを表示するなどにこの仕組みは応用できます。
6-6JavaScriptコンポーネント用のアダプターの開発方法
すでに存在するJavaScriptコンポーネントをバインド可能にするためには、アダプターが必要です。アダプターの開発方法を説明しますが、コンポーネントごとに動作が違うため、最終的には使用するコンポーネントの特性に合わせて調整が必要になります。まだ公開されていないコンポーネントのアダプターを開発したら、是非ともコミットしてください。
コンポーネントの定義と一例
リスト6-6-1は、jQueryUIに含まれているDatePickerを利用するためのアダプターの例です。DatePickerはカレンダー形式のユーザーインターフェースで日付の選択入力が可能なものです。テキストフィールドに対して、クリックすればDatePickerのカレンダー画面がポップアップするといった動作を行います。
このコンポーネントを使用できるようにするドライバーがリスト6-6-1です。IMParts_Catalogというグローバル変数に、「jquery_datepicker」というプロパティを用意し、そのプロパティにドライバーのオブジェクトを設定しておきます。この「jquery_datepicker」は、data-im-widget属性に記述する値です。
IMParts_Catalog["jquery_datepicker"] = {
instanciate: function (targetNode) {
const nodeId = targetNode.getAttribute('id');
this.ids.push(nodeId);
targetNode._im_getComponentId = function () {
const theId = nodeId;
return theId;
};
targetNode._im_setValue = function (str) {
const aNode = targetNode;
aNode.value = str;
};
targetNode._im_getValue = function () {
const aNode = targetNode;
return aNode.value;
};
},
ids: [ ],
finish: function () {
for (let i = 0; i < this.ids.length; i++) {
var targetId = this.ids[i];
var targetNode = $('#'+targetId);
if (targetNode) {
targetNode.datepicker({
onSelect: function(dateText) {
this.value = dateText;
IMLibUI.valueChange(this.id);
}
});
}
}
this.ids = [ ];
}
};
INTER-Mediatorのレポジトリには、Samples/Sample_webpageフォルダーに、リスト6-6-2に加えて、TinyMCEやCodeMirrorのドライバーがあります。同じフォルダーに、それらのドライバーの利用例のページファイルもあります。詳細は、『5-4 JavaScriptコンポーネントの利用』を参照してください。
ドライバーオブジェクトの構成
ドライバーのオブジェクトに必要なメソッドは、引数をひとつ取るinstanciateと、引数のないの2つのメソッドです。加えて、これらのメソッド間でコンポーネントを適用したオブジェクトを後から参照するためにid属性を記録しておくidsプロパティを定義して、初期値は要素のない配列にしておきます。
INTER-Mediatorは、data-im-widget属性がある要素の場合、その値からドライバーのオブジェクトを参照します。そして、そのオブジェクトのinstanciateメソッドを呼び出します。その要素には通常はターゲット指定を行っているはずですが、id属性を設定した後にドライバーのinstanciateメソッドが呼び出されます。そして、instanciateメソッドの引数は、data-im-widget属性がある要素を参照した状態で呼び出されます。
コンポーネントによってその後の構成は異なりますが、jQueryUIのDatePickerは比較的シンプルな方です。引数に与えられた要素への参照からid属性値を得て、idsプロパティへ要素として追加しています。HTMLエディターのTinyMCEでは、要素の子要素にTEXTARERタグの要素を追加し、その要素をTinyMCEに渡してエディターを構築するようにしています。これら、実装はコンポーネントの初期化の動作に依存します。
instanciateメソッドは、リピーターにデータベースのデータを合成しているときに逐一呼び出されます。したがって、レコード数×data-im-widget属性がある要素の数だけ呼び出されることになります。一方、finishメソッドは、エンクロージャーの中にすべての複製されたリピーターが挿入された最後に呼び出されます。つまり、ページのDOMの中に、データベースのデータを合成したものがすべて含まれる状態で呼び出されます。通常はコンテキストにつき1回だけ呼び出されます。
このように、instanciateとfinishがあるのは、instanciateを実行する段階では、ページのDOMの中に要素はなく、リピーターの複製をエンクロージャーに追加する前に呼び出されるからです。このような状態では初期化がうまくいかないコンポーネントがあるため、個別の呼び出しとページに組み込まれてから呼び出される2つのメソッドを用意しました。
ドライバーオブジェクト内で行うこと
ドライバーオブジェクトのinstanciateおよびfinishメソッド内では、各data-im-widget属性が指定された要素に対して、表6-6-1のメソッドを定義することです。メソッドの設定はinstanciate側では、_im_getComponentIdは必ず行えるとして、_im_setValueメソッドもそこで指定します。そうしないと、データベースのデータの合成で値の設定が正しく行われません。_im_getValueについてはinstanciateあるいはfinishのどちらでもいいのですが、通常はinstanciateで設定してみてうまくいかないときにはfinishメソッド内で、idsプロパティに保存されているid属性値ひとつひとつについて処理をするといった思考錯誤が必要です。これらのメソッド内では、コンポーネント特有の事情を考慮して、値を設定したりあるいは取り出したりということを行います。jQueryUIのDatePickerの場合は、比較的シンプルですが、別のコンポーネントではコンポーネントに特有のAPIを利用する必要がある場合もあります。
メソッド名 | 引数 | 動作(返り値) |
---|---|---|
_im_getComponentId | なし | 要素のid属性値を返す |
_im_setValue | ひとつ | 要素に引数の値を設定する。返り値なし |
_im_getValue | なし | 要素の値を取り出して返す |
なお、jQueryの場合は、ページ上にあるテキストフィールドを参照してオブジェクトが増やされますが、そのテキストフィールドは追加されたオブジェクトから独立しています。通常のテキストフィールドだと、テキストを修正して別のフィールドに移動するときにchangeイベントが発生しますが、DatePickerはテキストフィールドとしてのイベントは発生されません。そこで、datepickerメソッドで各要素でDatePickerを有効にしたときに、onSelectメソッドを実装して、選択時の処理を記述しています。IMLibUI.valueChangeは、テキストフィールドのchangeイベントが呼び出されたときに使用するもので、id属性を与えれば、データベースの更新や別の要素で表示しているフィールドの表示、さらには再計算など必要な更新処理をすべて行うメソッドを呼び出しています。結果的に、_im_setValueは使われることなく、コンポーネントのイベントを得て更新処理を実施しています。
ドライバーのサンプルで利用したAPI
DatePickerのドライバーは以下のメソッドを利用していますが、ドライバーに特有なAPIではなく、他の場面でも利用できるものです。
IMLibUI.valueChange(idValue)
引数に指定したid属性値を持つ要素に見えている値を用いて、データベースの更新やバインドされている他のフィールドへの反映、再計算などを行います。通常、テキストフィールドは、changeイベントが発生したときにこのメソッドが呼び出されるようになっています。
このセクションのまとめ
JavaScriptで作られたコンポーネントをバインドできるようにするためにはドライバーが必要です。ドライバーはグローバル変数IMParts_Catalogのdata-im-widget属性に指定する値と同一の名称のプロパティにオブジェクトとして定義します。オブジェクトでは、instanciateとfinishのメソッド定義し、_im_getComponentId、_im_setValue、_im_getValueの3つのメソッドを、data-im-widget属性が設定されている要素に追加する必要があります。それ以上の実装に関することは、コンポーネントの事情に応じて対処する必要があります。