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のプログラムを作成しておきます。

リスト6-1-1 詳細ページへの移動リンクやボタンの例
<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)』で進めてください。

定義ファイルにデータベースアクセスに必要な設定を行う

1演習環境を起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def16.phpを編集する」をクリックし、定義ファイルエディターでdef16.phpファイルを編集します。(もし、他の用途で16番目を利用しているのなら、例えば、def31.phpを利用するなど、別の番号のセットを使用してください。その場合ソースコードの記述が変わる部分がありますが、可能な限り注記します。)
3Contextsの中にはすでにpostalcodeコンテキストが定義されています。repeat-controlキーの値を空白にします。
Contextsにあるその他のテキストフィールドはそのまま利用します。
4Database Settingsに設定を行います。db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
5Debugについては、「2」のままにしてこの後の作業を行ってください。この演習は、デバッグ情報をみながら動作を確認します。

ページファイルの作成

1「http://localhost:9080」で開いたページに戻り「page16.htmlを編集する」をクリックし、ページファイルのpage16.htmlを編集するページファイルエディターを開きます。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="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>
ヘッダー部にあるSCRIPTタグ内にスクリプトが記述されていて、ページ合成を行う前に呼び出されるメソッドでいくつかのことが行われています。INTERMediatorOnPage.getURLParametersAsArrayメソッドは、URLのパラメーター部分にあるデータをキーと値に分離し、キーをプロパティとして持つオブジェクトに変換した結果を返します。そして、INTERMediator.clearConditionにより、postalcodeコンテキストの追加の検索条件を消去します。コンテキスト定義のqueryに記述した条件はこのメソッドでは消えません。そして、q=のパラメーターがある場合、addConditionによって、追加の検索条件をpostalcodeに追加します。ここでは、コンテキスト定義とaddConditionの2つの条件が設定され、他に指定はないので、ANDで結合されます。

ページの表示と結果の確認

1「http://localhost:9080」で開いたページに戻り「page16.htmlを表示する」をクリックし、ページファイルのpage16.htmlを表示します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
2ページの冒頭にあるデバッグ情報には、データベースに与えた検索のためのSQLステートメントが見えています。その部分を特定します。
SELECT * FROM postalcode WHERE (`f3` LIKE '1%') ORDER BY `f3` ASC LIMIT 10 OFFSET 0
ここに見えている検索条件は、コンテキストのqueryキーで指定したものだけです。URLにはパラメーターが付けられていないので、ページ合成前のに実行されるメソッドで追加した検索条件は追加されません。
3ページ末尾にあるページ合成の結果を参照します。検索条件に合致するレコードが一覧されています。
4パラメーターを付加してページを表示してみます。ブラウザーのアドレス欄には「http://localhost:9080/page16.html」が表示されているので、以下のように?以下のパラメーターをつけたURLへとキータイプし、returnキーを押してページアクセスを行います。
http://localhost:9080/page16.html?q=16%25
MySQLの場合、「f3 LIKE '16%'」すなわち、16で郵便番号が始まる地名を検索する条件を指定します。q=以降は値として設定されるものです。このとき、「16%」が設定したい値ですが、URLは独特のエンコーディングを行い、%は特別な意味を持ちます。そのため、%自体を文字列として指定したい場合には、その文字コードの16進数表記である「25」を交えて「%25」と記述することで、デコードされた場合には「%」という文字列になりなす。JavaScriptでは、エンコードやデコードのための関数としてencodeURIComponentやdecodeURIComponentが用意されています。
5ページの表示ができれば、ページの冒頭にあるデバッグ情報に見えているデータベースに与えた検索のためのSQLステートメントを特定します。
SELECT * FROM postalcode WHERE (`f3` LIKE '1%') AND (`f3` LIKE '16%') ORDER BY `f3` ASC LIMIT 10 OFFSET 0
コンテキストのqueryキーで指定した条件と、URLのパラメーターの値をもとにJavaScriptのプログラムで追加した2つの条件が設定されており、ANDでそれぞれ結ばれています。
6ページ末尾にあるページ合成の結果を参照します。検索条件に合致するレコードが一覧されています。

検索のためのユーザーインターフェースと連動する

1「page16.htmlを編集する」をクリックして表示したページに戻ります。閉じてしまっていれば、「http://localhost:9080」で開いたページに戻り「page16.htmlを編集する」をクリックします。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)最初に追加したINTERMediatorOnPage.doBeforeConstructメソッドは、ここでは使わないのでメソッド定義ごとコメントにします。
<!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>
ページ上に新たにテキストフィールドと、ボタンが登場しました。テキストフィールドにはid属性を設定し、ボタンにはonclick属性を設定しています。ボタンをクリックすると、search関数が呼び出されます。テキストフィールドの値をもとに検索条件を与えていますが、都道府県名、市区町村名、町域名がそれぞれf7、f8、f9フィールドなので、それぞれに対して、部分一致で検索をかけています。f7〜f9についてはOR演算を行い、もともと、コンテキストのqueryキーの条件とはさらにANDとなるように、検索条件を指定しています。この検索条件の指定方法は『2-5 検索と並べ替えに関する設定』でも指定しています。ここでのポイントは、検索条件の文字列に対して、変数strへの代入部分にあるように、前後に%を付加しています。テキストフィールドに「北」と入れれば、検索条件は例えば「f7 LIKE '%北%'」となります。プログラムで記述すると、検索条件の指定も計算式等で記述でき、さまざまな処理を経て適用することができます。INTERMediator.constructでページ合成を行いますが、ここで引数にコンテキストオブジェクトを指定すると、そのコンテキストのみ再描画されます。コンテキストオブジェクトへの参照は、IMLibContextPool.contextFromNameメソッドを使って引数にコンテキスト名を指定することで得られます。
2「http://localhost:9080」で開いたページに戻り「page16.htmlを表示する」をクリックし、ページファイルのpage16.htmlを表示します。パラメーターはなしで表示するので、もう一度「page16.htmlを表示する」をクリックして新たにページを開く方が手軽でしょう。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
3スクロールしてページ合成した部分を参照します。ここで、検索条件を指定するテキストフィールドと、「検索」ボタンが表示されています。テキストフィールドに「北」と入力して、「検索」ボタンをクリックします。
4新たに表示されたデバッグエリアに検索のためのSQLステートメントがあります。次のようなステートメントです。
SELECT * FROM postalcode WHERE (`f3` LIKE '1%') AND (`f7` LIKE '%北%' OR `f8` LIKE '%北%' OR `f9` LIKE '%北%') ORDER BY `f3` ASC LIMIT 10 OFFSET 0
5ページの末尾には、検索結果のレコードを基にしたページが合成されています。町名や区名に「北」が含まれる地名が検索されています。

演習のまとめ

演習プログラムで条件を指定する検索機能を持つページ(FileMaker)

 テキストフィールドに入れた文字列を、JavaScriptのインターフェースで指定する検索条件として与える形式のページを作成します。この演習は、MySQLとFileMakerで細かな点で異なるため、同一の演習をそれぞれのデータベース向けに別々に記述します。FileMakerで演習をされる場合には、このまま進んでください。MySQLで演習される方は、この演習の前にあるMySQL向けの演習手順『プログラムで条件を指定する検索機能を持つページ(MySQL)』で進めてください。

定義ファイルにデータベースアクセスに必要な設定を行う

1演習環境を起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def17.phpを編集する」をクリックし、定義ファイルエディターでdef17.phpファイルを編集します。(もし、MySQLの演習に続いてこの演習を進めていたり、他の用途で17番目を利用しているのなら、例えば、def19.phpやdef31.phpを利用するなど、別の番号のセットを使用してください。その場合ソースコードの記述が変わる部分がありますが、可能な限り注記します。)
3Contextsの中にはすでにpostalcodeコンテキストが定義されています。repeat-controlキーの値を空白にします。
4Contextsの中のQueryと書かれた背景がグレーの部分を特定します。そして、operatorを「bw」、valueを「1」に切り替えます。
Contextsにあるその他のテキストフィールドはそのまま利用します。
5Database Settingsに設定を行います。db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
6Debugについては、「2」のままにしてこの後の作業を行ってください。この演習は、デバッグ情報をみながら動作を確認します。

ページファイルの作成

1「http://localhost:9080」で開いたページに戻り「page17.htmlを編集する」をクリックし、ページファイルのpage17.htmlを編集するページファイルエディターを開きます。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="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>
ヘッダー部にあるSCRIPTタグ内にスクリプトが記述されていて、ページ合成を行うの前に実行されるメソッドでいくつかのことが行われています。INTERMediatorOnPage.getURLParametersAsArrayメソッドは、URLのパラメーター部分にあるデータをキーと値に分離し、キーをプロパティとして持つオブジェクトに変換した結果を返します。そして、INTERMediator.clearConditionにより、postalcodeコンテキストの追加の検索条件を消去します。コンテキストのqueryに記述した条件はこのメソッドでは消えません。そして、q=のパラメーターがある場合、addConditionによって、追加の検索条件をpostalcodeに追加します。ここでは、コンテキスト定義とaddConditionの2つの条件が設定され、他に指定はないので、ANDで結合されます。なお、FileMakerは同一のフィールドに関する条件式をURL内に複数記述することはできないので、コンテキストはf3、パラメーターで与える方はf8フィールドを対象としました。

ページの表示と結果の確認

1「http://localhost:9080」で開いたページに戻り「page17.htmlを表示する」をクリックし、ページファイルのpage17.htmlを表示します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
2ページの冒頭にあるデバッグ情報には、データベースに与えた検索のためのURLの一部が見えています。その部分を特定します。
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*"}]}
この記述はFileMaker ServerのXML共有(あるいはPHP共有)を利用してデータベース処理を行う時のURLの一部です。詳細は、「FileMaker Server 14 カスタムWeb公開ガイド」の第5章に示されています。ここで重要なことは、検索条件が「フィールド名.op=演算子」と「フィールド名=値」の2つのパラメーターで表現されることです。つまり、このURLでは、「f3 bw 1」、つまり、f3フィールドの値が1で始まるという条件がURLから読み取れます。これは、コンテキストのqueryキーで指定したものだけです。ページに接続したときのURLにはパラメーターが付けられていないので、ページ合成前に実行されるメソッドで記述した検索条件は追加されません。
3ページ末尾にあるページ合成の結果を参照します。検索条件に合致するレコードが一覧されています。
4パラメーターを付加してページを表示してみます。ブラウザーのアドレス欄には「http://localhost:9080/page17.html」が表示されているので、以下のように?以下のパラメーターをつけたURLをキータイプし、returnキーを押してページアクセスを行います。
http://localhost:9080/page17.html?q=北
qの値と、もともとプログラムに設定されているプロパティ値と合わせて、「f8 cn 北」つまりf8フィールド(市区町村名)に、「北」を含むレコードが検索される条件になります。
5ページの表示ができれば、ページの冒頭にあるデバッグ情報に見えているデータベースに与えた検索のためのURLの一部を特定します。
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*"}]}
コンテキストのqueryキーで指定した条件(f3 bw 1)と、URLのパラメーターの値をもとにJavaScriptのプログラムで追加した条件(f8 cn 北)が設定されています。f8キーに対する値「%E5%8C%97」は、「北」をUTF-8で表現し、それらをURLに組み入れられるようにエンコードした結果です。2つの検索条件がありますが、それらはANDで結ばれます。
6ページ末尾にあるページ合成の結果を参照します。検索条件に合致するレコードが一覧されています。

検索のためのユーザーインターフェースと連動する

1「def17.phpを編集する」をクリックして表示したページに戻ります。閉じてしまっていれば、「http://localhost:9080」で開いたページに戻り「def17.phpを編集する」をクリックします。
2FileMakerでは複雑な条件の指定が難しいので、ここでは、コンテキストのqueryキーの設定を削除します。ContextsにあるQueryの見出しの下の行の右にある「削除」ボタンをクリックして、Query行を削除します。確認のダイアログボックスが表示されるので、OKボタンをクリックします。
3コンテキストの中のQueryの設定がなくなりました。
4「page17.htmlを編集する」をクリックして表示したページに戻ります。閉じてしまっていれば、「http://localhost:9080」で開いたページに戻り「page17.htmlを編集する」をクリックします。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="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>
ページ上に新たにテキストフィールドと、ボタンが登場しました。テキストフィールドにはid属性を設定し、ボタンにはonclick属性を設定しています。ボタンをクリックすると、search関数が呼び出されます。テキストフィールドの値をもとに検索条件を与えていますが、都道府県名、市区町村名、町域名がそれぞれf7、f8、f9フィールドなので、それぞれに対して、部分一致で検索をかけています。最初の{field: "__operation__", operator: "ex"}という検索条件により、f7〜f9についてはOR演算を行います。この検索条件の指定方法は『2-5 検索と並べ替えに関する設定』でも指定しています。ここではテキストフィールドの値をそのまま検索条件に指定していますが、プログラムで記述すると、検索条件の指定も計算式等で記述でき、さまざまな処理を経て適用することができます。INTERMediator.constructでページ合成を行いますが、ここで引数にコンテキストオブジェクトを指定すると、そのコンテキストのみ再描画されます。コンテキストオブジェクトへの参照は、IMLibContextPool.contextFromNameメソッドを使って引数にコンテキスト名を指定することで得られます。
5「http://localhost:9080」で開いたページに戻り「page17.htmlを表示する」をクリックし、ページファイルのpage17.htmlを表示します。パラメーターはなしで表示するので、もう一度「page17.htmlを表示する」をクリックして新たにページを開く方が手軽でしょう。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
6スクロールしてページ合成した部分を参照します。ここで、検索条件を指定するテキストフィールドと、「検索」ボタンが表示されています。テキストフィールドに「北」と入力して、「検索」ボタンをクリックします。
7新たに表示されたデバッグエリアに検索のためのSQLステートメントがあります。次のようなステートメントです。
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*"}]}
8ページの末尾には、検索結果のレコードを基にしたページが合成されています。町名や区名に「北」が含まれる地名が検索されています。

演習のまとめ

ページ合成およびブラウザー判定に使用したAPI

 このセクションのプログラムで使用したAPIや関連するAPIについて、まとめておきます。

INTERMediator.construct(context, recordset)

 ページ全体あるいは部分の合成を行います。データベースの内容を表示するには、必ずこのメソッドは呼び出す必要がありますが、ページをロードしたときには自動的にこのメソッドが呼び出されます。Ver.5.4-devの途中まではonloadイベントでこのメソッドの呼び出しが記述される必要がありましたが、現在は記述の必要はありません。記述の必要があるのは、ページ表示後に表示内容の更新を意図的に行うような場合です。(返り値はなし)

引数指定内容
contexttrueあるいは省略ならページ全てを合成する。contextを指定すると、そのコンテキストのみを再合成するが、その場合はIMLIbContext変数をクラスとしたコンテキストへの参照を指定する
recordsetページ全体の合成では省略する。ここにオブジェクトの配列の形式でレコードを指定すると、そのレコードに関連したレコードを、contextで指定したコンテキストに対して再合成する
表6-1-1 INTERMediator.constructメソッドの引数

INTERMediatorOnPage.INTERMediatorCheckBrowser(deleteId)

 定義ファイルの設定、あるいはparams.phpを参照して、サポートしているブラウザーなのかどうかを判定します。サポートしていない場合には既定のエラーメッセージのみを画面に表示します。なお、このメソッドは、ページをロードするときに自動的に呼びだされるため、通常は使用することはないと思われます。

引数指定内容
deleteId判定の後、非対応ブラウザーであれば削除されるBODY要素内の要素のid属性の値
[返り値]対応ブラウザーならtrue、そうでなければfalse
表6-1-2 INTERMediatorOnPage.INTERMediatorCheckBrowserの引数と返り値

INTERMediatorOnPage.doBeforeConstruct() = function() {...}

 ページ合成が行われる直前で呼び出されるメソッドで、アプリケーションはこれを呼び出すのではなく、アプリケーション側で定義しておくことで、INTER-Mediatorによって呼び出されます。引数および返り値はありません。

INTERMediatorOnPage.doAfterConstruct() = function() {...}

 ページ合成が終わったときに呼び出されるメソッドで、アプリケーションはこれを呼び出すのではなく、アプリケーション側で定義しておくことで、INTER-Mediatorによって呼び出されます。引数および返り値はありません。

INTERMediatorOnPage.isAutoConstruct

 ページをロードしたときの自動的なページ合成を行うかどうかを指定します。既定値はtrueです。ページの自動合成をさせたくないような場合、これをfalseとします。例えば、doBeforeConstructメソッドの中で特定の条件が成り立てばこのプロパティにfalseを代入して、ページ合成処理をさせないようにできます。このプロパティの値に関係なくdoBeforeConstructメソッドは定義されていれば実行されますが、doAfterConstructメソッドはこのプロパティがtrueの時のみ実行されます。

コンテキストの検索条件を追加指定するAPI

 

INTERMediatorOnPage.getURLParametersAsArray()

 自分自身のページのURLに含まれるパラメーターを、オブジェクトとして返します。

引数指定内容
[返り値]URLのパラメーターにある「キー=値」のそれぞれのセットについて、キーをプロパティ名、値をそのプロパティに対する値として持つオブジェクト
表6-1-3 INTERMediatorOnPage.getURLParametersAsArrayの返り値

INTERMediator.clearCondition(contextName)

 コンテキストに対して追加される検索条件を、指定したコンテキストに対して消去します。(返り値なし)

引数指定内容
contextNameコンテキスト名、すなわち定義ファイルのコンテキスト定義にあるnameキーの値
表6-1-4 INTERMediator.clearConditionの引数

INTERMediator.addCondition(contextName, criteria)

 コンテキストに対する検索条件を追加します。(返り値なし)

引数指定内容
contextNameコンテキスト名、すなわち定義ファイルのコンテキスト定義にあるnameキーの値
criteria検索条件を示すオブジェクト。プロパティはfield、operator、valueで、それぞれ定義ファイルでのqueryキーの配列におけるキーと対応している
表6-1-5 INTERMediator.addConditionの返り値

 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でのマスターおよびディテールのコンテキストオブジェクトを直接取り出すメソッドもあります。

図6-2-1 コンテキストオブジェクトの立ち位置

コンテキストオブジェクトとそのプロパティ

 IMLibContext変数が参照するオブジェクトをクラスとして生成するコンテキストオブジェクトには、データベースから取り出し、ページ上のいずれかのノードに展開したデータが保持されています。ページ展開で得られたデータを取り出すには、このコンテキストオブジェクトに保持された値を利用するのがひとつの方法です。表6-2-1には、コンテキストオブジェクトで利用することがありそうなプロパティをまとめておきました。

プロパティ内容
contextNameコンテキスト名
enclosureNodeエンクロージャーのノードへの参照
repeaterNodes展開前に初期状態として保持したリピーターで、ノードへの参照の配列
storeデータベースから取得し、ページに展開したデータ
storeCapturedページ展開直後のページに展開したデータ(Control+Shift+Zによる復帰をサポートするため)
表6-2-1 使用する機会のあるコンテキストオブジェクトのプロパティ

 表6-2-1で、ページに展開したデータは、storeプロパティに保持されています。storeプロパティは若干複雑なオブジェクト構成になっています。レコードを示すキー値として、「主キーフィールド名=フィールドの値」の形式を持ちます。主キーフィールド名は、コンテキスト定義のkeyキーに対する値です。keyキーの値が「id」だった場合、例えば、「id=3」などがレコードを示すキーになります。そしてひとつのレコードは、フィールド名がプロパティになり、その値がデータベースから得られた値になります。例えば、表6-2-2のようなリレーションが得られた場合、3つのフィールドがいずれもページ上に展開されれば、storeプロパティの値はリスト6-2-1のような形式になります。

idtext1num1
1suger4314
2salt2983
3saurce9223
表6-2-2 コンテキストで得られたリレーションの例
リスト6-2-1 表6-2-2から構成されるコンテキストオブジェクトのstoreプロパティ
{
    "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プロパティを探るのが一番効率的ですが、コンテキストへのデータの設定は、用意されているメソッドを利用するのが良いでしょう。基本的に、コンテキストの特定のレコードの特定のフィールドに値を設定すると、そのフィールドとバインドしているページ上の要素でも設定した値が見えます。また、その値をデータベースへ書き込む処理も行うメソッドもあります。どういうメソッドがあるかは、この後の演習と記事を参照してください。

演習フィールドを更新するボタンを設置する

 承認ワークフローを実装するようなアプリケーションにおいて、「承認した」ということを記録するために、承認日時とログイン名を記録するのがひとつの方法としてあります。その時、日時や名前などを手入力はしたくはないと考えるところでしょう。そこで、「ボタンを押すと、指定のフィールドに現在の日時が入力される」というプログラムをボタンで呼び出せば、承認ボタンに対するもっとも重要な要求が満たされます。もちろん、ユーザーに応じて異なる認証権限を与えるアクセス権設定や、承認のキャンセルをどうすればいいかなど、ワークフローに関わるアプリケーションは状態の遷移に伴って多数の要件が絡みます。この演習では、その処理の一部だけを紹介するものとなります。

定義ファイルにデータベースアクセスに必要な設定を行う

1演習環境を起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def18.phpを編集する」をクリックし、定義ファイルエディターでdef18.phpファイルを編集します。(もし、他の用途で18番目を利用しているのなら、例えば、def31.phpを利用するなど、別の番号のセットを使用してください。その場合ソースコードの記述が変わる部分がありますが、可能な限り注記します。)
3Contextsの中のQueryと書かれた背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
4「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
5Contextsにあるname、table、viewの値を「testtable」とします。key、paging、repeat-control、records、maxrecordsについてはそのまま元から入っている情報をそのまま利用します。
Sortingの次の行にあるfieldの値を「id」にします。directionは「ASC」のままにします。
6Database Settingsに設定を行います。
[MySQL]の場合
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
[FileMaker]の場合
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
7Debugについては、「false」にすると、デバッグ情報が出なくなります。なお、デバッグ情報をみながら動作を確認したい方は、「2」のままにしてこの後の作業を行ってください。

ページファイルの作成

1「http://localhost:9080」で開いたページに戻り「page18.htmlを編集する」をクリックし、ページファイルのpage18.htmlを編集するページファイルエディターを開きます。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="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>
ページファイルのHTML部分はこれまでに説明してきたことから大きく違いはありません。testtableコンテキストは、testtableそのままの内容で、日付時刻型のdt2フィールドと、連番が自動的に設定されている数値型のidフィールドを表示しています。各行には、「承認」ボタンがあり、そのレコードのdt2フィールドに現在の日付時刻を入力する機能をもたせています。ここで、testtableのどのレコードなのかを特定するために、ボタンをクリックしたときに実行されるapproval関数の引数に、idフィールドの値を指定します。そのために、onclick属性では「approval($)」のように、フィールドのデータと置き換える箇所に$を記述します。そして、同じBUTTONタグのdata-im属性に指定したターゲット指定では、testtableのidフィールドに対して「$onclick」、つまり、idフィールドの値をonclick属性の$と置き換えるという動作を指定しています。例えば、idフィールドの値が23のレコードに対しては、onclick属性の値は「approval(23)」となり、approval関数の引数に、idフィールドの値が指定されます。
2ヘッダー部のSCRIPTタブの指定箇所に、以下のプログラムを記述します。
function approval(id)	{
    const currentDT = INTERMediatorLib.dateTimeStringISO();
    const context = IMLibContextPool.contextFromName("testtable");
    context.setDataWithKey(id, "dt2", currentDT)
}
INTERMediatorLib.dateTimeStringISOメソッドは、引数なしで実行すると、現在の日付に対するISO8601形式の日付の文字列を返します。また、INTERMediatorLib.dateTimeStringFileMakerは現在の日付のFileMaker形式(ロケールに関わらず、月/日/年)の文字列を返します。IMLibContextPool.contextFromNameにより引数で指定したコンテキスト名に対するコンテキストオブジェクトへの参照を返します。コンテキストオブジェクトに対してsetDataWithKeyメソッドを実行すると、指定した主キーの値をもつレコードの指定したフィールドに指定したデータを入力できます。データはコンテキストに記録されるだけでなく、そのフィールドにバインドしている要素へも伝達され、さらにデータベースへの書き戻しも行います。したがって、画面上では「自動的に更新される」ように動作します。

ページ上のプログラムを実行してみる

1「http://localhost:9080」で開いたページに戻り、「page18.htmlを表示する」をクリックして表示したタブあるいはウインドウを表示します。testtableテーブルの内容が、1レコードずつ参照できます。最初のトライであるなら全くレコードが表示されていない状態かもしれません。ページネーションにある「レコード追加:testtable」の部分をクリックして、レコードを追加しておきます。
2いずれかのレコードの「承認」ボタンをクリックします。対応する「承認日時」のテキストフィールドに、現在の日時が設定されました。また、同じフィールドにバインドしたSPAN要素の方にも同じ日時の文字列が表示されています。
データベースに本当に保存されているのかを確認したいのであれば、ページネーションの「更新」ボタンをクリックして、ページを更新してみてください。同じ日時が2つのバインドした要素に見えているはずです。
3もう一度レコードの「承認」ボタンをクリックします。対応する「承認日時」のテキストフィールドおよびSPAN要素の部分に、現在の日時が設定されました。
4ページネーションにある「レコード追加:testtable」の部分をクリックして、レコードを追加し、「承認」ボタンをクリックします。やはり画面が更新されて、新しく作成された「承認日時」のテキストフィールドに、現在の日時が設定されました。

演習のまとめ

コンテキストオブジェクトへのデータの設定と取り出し

 IMLibContext変数によって作られるオブジェクトでは、以下のメソッドも利用できます。IMLibContextの部分は、例えば、IMLibContextPool.contextFromName("...")を使い引数に指定したコンテキスト名より得られたコンテキストオブジェクトの変数を指定します。いくつかのプログラム例を、メソッドの説明の後に示します。

IMLibContext.setValue(recKey, key, value, nodeId, target)

 コンテキスト内の指定したレコードの指定したフィールドに値を設定する。ページ合成時に、INTER-Mediatorは自動的にこのメソッドを呼び出して、コンテキストとページ内の要素との対応情報を保持する。ページ合成後に、nodeIdとtargetを省略してこのメソッドを呼び出すと、値を保存すると同時に、他のコンテキストの同じテーブルの同じレコードの同じフィールドとバインディングしている値も更新するので、結果として各要素に表示する値も更新される。ただし、データベース処理は行わず、ローカルのコンテキストの値を更新するのみである。

引数指定内容
recKeystoreプロパティのオブジェクトのプロパティ(id=1などのキー)を指定する
keyフィールド名
value
nodeId[省略可能]コンテキストのこの値とバインディングした要素のid属性値
target[省略可能]バインディングした要素のターゲット。ターゲットなしは "" を指定
[返り値]更新された要素のid属性値の配列
表6-2-3 setValueの引数と返り値

IMLibContext.setDataWithKey(pkValue, key, value)

 コンテキスト内の指定したレコードの指定したフィールドに値を設定し、バインディングされている他の要素への更新を行うとともに、データベースへの更新を行う。

引数指定内容
pkValue主キー(コンテキスト定義のkeyキーで指定したフィールド)の値のみで対象レコードを指定
keyフィールド名
value
[返り値]更新された要素のid属性値の配列
表6-2-4 setDataWithKeyの引数と返り値

IMLibContext.setDataAtLastRecord(key, value)

 コンテキストの最後のレコードにある指定したフィールドに値を設定し、バインディングされている他の要素への更新を行うとともに、データベースへの更新を行う。

引数指定内容
keyフィールド名
value
[返り値](なし)
表6-2-5 setDataAtLastRecordの引数と返り値

IMLibContext.getValue(recKey, key)

 コンテキスト内の指定したレコードの指定したフィールドの値を得る。なお、マスター/ディテール形式のユーザーインターフェースにおいて、keyに "_im_button_master_id" を指定すると、「詳細」ボタンの要素に設定されているid属性値を得られるので、プログラムでクリック操作をしたい時には利用できる。

引数指定内容
recKeystoreプロパティのオブジェクトのプロパティ(id=1などのキー)を指定する
keyフィールド名
[返り値](なし)
表6-2-6 getValueの引数と返り値

IMLibContext.getDataAtLastRecord(key)

 コンテキストの最後のレコードにある指定したフィールドの値を得る。

引数指定内容
keyフィールド名
[返り値]最後のレコードの指定したフィールドの値
表6-2-7 getDataAtLastRecordの引数と返り値

コンテキストの情報取得

 コンテキストオブジェクトからコンテキスト定義を得るなどの情報取得のためのメソッドとして以下のようなものが利用できます。

IMLibContext.getContextDef()

 定義ファイルに記述したコンテキスト定義を得る。返り値はひとつのコンテキスト定義を示すオブジェクト。

IMLibContextPool.contextFromName(contextName)

 引数に指定したコンテキスト名に対するコンテキストオブジェクトをひとつだけ返します。定義ファイルのコンテキスト定義よりひとつのコンテキストしか生成していない場合に利用します。

引数指定内容
contextNameコンテキスト名、すなわち定義ファイルのコンテキスト定義にあるnameキーの値
[返り値]コンテキストオブジェクトへの参照
表6-2-8 IMLibContextPool.contextFromNameの引数と返り値

IMLibContextPool.getContextFromName(contextName)

 引数に指定したコンテキスト名に対するコンテキストオブジェクトの配列を返します。定義ファイルのコンテキスト定義から複数のコンテキストを生成している場合に利用します。

引数指定内容
contextNameコンテキスト名、すなわち定義ファイルのコンテキスト定義にあるnameキーの値
[返り値]コンテキストオブジェクトへの参照の配列
表6-2-9 IMLibContextPool.getContextFromNameの引数と返り値

IMLibContext.getContextInfo(nodeId, target)

 要素のid属性値とターゲットから、コンテキスト情報を得る。

引数指定内容
nodeId要素のid属性値
target要素のターゲット。ターゲットなしは "" を指定
[返り値]要素とバインディングしているコンテキスト情報({context: this, record: recKey, field: key}形式のオブジェクト)
表6-2-10 getContextInfoの引数と返り値

IMLibContext.getContextValue(nodeId, target)

 要素のid属性値とターゲットから、コンテキストの値を得る。

引数指定内容
nodeId要素のid属性値
target要素のターゲット。ターゲットなしは "" を指定
[返り値]引数で指定した要素とターゲットにバインディングしている値
表6-2-11 getContextValueの引数と返り値

コンテキストを利用したサンプルプログラム

 プログラムの簡単なサンプルを示します(リスト6-2-2)。INTER-Mediatorで作成したアプリケーションでは、ひとつのコンテキスト定義をもとにしたページ上のコンテキストはひとつだけという場合がよくあります。その時、コンテキストオブジェクトを参照するには、IMLibContextPool.contextFromName(...)を利用できます。引数はコンテキストの "name" キーの値、つまりコンテキスト名を指定します。もし、コンテキストが複数ある場合には、IMLibContextPool.getContextFromName(...) を使用して、該当するコンテキストを返された配列から取り出さなければなければなりません。

 ひとつのコンテキストにレコードがひとつだけという場合はよくあります。つまり、コンテキスト定義の "records" キーの値を1にしているような場合です。コンテキストのメソッドの中に「最後のレコード」に対応するものが用意されていますが、もちろん複数のレコードの最後のレコードに適用できると同時に、1レコードしかない場合には、確実にその1レコードに対して処理をするメソッドとしても利用できます。getDataAtLastRecordメソッドで引数にフィールド名を指定すれば、データベースの値を取得できます。また、setDataAtLastRecordメソッドを利用すれば、コンテキストの値を更新してバインディングしている他の要素の値も更新するとともに、データベースの該当フィールドを更新します。

リスト6-2-2 コンテキストを利用したプログラムの例
var context = IMLibContextPool.contextFromName("contextName");
var idValue = context.getDataAtLastRecord("id");
context.setDataAtLastRecord("price", 350);

 もし、複数レコードがあるようなコンテキストを変数contextで参照していたとしたら、Object.keys(context.store)で、レコードを指定するキーが配列で得られます。必要であれば、そのキーをもとに順番に処理をしたり、あるはfor..inを利用するなどして、コンテキストの各レコードに対して処理を行うことができます(リスト6-2-3)。

リスト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のように記述します。

リスト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キーを持つ連想配列を書き並べます。

リスト6-2-5 ローカルコンテキストの初期値を設定した定義ファイルの例
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ファイルに記述しておくことでも対処ができます。

リスト6-2-6 
<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のような形式になります。つまり、左側のキーを利用すれば、右側に記載した内容のデータが得られます。なお、キーに対する値は操作によってはあったりなかったりします。

リスト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の関数呼び出しの引数に設定される値も同様)
表6-3-1 引数と返り値

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プロパティが指定される
→primaryKeyOnlytrueなら検索条件は主キーフィールドのみを指定してデータベースアクセスする(省略可能)
completion, successProcデータベース処理が成功した後に呼び出されるクロージャー
failedProcデータベース処理が失敗した後に呼び出されるクロージャー
[返り値]以下のプロパティを持ったオブジェクト(引数completionの関数呼び出しの引数に設定される値も同様)
→recordset検索して得られたレコード。1レコードはフィールドをキーとしたオブジェクトで、その配列がこのプロパティの値
→totalCount検索条件に合致したレコードの総数
→count実際に取り出したレコード数
→registeredidこのコンテキストの登録ID値。マルチユーザー利用での同期のときに利用される
→nullAcceptableデータベースがnull値をサポートすればtrue。Ver.5.3現在、積極的な利用はされていない情報
表6-3-2 引数と返り値

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の関数呼び出しの引数に設定される値も同様)
表6-3-3 引数と返り値

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データベース処理が失敗した後に呼び出されるクロージャー
[返り値]オブジェクトが返るが、アプリケーションにとって有用な情報はない
表6-3-4 引数と返り値

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の関数呼び出しの引数に設定される値も同様)
表6-3-5 引数と返り値

 このAPIを使ったサンプルプログラムとしては以下のようなものです。おおむね、前の節の演習と同様な処理になります。INTERMediator_DBAdapter.db_update_asyncは非同期通信処理を行うので、原則的にはこの呼び出し後には何も記述がないのが一般的でしょう。通信後の処理は、引数にクロージャーとして記述します。通信成功時には、コンテキストの再描画を行い、書き込んだ結果を反映させています場合によっては変数resultの結果からデータベース処理結果を取り出すこともできます。失敗時には単にアラートを出すだけになっています。

リスト6-3-2 直接的な通信処理を利用するプログラムの例
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();」の形式でキューの最後であることを示すようにします。そうしないと、次のキューに移行しません。キューの処理は非同期でも構いませんが、どこかで必ず引数の処理を実行します。

リスト6-3-3 キューに通信処理を組み入れるプログラムの例
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を使う必要はありません。

日付時刻の文字列生成のメソッド

 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のアップデート』を参照してください。

定義ファイルにデータベースアクセスに必要な設定を行う

1演習環境を起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def19.phpを編集する」をクリックし、定義ファイルエディターでdef19.phpファイルを編集します。(もし、他の用途で19番目を利用しているのなら、例えば、def31.phpを利用するなど、別の番号のセットを使用してください。その場合ソースコードの記述が変わる部分がありますが、可能な限り注記します。)
3Contextsの中のQueryと書かれ背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
4「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
5同様に、Sortingの次の行にある「削除」ボタンを押し、確認にOKボタンをクリックして、こちらの設定も削除しておきます。
6name、tableともに「item」、keyを「id」、recordsとmaxrecordsを「10000」、つまりitemテーブルのレコード数より多い数とします。
7[MySQL]の場合
viewを「item_display」にします。
[FileMaker]の場合
viewを「item」にします。
8Contextsのその他のテキストフィールドは空白にします。
9Database Settingsに設定を行います。
[MySQL]の場合
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
[FileMaker]の場合
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
10Debugについては、「false」にすると、デバッグ情報が出なくなります。なお、デバッグ情報をみながら動作を確認したい方は、「2」のままにしてこの後の作業を行ってください。

ページファイルの作成と初期データの確認

1「http://localhost:9080」で開いたページに戻り「page19.htmlを編集する」をクリックし、ページファイルのpage19.htmlを編集するページファイルエディターを開きます。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>
</table>
</body>
</html>
2「http://localhost:9080」で開いたページに戻り、「page19.htmlを表示する」をクリックして表示したタブあるいはウインドウを表示します。itemテーブルの内容が、1レコードずつ参照できます。itemテーブルは、『4-4 計算プロパティの設定』でも利用したものですが、サンプルにある「Sample_invoice」フォルダーで伝票形式のページ作成での明細で利用しているテーブルです。ここでは、product_idフィールドの値が同一のレコードが少なくとも2つ以上はある状態(以下の図ではproduct_id=3のレコードが2つあります)にしてください。ない場合には、『4-4 計算プロパティの設定』の演習結果のページで、同一product_idに対して複数の明細レコードができるように、レコードを追加してください。

合計金額を表示できるようにする

1「def19.phpを編集する」をクリックして表示された定義ファイルエディターのウインドウあるいはタブを選択して、def19.phpを定義ファイルエディターで表示します。もし、閉じてしまっていれば「http://localhost:9080」で開いたページに戻り、「def19.phpを編集する」をクリックして表示します。(別の番号のファイルで作業している場合には、その番号に応じた定義ファイルを開いてください。)
2ページ上部の「Show All」をクリックして、すべての設定項目を表示します。
3itemコンテキストのpost-enclosureに「afterItemList」と入力します。Tabキーを押して次のフィールドに移動し、入力結果を確定しておきます。
4「page19.htmlを編集する」をクリックして表示されたページファイルエディターのウインドウあるいはタブを選択して、page19.htmlをページファイルエディターで表示します。もし、閉じてしまっていれば、「http://localhost:9080」で開いたページに戻り「page19.htmlを編集する」をクリックします。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。ヘッダー部分にJavaScriptのプログラムを表示します。また、テーブルにはTFOOTタグを追加して、フッターを追加します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
<!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>
合計をテーブルのフッター部に表示することにします。単にセルをひとつだけ追加するだけでは、レコードの部分の列の並びとずれてしまいます。そこで、TFOOTタグ内には、TBODYタグ内と同じ数だけセルを表示し、一番最後のセルに対して、id属性として「total」を設定しておきます。ここでは特にスタイルは変更していませんが、例えば罫線を消すなど必要に応じてスタイル指定をすれば、より見やすくなるでしょう。
5前のステップのヘッダー部に示した箇所に、以下のプログラムを追加します。
<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>
テーブルに利用しているitemコンテキストのpost-enclosureで指定した名前は、INTERMediatorOnPageオブジェクトに定義するメソッドの名前と同一にしておきます。また、このメソッドは引数をひとつだけ持つことができ、引数にはエンクロージャーへの参照が得られます。このafterItemListは、itemコンテキストをページ上に展開し、すべてのレコードに対するリピーターがエンクロージャーに追加された後に呼び出されます。特定のコンテキストについてデータベースとHTMLの合成処理が終わった後に呼び出されます。プログラムの前半は、コンテキストオブジェクトを参照して、その内容、すなわちデータベースから取り出したデータを取り出して、すべてのレコードについて、amountフィールドの値を取り出しています。コンテキストオブジェクトを参照する変数contextに対してstoreプロパティを利用すると、データベースから取り出したデータのうち、ページ上に展開したものがすべてそこに入っています。通常は3階層のオブジェクトになっており、storeの直下はレコードを指定するプロパティ(「主キーフィールド名=主キーフィールド値」の形式の文字列)です。さらにそこから参照されるオブジェクトは、キーがフィールド名、値が対象レコードのフィールドの値となっています。構造は複雑ですが、表構造であるリレーションを、階層構造として記録したのがstoreプロパティです。変数sに対して、amountフィールドの内容を累積して合計を求めています。なお、フィールドの値は文字列として扱うので、数値への変換メソッドとしてJavaScriptの組み込み関数であるparseFloat関数を利用しています。IMLibFormat.numberFormatはカンマ付きの数値にするメソッドで、フッターにあるid=totalのセルを参照して、そこに書式化した合計値をテキストとして追加し、セル上に計算結果を見えるようにしています。
6「page19.htmlを表示する」をクリックして表示されたページファイルエディターのウインドウあるいはタブを選択してpage19.htmlを表示し、ブラウザーの更新ボタンをクリックして画面を更新します。もし、閉じてしまっていれば、「http://localhost:9080」で開いたページに戻り「page19.htmlを表示する」をクリックします。一連のレコードの最後に行が追加されており、amountフィールドの合計が表示されています。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)

金額の小計を表示できるようにする

1「def19.phpを編集する」をクリックして表示された定義ファイルエディターのウインドウあるいはタブを選択して、def19.phpを定義ファイルエディターで表示します。もし、閉じてしまっていれば「http://localhost:9080」で開いたページに戻り、「def19.phpを編集する」をクリックして表示します。(別の番号のファイルで作業している場合には、その番号に応じた定義ファイルを開いてください。)
2itemコンテキストのSortingの下にある「追加」ボタンをクリックします。本当にレコードを作成して良いかをたずねられるので、OKボタンをクリックして項目を1行追加します。
3fieldに「prodcut_id」、directionに「ASC」を指定します。小計を求めるためには、どのフィールドの値を記述にしてグループ化するかを決める必要がありますが、ここではproduct_idが同一のレコードについて小計を取ることとします。そのためには同一のグループが連続している必要があるので、ソート条件として該当するフィールドを指定します。
4ページ上部の「Show All」をクリックして、すべての設定項目を表示します。
5itemコンテキストのpost-repeaterに「afterItemRecord」と入力します。Tabキーを押して次のフィールドに移動し、入力結果を確定しておきます。
6「page19.htmlを編集する」をクリックして表示されたページファイルエディターのウインドウあるいはタブを選択して、page19.htmlをページファイルエディターで表示します。もし、閉じてしまっていれば、「http://localhost:9080」で開いたページに戻り「page19.htmlを編集する」をクリックします。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。ヘッダー部分にあるJavaScriptのプログラムを修正します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
<!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>
2つのfunctionによりメソッドが追加されています。まず、コンテキストのpost-repeaterに指定した名前と同じ名前のafterItemRecordメソッドを、INTERMediatorOnPageオブジェクトに定義します。このメソッドが定義されると、itemコンテキストのひとつのリピーターがエンクロージャーに追加された後に呼び出されます。引数には、直前に追加されたリピーターの配列が表示されます。INTERMediatorOnPage.afterItemRecordメソッドでは、INTERMediatorOnPage.getNodeIdsHavingTargetFromNodeメソッドを使用して引数に渡されたリピーターのノードより下位に存在する、ターゲット指定が「item@product_id」のノードを探しています。つまり、ページ上のproducut_idが表示されたセルのid属性を得ています。このメソッドの返り値は配列ですが、該当するノードはひとつに限るので、添え字が0の要素があれば、product_idが表示されたセルのid属性が分かります。そして実際にそのノードにある文字列をinnerHTMLプロパティで取り出しています。さらに同様に、追加されたばかりのリピーターからamountフィールドの値を取り出しています。
プログラムの最後の方に、グローバル変数のprevIdとsubtotalが定義されています。INTERMediatorOnPage.afterItemRecordメソッドは、レコードの数だけ呼び出されるのですが、1回の呼び出しで合計を求めるより、グローバル値に累積させた方がプログラムがシンプルになるので、amoutフィールドの値の小計を累積するための変数としてsubtotalを使います。そして、prevIdは前のレコードのproduct_idフィールドの値です。このフィールドの値が、直前のレコードから変化していれば、そこに小計のテーブル行を追加します。追加する処理は、addSubTotalLine関数で行います。ここで若干ややこしくなるのは、product_idの値が変わってしまった状態のレコードがすでにエンクロージャーに追加されてしまっていることです。したがって、小計自体の行は、現在のリピーターの前に追加しなければなりません。1行目よりも前に表示されないようにするために初期値を-1にして、それも交えて判定を行い、行の追加を行います。なお、addSubTotalLineのisBefore引数は、通常はリピーターの前に小計を入れますが、すべてのレコードの処理を終えた後には無条件に小計の行を追加しなければならないので、前か後かを指定できる関数にしました。
addSubTotalLineメソッドは、リピーターのノードの配列と、小計行をリピーターの前に入れるかどうかのフラグをそれぞれ引数として持ちます。実際に小計の行を作るには、コンテキストオブジェクトのrepeaterNodesプロパティから、取り置いてあるリピーターを参照し、その複製をcloneメソッドで作成して得ています。このプログラムは、リピーターのルートはひとつのTRタグ要素なので、repeateNodesの最初の要素だけを複製するだけでリピーターの複製が得られます。そして、複製したリピーターの中にあるamountフィールドを表示するセルをINTERMediatorOnPage.getNodeIdsHavingTargetFromNodeメソッドで取得しています。引数に応じて、複製したリピーターを、現在挿入されたレコードのデータを合成したリピーターの前か後かに追加をします。その後、subtotal変数の値を、amountフィールドのセルに設定していますが、小計で追加された行であることが分かるように、セルの背景を白にしておきました。
7「page19.htmlを表示する」をクリックして表示されたページファイルエディターのウインドウあるいはタブを選択してpage19.htmlを表示し、ブラウザーの更新ボタンをクリックして画面を更新します。もし、閉じてしまっていれば、「http://localhost:9080」で開いたページに戻り「page19.htmlを表示する」をクリックします。product_idフィールドの値の変わり目に行が追加されており、amountフィールドの小計が表示されています。また、合計は合計で表示されています。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)

この演習のまとめ

ページ合成処理に割り込むメソッド

 ページ合成に割り込むメソッドを、アプリケーション側で定義することができます。詳細はこのセクションの最初の部分でも解説しています。post-repeaterキーの値をメソッド名にした場合、メソッドを呼び出すときの引数は、追加したリピーターを参照しています。post-enclosureキーの値のメソッドの場合は、引数はエンクロージャーのノードです。いずれのメソッドも、返り値は不要です。

INTERMediatorOnPage.《post-repeaterキーの値》 = function(target) {...}

INTERMediatorOnPage.《post-enclosureキーの値》 = function(target) {...}

ノード検索のためのメソッド

 あるノードから、引数に指定したターゲット指定を持つノードを返すメソッドがいくつかあります。演習で使用したものは、最初のひとつだけですが、いくつか異なるバリエーションのメソッドを用意してあります。

INTERMediatorOnPage.getNodeIdsHavingTargetFromNode(nodes, targetDef)

 指定したノードの配列の子要素の中で、引数に指定したターゲット指定を持つノードのid属性値、あるいはそのノードへの参照を配列で返します。

引数指定内容
nodesルートとなるノードあるいはノードの配列
targetDef検索するターゲット指定
[返り値]該当するノードがid属性があればそのid値、id属性がなければそのノードへの参照を返す。指定したターゲット指定のノードが複数あることもあるので、返り値は文字列ないしはノードへの参照の配列になる。
表6-4-1 引数と返り値

INTERMediatorOnPage.getNodeIdsHavingTargetFromRepeater(fromNode, targetDef)

INTERMediatorOnPage.getNodeIdsHavingTargetFromEnclosure(fromNode, targetDef)

 指定したノードより上位階層にさかのぼり、最初に見つけたリピーターあるいはエンクロージャーに含まれる子要素で、引数に指定したターゲット指定を持つノードのid属性値、あるいはそのノードへの参照を配列で返します。

引数指定内容
fromNode基準となるノード
targetDef検索するターゲット指定
[返り値]該当するノードがid属性があればそのid値、id属性がなければそのノードへの参照を返す。指定したターゲット指定のノードが複数あることもあるので、返り値は文字列ないしはノードへの参照の配列になる。
表6-4-2 引数と返り値

書式設定のためのメソッド

 書式設定や数値化のためのメソッドとして以下のようなものが用意されています。もちろん、自分で作ったり、他のライブラリを利用して書式を整えてもかまいません。

IMLibFormat.numberFormat(value, digits)

 数値を3桁ごとのカンマ付きおよび小数点以下の桁数指定をして書式化した文字列を返す。

引数指定内容
value書式化の対象となる値
digits少数以下の桁数
[返り値]書式化した文字列
表6-4-3 引数と返り値

このセクションのまとめ

 ページ合成の処理に割り込むメソッドを利用することで、ページ合成の結果をダイナミックに変化させることができます。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 計算プロパティの設定』で作成したものの計算式を設定していないものを作成します。ページファイルの記述の一部は、コピー&ペーストで入力してもいいでしょう。

定義ファイルにデータベースアクセスに必要な設定を行う

1演習環境を起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def20phpを編集する」をクリックし、定義ファイルエディターでdef20.phpファイルを編集します。(もし、他の用途で20番目を利用しているのなら、例えば、def31.phpを利用するなど、別の番号のセットを使用してください。その場合ソースコードの記述が変わる部分がありますが、可能な限り注記します。)
3Contextsの中のQueryと書かれ背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
4「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
5同様に、Sortingの次の行にある「削除」ボタンを押し、確認にOKボタンをクリックして、こちらの設定も削除しておきます。
6Contextsでは、name、table、viewに「product」、keyに「id」、pagingに「true」、repeat-controlに「confirm-insert confirm-delete」、recordsに「1」、maxrecordsに「1」を指定します。その他のテキストフィールドは空白にします。
7Contextsの見出しの下にある「追加」ボタンをクリックします。追加するかどうかをダイアログボックスで確認するので、OKボタンをクリックして、コンテキストの定義をひとつ追加します。
8新しく追加されたコンテキストでは、name、table、viewに「item」、keyに「id」、epeat-controlに「confirm-insert confirm-delete」を指定します。その他のテキストフィールドは空白にします。
9後から作ったitemコンテキストの中のRelationshipの見出しを見つけます。その下にある「追加」ボタンをクリックします。追加するかどうかをダイアログボックスで確認するので、OKボタンをクリックして、設定を1項目分追加します。
10Relationshipの新しい行では、foreign-keyに「product_id」、join-fieldに「id」、operatorに「=」を指定します。Tabキーを押して次のフィールドに進み、入力結果を確定させておきます。
11Database Settingsに設定を行います。
[MySQL]の場合
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
[FileMaker]の場合
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
12Debugについては、「false」にすると、デバッグ情報が出なくなります。なお、デバッグ情報をみながら動作を確認したいので「2」のままにして、この後の作業を行ってください。

ページファイルの作成と初期状態の確認

1「http://localhost:9080」で開いたページに戻り「page20.htmlを編集する」をクリックし、ページファイルのpage20.htmlを編集するページファイルエディターを開きます。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。なお、このページは、『4-4 計算プロパティの設定』の演習で作成したページとおおむね同一です。そちらのページファイルの内容をコピーして編集してもかまいませんが、いくつか削除が必要な箇所もありますので、以下のような結果になるように変更をしてください。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
<!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>
ページファイルでは、2つのテーブルがあり、最初のテーブルの中にはさらにテーブルがあります。最初のテーブルはproductコンテキストを使用し、その中にあるテーブルはitemコンテキストを利用しています。productテーブルのある商品とその売り上げに対する1対多の関係にあるitemテーブルのレコードが、内部のテーブルでいくつか表示されます。ページの最後の方にあるテーブルは、Post Onlyモードでitemコンテキストに新しいレコードを作るためのものです。ここまでは、『3-4 入力専用のPost Onlyモード』や『4-3 複数のコンテキストとリレーションシップ』で説明した内容と大きくは違いません。
2「http://localhost:9080」で開いたページに戻り、「page20.htmlを表示する」をクリックして表示したタブあるいはウインドウを表示します。productテーブルの内容が、1レコードずつ参照でき、それぞれのページではproductテーブルのidフィールドと同じ値のproduct_idフィールドの値を持つitemフィールドの値が表示されています。また、ページの下の方には、Post Onlyモードのテーブルが表示されています。
3Post Onlyモードのテーブルのqtyとunitpriceに適当な数値を入力して、「追加」ボタンをクリックします。itemテーブルを展開している部分に変化はありません。デバッグメッセージの中で、MySQLではINSERTステートメント、FileMakerでは-newパラメーターでXML共有へのアクセスを記録している部分を参照すると、qtyとunitpriceの2つのフィールドにしか値は設定されていません。itemテーブルのproduct_idフィールドはnullになっているので、リレーションシップ上ではproductテーブルのレコードとの関連はないレコードが作成されてしまいました。
[MySQLの場合]
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

外部キーのフィールドにも適切な値を入力する

1「page20.htmlを編集する」をクリックして表示されたページファイルエディターのウインドウあるいはタブを選択して、page20.htmlをページファイルエディターで表示します。もし、閉じてしまっていれば、「http://localhost:9080」で開いたページに戻り「page20.htmlを編集する」をクリックします。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。ヘッダー部分にJavaScriptのプログラムを追加します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
<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>
INTERMediatorOnPage.processingBeforePostOnlyContextメソッドは、Post Onlyモードでの「追加」ボタンを押した直後に呼び出されます。まだ、データベース処理に移行する前であり、ここで値のチェック等も行えます。ここでは、一番外側のテーブルで利用しているproductコンテキストのコンテキストオブジェクトを参照し、最初のレコードのidフィールドの値を変数pidに取得しています。productコンテキストは1レコードのみなので、一番最初のキーのものしか存在しません。そして、INTERMediator.additionalFieldValueOnNewRecordを利用すると、指定したコンテキストに対して新たなレコードを作成するとき、フィールドの初期値を指定できます。ここではitemコンテキストに対してproduct_idの値が、productコンテキストのidフィールドの値になるような定義が追加されることになります。INTERMediatorOnPage.processingBeforePostOnlyContextメソッドは明示的にtrueを返さないと、レコード作成の処理が中断して、データベース処理に入りません。
2「page20.htmlを表示する」をクリックして表示されたページファイルエディターのウインドウあるいはタブを選択してpage20.htmlを表示し、ブラウザーの更新ボタンをクリックして画面を更新します。もし、閉じてしまっていれば、「http://localhost:9080」で開いたページに戻り「page20.htmlを表示する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
3Post Onlyモードのテーブルのqtyとunitpriceに適当な数値を入力して、「追加」ボタンをクリックします。itemテーブルを展開している部分に変化はありませんが、デバッグメッセージの中のINSERTステートメントの部分(図では選択している部分)を参照すると、qtyとunitpriceに加えて、外部キーとなるproduct_idフィールドの3つのフィールドに値が設定されています。product_idフィールドへの追加は、プログラムで追加した部分により実現されています。
[MySQLの場合]
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

入力後にコンテキストを更新する

1「page20.htmlを編集する」をクリックして表示されたページファイルエディターのウインドウあるいはタブを選択して、page20.htmlをページファイルエディターで表示します。もし、閉じてしまっていれば、「http://localhost:9080」で開いたページに戻り「page20.htmlを編集する」をクリックします。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。ヘッダー部分にJavaScriptのプログラムを追加します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
<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>
INTERMediatorOnPage.processingAfterPostOnlyContextメソッドは、Post Onlyモードでの処理において、データベース処理が成功すると呼び出されます。引数には、エンクロージャーのノードと、新たに作成されたレコードの主キー値が設定されて、定義したメソッドの呼び出しが行われます。ここでは、単にitemコンテキストの再描画を行っているだけです。これにより、itemテーブルに関連レコードが作成され、再度itemコンテキストの展開が行われ、増えたレコードが追加されて表示されます。
2「page20.htmlを表示する」をクリックして表示されたページファイルエディターのウインドウあるいはタブを選択してpage20.htmlを表示し、ブラウザーの更新ボタンをクリックして画面を更新します。もし、閉じてしまっていれば、「http://localhost:9080」で開いたページに戻り「page20.htmlを表示する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
3Post Onlyモードのテーブルのqtyとunitpriceに適当な数値を入力して、「追加」ボタンをクリックします。itemテーブルを展開している部分に追加されました。データベースへの入力とともに、コンテキストの更新をその後に行うことで、追加後に即座に表示されるようになりました。

演習のまとめ

演習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のページにジャンプします。ここで購入品目と、それに合わせた合計金額が表示されています。ロジック的なものは、このページのコンテキストに定義した計算式により、選択した品目に応じた合計金額が表示されるということです。この状態からの発展については、ページを作成する作業の後に説明します。

図6-5-1 名前と購入品目を指定するページ
図6-5-2 前のページに応じて、合計金額表示される

最初のページ用の定義ファイルに必要な設定を行う

1演習環境を起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://localhost:9080」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
この演習では、2つのページを作成します。以下の流れでは、page21.html/def21.phpとpage22.html/def22.phpを利用します。もし、それらの番号を他の用途に使ってしまっていれば、別の2組のセットを利用してください。連続していなくてもかまいませんが、連続している方が解りやすいでしょう。異なる番号のセットを利用した場合、ソースコードの記述が変わる部分がありますが、可能な限り注記します。
2「def21.phpを編集する」をクリックし、定義ファイルエディターでdef21.phpファイルを編集します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
3Contextsの中のQueryと書かれ背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
4「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
5同様に、Sortingの次の行にある「削除」ボタンを押し、確認にOKボタンをクリックして、こちらの設定も削除しておきます。
6Contextsでは、name、table、viewに「testtable」、keyに「id」と指定します。Contextsにあるその他のテキストフィールドは空白にします。
7Database Settingsに設定を行います。
[MySQL]の場合
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
[FileMaker]の場合
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
8Debugについては、「false」にすると、デバッグ情報が出なくなります。なお、デバッグ情報をみながら動作を確認したい方は、「2」のままにしてこの後の作業を行ってください。

最初のページのページファイルの作成

1「http://localhost:9080」で開いたページに戻り「page21.htmlを編集する」をクリックし、ページファイルのpage21.htmlを編集するページファイルエディターを開きます。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。番号にかかる部分は、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="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>
Post Onlyモードのページ部分はすでに説明した通りで、testtableコンテキストにあるtext1、num1、num2、num3の4つのフィールドにページ上で指定した値が設定されるようになっています。SCRIPTタグのプログラムを見てください。INTERMediatorOnPage.processingAfterPostOnlyContextは、Post Onlyモードでのデータベース処理が終わった後に呼び出されるメソッドです。引数は新たにtesttableテーブルに作成したレコードの主キーフィールド値が設定されます。つまり、testtableテーブルのidフィールドであり、コンテキスト定義のkeyキーに対する値が主キーフィールド名になります。その値が120であれば、変数returnValueが120になり、結果として「location.href = "page22.html?id=120;」というJavaScriptのプログラムが実行されます。結果的にページに表示される内容は、page21.htmlからpage22.htmlに移行しますが、id値をパラメーターに伴って別のページが呼び出されることになります。

2番目のページの定義ファイルを設定する

1「def22.phpを編集する」をクリックし、定義ファイルエディターでdef22.phpファイルを編集します。(もし、他の用途で22番目を利用しているのなら、例えば、def31.phpを利用するなど、別の番号のセットを使用してください。その場合ソースコードの記述が変わる部分がありますが、可能な限り注記します。なお、以下画面ショットはpage32.html/def32.phpを使用しています)
2Contextsの中のQueryと書かれ背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
3「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
4同様に、Sortingの次の行にある「削除」ボタンを押し、確認にOKボタンをクリックして、こちらの設定も削除しておきます。
5Contextsでは、name、table、viewに「testtable」、keyに「id」、recordsに「1」と指定します。Contextsにあるその他のテキストフィールドは空白にします。
6ページの最初にある「Show All」ボタンをクリックして、すべての設定項目を表示します。
7Calculationsの下にある「追加」ボタンをクリックし、確認のダイアログボックスでOKをクリックして、項目を作成します。全部で4つの項目を作成し、それぞれ、fieldとexpressionに次のようにフィールド名と式を設定します。
field = total, expression = num1 * 2000 + num2 * 3500 + num3 * 3333
field : item1, expression : if ( num1 == 1, 'inline', 'none' )
field : item2, expression : if ( num2 == 1, 'inline', 'none' )
field : item3, expression : if ( num3 == 1, 'inline', 'none' )
合計金額はtotalという計算プロパティで計算されます。現状は個数を指定しているのではなく、num1〜num3については買うなら1、買わないのならnullになります。計算においてはnullは0になるので、チェックのついた項目に対する金額、2000円、3500円、3333円のいずれかの合計がtotalに求められます。item1〜item3については、num1〜num3の値が1なら、「inline」となり、displayスタイル属性に指定することで、SPANタグ内の文字列を表示します。値が1でなければ「none」をdisplayスタイル属性に設定して非表示にします。この次のページファイルで、totalやitem1〜item3がどのような使われ方をしているのかを確認してください。
8Database Settingsに設定を行います。
[MySQL]の場合
db-classは「PDO」のままでかまいません。dsnに「mysql:host=db;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
[FileMaker]の場合
db-classを「FileMaker_DataAPI」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「gateway.docker.internal」、portに「443」、protocolに「https」、cert-vefifyingに「false」と入力します。
9Debugについては、「false」にすると、デバッグ情報が出なくなります。なお、デバッグ情報をみながら動作を確認したい方は、「2」のままにしてこの後の作業を行ってください。

2番目のページのページファイルの作成

1「http://localhost:9080」で開いたページに戻り「page22.htmlを編集する」をクリックし、ページファイルのpage22.htmlを編集するページファイルエディターを開きます。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="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">なべ&nbsp;</span>
        <span data-im="testtable@item2@style.display">やかん&nbsp;</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>
このページのTABLEではtesttableコンテキストを利用していますが、ヘッダー部分のプログラムにあるように、URLのパラメーターから取り出した値を、INTERMediator.addConditionメソッドを使ってtesttableコンテキストに検索条件として加えています。このプログラムについては、『6-1 再合成を利用した検索ページ』での演習とほぼ同様ですが、ここではid=のパラメーターが設定されていない場合はページ合成も行いません。通常、レコードとして存在する値をid=の後に与えて検索条件としてその値が指定されれば、主キーなので必ず1レコードが返ります。しかしながら、念のためにコンテキスト定義のrecordsも「1」にしています。なお、「決定」ボタンを押しても何も起こりません。プログラムを実装していないということです。

実際にページの移動を確認する

1「http://localhost:9080」で開いたページに戻り「page21.htmlを表示する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
2名前やチェックボックスを適当に設定して、「購入」ボタンをクリックします。
3次のような警告が出る場合もありますが、ここでの検証ではこれは無視してOKをクリックしてそのまま続けて問題はありません。この警告が出てもレコードは作成されます。
4名前と、購入品目、総合計を確認するページに移動しました。URLがpage22になっており、id=の後に整数が設定されていることを確認します。

演習のまとめ

入力と確認をより確実に行わせるために

 直前まで行っていた『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属性に記述する値です。

リスト6-6-1 jquery_datepicker_im.js
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なし要素の値を取り出して返す
表6-6-1 data-im-widget属性のある要素に付加するメソッド

 なお、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属性が設定されている要素に追加する必要があります。それ以上の実装に関することは、コンポーネントの事情に応じて対処する必要があります。