Category Archives: JavaScript

[IM]JavaScriptのメソッド内関数だけをテストする

INTER-Mediatorだけの話ではなく、JavaScript一般的な話です。メソッドの中に、そのメソッドの中だけで使う関数を書くということが可能なので、INTER-Mediatorではそういう実装をあちらこちらでやっています。処理の多いメソッドでは関数に分離することで見通しが良くなりますが、加えて、メソッド内の関数レベルでのテストをするということも考えました。

以下のプログラムで、変数anObjectはあるオブジェクトを参照しています。オブジェクトはmethod1というメソッドがあり、そのメソッドの中で、内部の関数であるfunc1を呼び出しています。このプログラムにより、コンソールには、”method1″ “func1″と2行の出力が行われます。

var anObject = {
  prop1: null,
  method1: function() {
    console.log("method1");
    func1();
    // method1のその他の処理
    function func1() {
      console.log("func1");
    }
  }
}

anObject.method1();

ここで、func1だけをテスト対象にしたく、かつmethod1の実処理を行いたくないと考えたとします。その場合、次のようにプログラムを変更しました。このプログラムは、こちらのページの記載を参考にしました。

var bObject = {
  prop1: null,
  method1: function(isTesting) {
    console.log("method1: isTesting="+isTesting);
    this.func1 = function() {
      console.log("func1");
    };
    if(isTesting) {
      return;
    }
    this.func1();
    // method1のその他の処理
  }
}

bObject.method1();
(new bObject.method1(true)).func1();

ポイントは、内部関数func1をプロパティにしていることです。そして、テストか通常利用かを示す引数isTestingを追加します。この引数は省略すると、undefinedになりますので、テスト時はtrueを追加するとして、省略時は何も指定しないことにします。テストの時には、プロパティへ内部関数の設定だけを行い、メソッドはそのまま終了します。

最後からの2行目は、普通にmethod2を利用する場合を想定しており、コンソールには、”method1: isTesting=undefined” “func1″と出力されます。この記述は、最初のプログラムの最後の行にある「anObject.method1();」と全く同じで、method1はつまりは通常利用では何も変更せずにそのまま利用できます。

一方、一番最後の行の実行により、コンソールには”method1: isTesting=true” “func1″と表示されます。newがあることでmethod1をコンストラクタとして実行するので、メソッド自体を返し、その結果、func1関数がメソッドのように実行できます。コンソールに”func1″と表示するのは、method1内部のthis.func1()を実行している部分ではありません。isTesting編集がtrueなので、その前にmethod1は終了します。つまり、「(new bObject.method1(true)).func1();」という記述で、func1だけを実行できるので、この関数だけの単体テストができるということです。

ちなみに、この方法は、method1に引数があっても利用できます。

[IM]日付時刻関数の実装

INTER-Mediatorに日付時刻関数を実装しようとしています。というか、少し実装しました。とりあえずの目標はマニュアルに書いた関数のサポートです。クライアントサイドで動かすので、JavaScriptの仕組みとうまく連動させないといけません。しかし、そのままのスペックはちょっとどうかと思い、このような仕組みを考えました。

日付や時刻は原則として整数あるいは小数点数を取るにしても、連続した数値になります。この点は問題ないのですが、JavaScriptなら1ミリ秒が「1」、Excelだと1日が「1」というように、そのルールを知らないといけませんし、処理系によっては1秒が1の場合もあります。覚えておけばいいといえばいいのですが、決まっているから書かないという状況になると、つどつど調べないといけません。これは不便です。

この1単位の問題に加えて、データベースでは、DATE、TIME、DATETIMEという3つの型を併用します。もちろん、1ms=1にまとめるメリットはあるかと思いますが、あえて、INTER-Mediatorの中では、2つのスタンダードを作ることにしました。

  • 1日=1、つまり、DATE型フィールドに対応(date関数で生成=日付データ)
  • 1秒=1、細かい点はさておいて、TIME、DATETIMEに対応(datetime関数で生成=日時データ)

とします。日付の計算は日単位、日付時刻の計算は秒単位とするわけです。つまり、

  • date(‘2014-10-8’) – date(‘2014-10-6’) → 2
  • datetime(‘2014-10-8 09:00:00’) – datetime(‘2014-10-6 21:00:00’) → 129,600(36時間)

とすることで、データベースの型と、日付時刻関数の結果の対応付けを考えました。

ちなみに、JavaScriptではなぜgetMonth関数だけが0〜11になって実際の月の数値を得るには+1しないといけないのかとか、見方を変えればgetDate関数が0スタートになっているべきなのではなど、疑問は多々わきます。日付計算のしやすさなどの理由はあるかもしれませんが、ここがけっこうJavaScriptのはまりどころだったりします。

そこで、日付時刻の要素取得(つまり月や日を数値として取り出す)関数と、特定の要素に対する計算を行う関数を用意すれば、結果的には「見える通りの数値」として扱えるのではないかと考えました。たとえば、月を得る関数がmonth()だとして、11月なら11という数字が得られ、加えて3ヶ月後などの日付を求めるaddmonth(d, x)があれば用途は足りると考えたわけです。しかし、日付と日時というダブルスタンダードを持ち込むデメリットがここで発生します。整数化された数値を見て、どちらの型なのかがわかりません。結果的に、

  • 日付データの月を返す:monthd(x)
  • 日時データの月を返す:monthdt(x)
  • 日付データに指定した月数を加えた値を返す:addmonthd(x)
  • 日時データに指定した月数を加えた値を返す:addmonthdt(x)

という手法にならざるを得ないと考えました。関数が増えるだけ面倒もあるかもしれませんが、要素取得や計算の関数の末尾が「d」なのか「dt」なのかという点を注意すればいいので、さほどややこしいルールとも考えられません。

しかし、このままだと、month関数やaddmonth関数の存在が気になります。「なし」でもいいのですが、名前的にはもったいないです。考えたのは、なんらかの設定で、日付計算するか、日時計算するのかということを、定義ファイル上のオプション指定で決められるようにするという手法です。たとえば、デフォルとは日付としても、何かの指定をすれば日時になるという具合です。キーワードなどはまだ考えていません。つまりこういう関数があるということです。

  • 月を返す(単位は設定依存):month(x)
  • 指定した月数を加えた値を返す(単位は設定依存):addmonth(x)

 

関数の数は増えますが、把握できる内容ではないかと思います。

タイムゾーンもまともに考えれば頭が痛いですが、日時の解釈の問題と考え、そしてデータベース内ではタイムゾーンは一定にする、あるいはしたいぞと思うことが普通なので、日付あるいは日時データの書式化の問題かとも考えていますが、ここはまだじっくり考えていません。

以上のような方針で実装を考えていますが、どうでしょうか?ちなみに、今現在レポジトリにあるものは、date関数とdatetime関数は実装されています。

JavaScriptの日付のパースはあまりに微妙

JavaScriptの日付や時刻の処理ははまりどころが多いのでも有名ですが、こんなはまりポイントを発見したので、まとめておきます。JavaScriptのDateクラスを使えば日付時刻を記録できるオブジェクトです。getTime()メソッドで1970年1月1日からのミリ秒経過時間(基準からの経過時間)を得て計算するのが一般的なパターンです。文字列で与えられた日付や時刻があったとき、Dateクラスのスタティックメソッドのparseにより、基準からの経過時間が得られます。MDNのドキュメントには、RFC2822 or ISO 8601に従った文字列ならOKとなっています。前者は「Oct 5, 2014」、後者は「2014-10-05」といった文字列です。

では、「2014-10-5」でも一瞬いいのじゃないかなと思って調べてみました。実は、この形式はISO 8601のルール上では間違いです。月と日は常に2桁である必要があります。まず、Chromeです。最初の2行を見れば、一見すると、2014-10-05でも、2014-10-5でもいいように見えます。しかし、Date.parse()の数値を見ると、右側の実行結果の2行目になりますが、ちょっとだけ違う数値になっています。3月3日について、月日の1ないしは2桁の全ての組み合わせてやってみたのが、引き続く4行です。最初の2行に戻ると、9時間の差があります。

shot8846

つまり、Chromeでは、ISO 8601に従った「2014-10-05」は、日本時間の10月5日のAM 9:00であり、そうでないものは標準時での10/5 0:00になるとうことですね。つまり、日本時間にすると9時間前なので、ちょっとだけ小さな数字になります。

Firefoxを見てみましょう。なんと、「2014-10-5」などのISO 8601のルールをはずれたものは、エラーとなります。正しいといえば正しい。

shot8847

Chromeで次のような式を計算したら、-1になったんですよね。0になることを期待しますが、そうではなかった。それで気づきました。それに、Firefoxでは結果がでないということもありますね。

Date.parse("2014-10-5") / (60*60*24*1000) - Date.parse("2014-10-05") /(60*60*24*1000)

ほんとにはまりどころ満載です。

[IM]コンテキストの共有化とPusherの利用

INTER-Mediatorでは、「コンテキスト」は、データベースに対するデータの出入り口的なイメージのものであり、検索条件などでの意味づけされたデータソースを意味します。その「共有化」とは、同一エンティティが複数のページ上のオブジェクトに展開されているとき、1つのエンティティを変更すると、その結果が他のオブジェクトにも反映される仕組みと定義します。Ver.4.4までに、単一ページ内のコンテキストの共有化が実現しています。つまり、あるページ上に、同一フィールドとバインドした要素があるとすると、一方を変更すると、もう一方は自動的に更新します。この動作を実現するためのプログラミングは必要なく、バインドの設定(ターゲット指定の付与)だけで可能です。

Ver.4.5に向けて、コンテキストの共有化をマルチクライアントで実現する仕組みを開発しており、概ね動くところまできました。つまり、同一のページを複数のクライアントで参照しているとき、誰かがデータを変更すると、その結果は他のユーザのページにも反映されるという動作が典型的です。従って、1つのフィールドを単一の要素にバインドしている場合でも、マルチユーザつまり複数のブラウザで同一のエンティティをバインドしているという点で「共有化」されていると言えるわけです。

コンテキストの共有化を実現するために、ページファイル上でのターゲット指定や、定義ファイルでのコンテキスト定義以外に何をしなければならないかをこの文書にまとめておきます。単一ページ内のコンテキストの共有化は特別な仕掛けは不要です。しかしながら、マルチクライアントでのコンテキストの共有化では、WebRTCを利用したPusherというサービスを利用することにしました。試用程度なら無償ですが、実運用には有償となってしまうものの、開発の効率化のために利用することにしました。

Pusherアカウントの取得とアプリケーション登録

Pusherのサイトでアカウントを取得します。Pusherでは「App」という単位で管理ができるので、たとえばINTER-Mediatorで作る1つのソリューションを、1つのPusherのAppとして登録するという方法もありますし、複数のソリューションで共有してもいいかもしれません。いずれにしても、アカウントを作成し、New Appというボタンなどで新たに1つのAppを作成します。ページ上に表示されるapp_id、key、secretの3つの情報がこの後に必要となります。

Pusherのサーバプログラムのインストール

PusherのサーバモジュールはPHP版を利用します。こちらのレポジトリをダウンロードし、そこから得られるlibディレクトリにあるPusher.phpという1つのファイルだけをサーバにインストールします。他は使用しません。ファイルはPHPの設定ファイル(php.iniが代表的)で、include_pathの設定で参照できるディレクトリにあればかまいません。もっとも安直な方法は、INTER-Mediatorフォルダに入れて、サーバにコピーしておくことです。もし、設定が以下のようなものであれば、例えば/usr/lib/phpディレクトリにPusher.phpをコピーしておけば良いでしょう。

include_path = ".:/usr/lib/php/pear:/usr/lib/php"

ページファイルへの追加

Pusherのクライアントソフトウエアを、ページファイルで組み込む必要があります。たとえば、以下のように、ヘッダ部で定義ファイル(include_MySQL.php)の読み込みの前に読み込みます。この方法だと、Pusherのサイトから直接取り出すので、ファイルを自分のサーバにコピーする必要はありません。ソースはこの通りコピペで大丈夫ですが、Pusherのバージョンが変わった時などはそれに合わせてください。

<html>
<head>
    :
    <script src="http://js.pusher.com/2.2/pusher.min.js" type="text/javascript"></script>
    <script type="text/javascript" src="include_MySQL.php"></script>
</head>

定義ファイルあるいはparams.phpへの追加

Pusherで定義したAppに関する指定は、定義ファイルのオプション部あるいはparams.phpで指定をします。原則的にはどちらか一方で定義をしてください。両方指定すると、定義ファイルの方が優先されます。定義ファイルでは、pusherをキーにした配列を定義し、さらにPusherのAppで示された3つの値を配列の各要素の値とします。以下は、定義ファイルでの定義例です。

IM_Entry(
    array( 
              /* コンテキストの定義 */ 
       ),
    array(
        :
        'pusher' => array(
            'app_id' => '1234',
            'key' => '9876543210',
            'secret' => '9876543210',
        ),
    ),
    array('db-class' => 'PDO'),
    false
);

params.phpファイルに記述するときには、以下のように、$pusherParameters変数に同様な配列として定義をします。

$pusherParameters = array(
 'app_id' => '1234',
 'key' => '9876543210',
 'secret' => '9876543210',
);

上記のいずれかがあると、マルチクライアントのコンテキストの共有化がオンになります。定義ファイルあるいはparams.phpの指定の有無だけで、共有化の利用/不使用が決まります。指定がないと一切何も行いません。指定があるのに、Pusherのサーバあるいはクライアントソフトウエアが利用できない状態になると、なんらかのエラーが発生します。

現状での制約

レコードの追加においては、そのコンテキストの検索条件を加味して、検索条件に合わないレコードの追加は行いません。しかしながら、別のクライアントで作成したレコードが当初はコンテキストに合わないものの、フィールドの値を変更してコンテキストの検索条件に合うようになっても、現状ではそのレコードが見えるようにはなりません。

さらに、コンテキストのソート条件は現状では加味されておらず、一連の表示リストのサイトに常に追加されます。

[IM]JavaScriptコンポーネントのプラグイン

以前から、HTMLエディタのtinyMCE、コードエディタのCodeMirrorや独自作成したファイルアップロードコンポーネントを使えるようにしていたのですが、なんとか「プラグイン」的に使える状態になったので、一度ドキュメントを作成します。JavaScriptで作ったコンポーネントに、データベースにあるフィールドの値を表示し、修正するとそれが書き戻される仕組みを提供します。ただし、コンポーネントごとに、初期化の方法は違うので、その部分を吸収するプラグインを作らないといけません。

JavaScriptコンポーネントの使い方

まず、JavaScriptのコンポーネントを使いたい場合には、次のように、data-im-widget属性にキーワードを書きます。プラグインができていればこれだけです。

<div data-im="testtable@text1" data-im-widget="tinymce"></div>

ここで、tinyMCEを使うにはプラグインが必要ですが、これについては、すでにINTER-Mediatorの中にあります。Samples/Sample_webpage/tinymce_im.jsがそれなので、たとえば、ページファイルのヘッダ部に、次のような記述を行ってtinyMCE自身の読み込みと、プラグインの読み込みを行っておきます。もちろん、パスは適切なものを指定してください。

<script type="text/javascript" src="tinymce/js/tinymce/tinymce.min.js"></script>
<script type="text/javascript" src="tinymce_im.js"></script>

Ver.4.4現在、tinyMCE、CodeMirror、それから独自に作ったファイルアップロードコンポーネント(Samples/Sample_webpage/fileupload_MySQL.htmlがサンプル)が利用可能です。

JavaScriptコンポーネントプラグインの作り方

プラグイン(前記のtinymce_im.jsに相当)は、JavaScriptで記述します。もちろん、tinymce_im.jsもサンプルとして参照する必要があるでしょう。

プラグインのファイルでは、IMParts_Catalog変数のオブジェクトに、プラグインのオブジェクトを追加します。このときのキーが、data-im-widget属性に指定するキーワードとなります。以下はその基本構造です。

IMParts_Catalog["tinymce"] = {
    instanciate: function (parentNode) { },
    ids: [],
    finish: function (update) { }
}

右辺のオブジェクトは、instanciateとfinishという2つのメソッドを持つ事が重要です。INTER-Mediatorは、ページ合成時に、im-data-widget属性があるノードを見つけると、そのノードを引数にとって、instanciateメソッドを呼び出します。

一方、ページ合成の最終段階、つまり、DOMオブジェクトが確定してページ上に存在する状態になった後に、finishメソッドが呼び出されます。結果的に、im-data-widget属性が設定された要素×レコード数の回数instanciateメソッドが呼び出され、最後に1回finishが呼び出されます。

プラグインの作業として必要なこと

この2つのメソッドが行うことは、コンポーネントに対するゲッタおよびセッタメソッドをそれぞれ展開したコンポーネントに対して設定することです。また、対応コンポーネントのid属性値を得るメソッドも実装します。たとえば、instanciateメソッドに記述するとしたら、次のようになります。instanciateメソッドを呼び出されたときの引数parentNodeあるいはコンポーネントのルートの要素に対して、以下の決められた名称のメソッドを実装します。メソッドの中身はtinyMCEの場合の例です。

parentNode._im_getComponentId = function () { // data-im-widgetのある要素に設定
    var theId = newId;
    return theId;
};
parentNode._im_setValue = function (str) { // data-im-widgetのある要素に設定
    var targetNode = newNode;
    targetNode.innerHTML = str;
};
targetNode._im_getValue = (function () { // コンポーネントのルートの要素に設定
    var thisId = targetId;
    return function () {
        return tinymce.EditorManager.get(thisId).getContent();
    }
})();

instanciateメソッドでの作業

通常、JavaScriptのコンポーネントは、特定の要素にidやclass値を適当に与えて、その要素の中に必要なオブジェクトを詰め込むといった動作をします。つまり、起点となる要素を用意しておき、そこに必要なオブジェクトを追加します。tinyMCEだと、手軽な作り方はTEXTAREAタグ要素を用意することですが、ページファイル上に記述した要素がどんな種類のタグ要素でもいいように、data-im-widget属性がある要素の子要素にTEXTAREAタグ要素を作り、その要素をtinyMCEで初期化するようにしています。

そのTEXTAREA要素のid属性は、適当に付けます。targetNodeで参照されるリピーター内の要素は、すでにid属性が設定されているので、そのid値に適当な文字を追加すれば、一意なid属性になります。_im_getComponentIdメソッドは、ここでのTEXTAREA要素に付けたidを返します。

なお、instanticateメソッド中は、まだ、リピーターはエンクロージャーに挿入されておらず、documentからたどれない状態になっています。その場合、初期化をしてもうまく動作しないと思われます。従って、instanciateメソッドでは、元になるTEXTAREA要素を作り、id番号を振り、そのid番号をidsプロパティの配列に追加して、必須のメソッドを定義するところまでしかできないのが一般的かと思います。idsプロパティはなくてもいいのですが、この後のfinalizeメソッドでそれぞれの要素を初期化するために、初期化すべき要素を後から特定できるようにするために、instanciateで作成した要素のidを残します。

instanciateメソッドの段階で実際に利用されるのは、_im_getComponentIdメソッドと_im_setValueメソッドなので、_im_getValueメソッドは実際には設定する必要はありません。_im_setValueメソッドについては、JavaScriptコンポーネントが初期化前であることを考慮して、機能する前の状態での値設定が可能なプログラムを記述する必要があります。

finalizeメソッドでの作業

この段階では、全ての要素がdocument配下にいるので、JavaScriptコンポーネントの初期化を実際に行います。なお、初期化した結果、ゲッタやセッタの動作を変えたい場合は、設定をしなおします。JavaScriptのコンポーネント内で修正した結果をデータベースに書き戻すには、_im_getValueメソッドを、初期化が終わった後の状態でのゲッタとして動作するように設定をする必要があります。単にデータベースに書き戻すだけでいいのなら、ゲッタの設定のみでかまいません。以下は、tinyMCEの例で、idsプロパティにコンポーネントのid属性値が配列として残されています。1つ1つの要素に対して、_im_getValueメソッドを追加しています。

 for (var i = 0; i < this.ids.length; i++) {
     var targetNode = document.getElementById(this.ids[i]);
     var targetId = this.ids[i];
     if (targetNode) {
         targetNode._im_getValue = (function () {
             var thisId = targetId;
             return function () {
                 return tinymce.EditorManager.get(thisId).getContent();
             }
         })();
     }
 }

tinyMCEのプラグインは、ページ上にあるすべてTEXTAREAをHTMLエディタにしてしまう動作で初期化するので、tinyMCE.initメソッドを呼び出すだけです。コンポーネントによっては、個別にオブジェクトをidsプロパティから取得したid値で参照して、それぞれに何らかのメソッドを適用しないといけないかもしれません。

結果的に、それぞれのJavaScriptコンポーネントごとにうまく初期化をしないといけませんが、場合によってはフレームワークの更新も必要になるかもしれません。

自分でJavaScriptコンポーネントを作る場合

自分で1からコンポーネントを作る場合は、以下のプラグインの骨格を作り、後は自由につくっていいでしょう。このクラスにメソッドを集めてもいいですし、他のファイルから参照してもかまいません。

IMParts_Catalog["myjscomponent"] = {
    instanciate: function (parentNode) { },
    ids: [],
    finish: function (update) { }
}

[IM]JavaScript、プロパティ、セッタ、オブザーバブル?

JavaScript一般のお話ですが、詳細はINTER-Mediatorがらみとなります。

まず、プロパティは、オブジェクトに対して自由に用意できるのですが、Object.definePropertyというメソッドを使えば、セッタ、ゲッタの実装ができます。IEのみ、このメソッドが限定された状況でしか使えないこともありますが、やはり複雑な事をするときにはセッタやゲッタを利用するのはきわめて自然な発送です。

ここで、オブザーバブルです。最近のJavaScriptフロントエンド系フレームワークではもはや実装されていて当たり前の機能です。MVCという点よりも、むしろオブザーバブルな方が重要な仕組みであるとも言えます。これは、フロントエンドなのでユーザインタフェースを構築することが大きな目的であることから来ます。ユーザインタフェース、つまりMVCのViewとModelの連動のために、イベントに対応するプログラムを書くという手法ではなく、イベントが自動的に伝搬されて行く仕組みが欲しい訳で、それをベースにすると、同一のエンティティにバインドした2つの要素が連動するとか、それをさらにはクライアント間で連動させるといったベースになるのです。ここを自動化できれば、アプリケーションの複雑さはそこそこ緩和でき、より手軽にアプリケーションの作成ができるようになると言えばいいでしょうか。

INTER-Mediatorでは、コンテキストに対する検索条件を、INTERMediator.additinalConditionというプロパティに記述することで、条件を追加できます。コンテキスト、つまりはテーブルへのアクセスにおいて、プロパティの値を条件として記述します。しかしながら、条件の記述においてはさまざまなパラメータが必要になるので、結果として、次の様な記述を行うのを基本にしています。

INTERMediator.additinalCondition["コンテキスト名"]
    = [{field: "フィールド名", operator: "演算子", value: "値"}];

連想配列や配列として解釈すると、きわめて階層が深いですが、必要な情報なので記録が必要です。右辺のオブジェクトの配列を指定できるので、単一のコンテキストで複数の条件を指定したり、あるいはもちろんのこと、コンテキストごとに追加条件を指定できます。

ここで、この追加条件を変更したときに、クッキーに記録させることを考え、なるほど、それならば、このプロパティのセッタを作ればいいのではないかと考えました。もちろん、大した実装でもないので、セッタやゲッタは簡単にできたのですが、セッタが呼ばれない事が時々あり、考え込んでしまいました。考えてみれば当たり前のことなのですが、既存のサンプルなどの動きが正しくなくなると、正しい答えに行くのに回り道をしてしまいます。

まず、以下のようにすればセッタが動きます。

INTERMediator.additinalCondition 
    = {"context": [{field: "f1", operator: "=", value: "123"]};

しかし、以下のようにするとセッタは動きません。ようするに、サンプルの多くの書き方では、セッタは動かないのです。

INTERMediator.additinalCondition["context"] 
    = [{field: "f1", operator: "=", value: "123"];

ここをえらく勘違いしていました。なぜ、後者はセッタが動かないのか? これは、additinalConditionプロパティそのものへのに対する代入をしていないからです。左辺の記述により、additinalConditionプロパティにアクセスするのでゲッタは動きます。そこで、オブジェクトがあれば取り出されて、そのオブジェクトに対して、右辺を代入しているので、additinalConditionプロパティ自体への代入はまったくしていないのです。そこから参照されているオブジェクトに対しての変更処理をしているので、additinalConditionプロパティのセッタは稼働しません。

結果的に、ここでの最下層にあるfieldプロパティなどを書き換えてもadditinalConditionプロパティのセッタが動くようにならないといけないわけでして、それを突き詰めれば、オブザーバブルな配列、オブザーバブルなオブジェクトが必要になってくるわけです。残念ながら、INTER-Mediatorではそこの実装はしていません。KnockoutやEmber.jsではそういう仕組みが用意されているのです。モデルに対するプログラミングインタフェースを持つとしたら、確かにオブザーバブルな配列は必要かもしれません。また、これらの代表的フレームワークに関わらず、オブザーバブルな配列の実装はかなりたくさん公開されています。

INTER-Mediatorに汎用的なオブザーバブルな配列やオブジェクトを組み込むのも、ある意味はいいのですが、その前に本来の目的に立ち返られないといけません。INTER-Mediatorはモデルをプログラマが定義しないと使えないのではなく、宣言的な記述で定義したコンテキストを仮想的に「モデル」として見る以上のとは通常はしなくてもかまいません。オブザーバブルな配列があれば、開発している私はいいのですが、利用者の直接的なメリットではないと考えます。

一方、プロパティの値を自動的にクッキーに入れれば、一見すると便利なのですが、アプリケーションで必要なのはむしろ、細かな記録と消去のような気がします。自動的に覚えさせるのは、確かにデモウケは良さそうですが、いくつかのサンプルに実装してみて感じることは「忘れてくれ!」と思う場面も多いことです。このあたり、どういう実装にするか非常に悩みます。今現在、セッタから呼び出されるようにしていますが、もしかすると、クッキーへの記録は明示的に開発者に記述してもらうのがいいのかもしれません。もし、現状のままだと、additinalConditionプロパティへの値の設定の書き方がきわめて限定される点も考慮すべきところです。この点はしばらく考えるつもりですが、ご意見があれば、FacebookのINTER-Mediatorのグループへどうぞ。

[IM]ローカルコンテキストの利用方法

INTER-Mediatorでは、タグのdata-im属性に、「テーブル名@フィールド名」の形式で記述するターゲット指定により、そのタグ要素がデータベースのフィールドにバインドされて、データベースから読み込んだデータを表示し、フォーム用のコンポーネントであればユーザによる変更結果をデータベースに書き戻す事が自動的に設定されます。しかしながら、データベースとは関係ない記憶領域があると何かと便利です。言い換えれば、データベースにしか保持できないとなると設計の順内製がそがれます。そこで、クライアントサイドの計算フィールドを実装し、さらにローカルコンテキストという機能を実装しました。

ローカルコンテキストの利用方法

ローカルコンテキストとは、データベースと連動していない記憶領域で、テーブル名は一律に「_」を使います。フィールド名は任意の変数名でかまいません。Sample_searchでは、type属性がtextのINPUTタグに対して、<input type=”text” data-im=”_@placeCondition”/> という設定を行いました。placeConditionがローカルコンテキスト内の名前です。この記述をするだけで、テキストフィールドに一度入力したデータは、ページを更新しても必ず再現されます。イメージとしてはどこかに記録されて、それが必要に応じて自動的に復活すると考えてください。保存場所はクッキーの中です。

通常のコンテキストは、1つのコンテキストに複数のレコードがあり、そのレコード内にフィールドがあるという構造です。ローカルコンテキストは「レコード」という構造がありません。コンテキスト名も常に決まっているので、実質的にはシンプルなキーバリューストアと同一です。一般のコンテキストは、エンクロージャーとリピーターという繰り返しを誘発する階層構造をベースにしますが、ローカルコンテキストは1ページごとに1つずつ持っており、どこにどう書いてもかまいません。同一のフィールド名のタグ要素が複数あれば、それらでの編集結果も連動します。

ローカルコンテキストへの直接アクセス

テキストフィールドがある場合、そこに入力した値を取り出すのは、通常はgetElementById等を使ってノードを参照します。一方、ローカルコンテキストを利用したテキストフィールドなら、ローカルコンテキストから値を取り出すほうが確実です。たとえば、前述のinputタグ要素の場合、

var c1 = IMLibLocalContext.getValue("placeCondition");

とすれば、テキストフィールドに入れた値を取得できます。なお、テキストフィールドをローカルコンテキストに反映するタイミングは、フォーカスがはずれたとき、つまりonchangeイベントで行っています。もし意図的に値をローカルコンテキストに記録するには、

IMLibLocalContext.setValue("placeCondition", value);

と記述します。

計算式の中に、ローカルコンテキストのリンクノードを参照する記述「_@field」などがあった場合、他のコンテキストにあるフィールドと同様なルートで検索します。つまり、同一のエンクロージャーを探し、なければ上位のエンクロージャを探してその子孫のノードを検索します。

ローカルコンテキストは、URLにひもづくクッキーに記録し、とりあえず、期限は1年にしてみました。いろいろなタイミングで自動的にクッキーに記録し、一方でクッキーから取り出すので、ほぼ、そういうことは意識しなくてもいいのですが、この後に説明する件で、一部のブラウザで書き込みを明示的に記述しないといけない場合があります。

連動するプロパティ

こうして、ローカルコンテキストが稼働しているので、一部のプロパティについては、ローカルコンテキストで保持することにしました。

INTERMediator.startFrom = 0;
INTERMediator.additionalCondition = {};
INTERMediator.additionalSortKey = {};

これらは、前から順番に、検索結果の何レコード目から表示するのか、追加の検索条件、追加のソート条件となります。たとえば、ユーザインタフェースを使って、追加の検索条件を与えると、それがローカルコンテキストに記録されて、事実上永続化されます。検索条件は、随時適用されるので、検索条件を与えるといつまでもその条件が適用されるようになります。

これらの動作に関連する情報は、以下のキー名を使ってローカルコンテキストに保存しています。なお、_im_で始まるキーは、システム予約としておきます。今後、こうした記録が増える可能性があります。

_im_startFrom
_im_additionalCondition
_im_additionalSortKey

たとえば、検索条件を特定のコンテキスト「context」に対して設定する場合、例えば、次のように記述するのは、従来と変わりありません。

INTERMediator.additionalCondition['context'] = {
    field: 'zipcode',
    operator: 'LIKE',
    value: IMLibLocalContext.getValue("criteria") + '%'
};

このとき、背後では、ローカルコンテキストに右辺のオブジェクトを記録し、さらにクッキーへの記録まで同時に行います。

Internet Explorer 8での対処

Ineternet Explorer Ver.8のみ、プロパティに記録したデータを自動的に永続化する事ができません。IE8の場合のみ、INTERMediatorオブジェクトのstartFrom、additionalCondition、additionalSortKeyプロパティに設定した直後に、

IMLibLocalContext.archive();

という呼び出しを入れて、ローカルコンテキストに反映しつつ、クッキーへの保存を行うようにします。IE9以降や、その他のブラウザではこの呼び出しはなくてもかまいませんが、あっても問題はありません。ただし、同じ作業を複数回行うことになるので、効率は低下します。INTER-Mediatorの内部ではIEやそのバージョンの把握を行っているので、たとえば、次のように記述すれば、IE8の場合だけ、前述のメソッド呼び出しが実装できます。

if (INTERMediator.isIE && INTERMediator.ieVersion < 9) {
    IMLibLocalContext.archive();
}

Internet Explorer 8にまた苦しめられる

Web系のお仲間の皆さんも同様にいつも苦しめられていると思われるIE8ですが、ここ最近にあったいくつかのはまりポイントを自分の備忘録としてもまとめておきます。まあ、あと数年はIE8からは逃れられないということで。

  • jQuery 2.0を入れたら動かない(対応ブラウザからはずれている)。慌てて1.10に戻す
  • JavaScriptではObject.keysという記述が使えない
  • 要素のid名と同一のグローバル変数が作られる

特に最後のは苦労しました。メッセージを見る限りは、「オブジェクトでサポートされていないプロパティまたはメソッドです。」と出ます。プログラムはこんな感じ。INPUTタグ要素で、idが「yourname」になっていると思ってください。

yourname = document.getElementById("yourname").value;

まさかgetElementByIdが使えないのかと思ったら、使えます。他の箇所では動いている。valueがない訳は絶対にない。当然右辺を疑うわけですが、必死に検索した結果、問題は左辺でした。つまり、以下の条件が満たされると発生されるトラブルだったのです。

  • IE8でJavaScriptでプログラムを組む
  • ページ内の要素のid属性と同一の変数を、何も定義しないでJavaScript側で使う

どうやら、IE8は、id属性と同一名のグローバル変数を勝手に定義するようです。上記のプログラムは、つまり、勝手に定義しているyourname変数が何らかのオブジェクトを記録していて、そのオブジェクトが書き換えに対応していないためにエラーが出ていると思っていいようです。

対処法は「var yourname」のように頭にvarを付けるか、ページにリンクされたjsファイル等でグローバルとして「var yourname;」のように変数定義することで回避できます。自分自身のグローバルでも、同一名称の変数は後から定義した方のストレージが有効になるので、IEが勝手に作るグローバル変数は無視されるようになるということです。つまり、変数定義を必ずしろというプラクティスをしていれば、エラーに合わないのですが、こう書いてしまったらエラーになってしまうよということですね。

INTER-Mediatorのターゲット指定を汎用化する

INTER-Mediatorでのターゲット指定は、タグ要素のclass属性に、IM[table@field@target] といった形式で記述するものだ。これにより、tableで指定したコンテキストにあるfieldというフィールドのデータを、そのタグ要素に埋め込む。targetを省略すると、フォーム要素以外ではテキストノードとして追加し、フォーム要素ではvalueやcheckedなどの適切な属性に設定される。targetでは、Ver.3.8現在、属性名、innerHTML、nodeText、$target、#target、style.styleNameをサポートしている。

このところ、いろいろなものをINTER-Mediatorで作っていて、ある同じようなパタンが頻繁に発生していることに気付いた。それは、

あるフィールドのデータがあれば表示し、なければ表示しない

という動作である。稀に逆もあるのだが、基本は同じだ。これを実現するために、たとえば、FileMakerだと、計算フィールドを利用する。あるフィールドfieldに対して、計算フィールドdisplayField = if ( field != “”; “block” ; “none” )を定義する。そしてたとえば、こんな感じのHTMLを書く。

<div class=”IM[table@displayField@style.display]”>
データ:<span class=”IM[table@field]”></span>
</div>

SQLデータベースなら、ビューを利用するのがいいだろう。たとえば、こんな感じだろうか?

CREATE VIEW tableview AS
SELECT field, if(LENGTH(field)>0, ‘block’, ‘none’) AS displayField FROM table;

これで対処はできるとは言え、もう少し簡単にならないかと考えてしまう。こうしたよくあることを手軽に対処できるというのはフレームワークに要求されることだ。もちろん、1つの方法は、ターゲット指定の3つ目のパラメータを増やす事だ。たとえば、こんなのはどうだろう?

<div>
データ:<span class=”IM[table@field|table@field@parentVisibility]”></span>
</div>

parentVisibilityという名前の属性はないので、HTMLのルールとのコンフリクトはない。フィールドの値が “” あるいは長さが0であれば、visibilityだったら自分自身、parentVisibilityだったら1レベル上位のノードのdisplayスタイルをnoneにするというところだ。

ということで、機能を増やしました…というのはなんかこの段階に来てやることかなと疑問に考えた。もっと汎用的にすべきではないのか?

そこで考えたのが、次のような仕様である。tagetの代わりに、プログラムのステートメントのような書き方をするということ。たぶん、これがいちばん汎用的と思われる。

table@field@object.method(parameters)

だが、いきなりこれだけですというのは敷居を高くするだけじゃないのかと思われるだろう。もちろん、その解決策はある。object.method(parameters)に対するエイリアスを定義することで、従来通りの記述をサポートするようにする。つまり、…@$onclickや、…@innerHTMLという記述はサポートする。ただし、これらはエイリアスという位置づけにする。ただ、内部的には#や$の扱いが微妙だが、これはif文をがんばって書くしかないだろう。

objectは、次の書き方をサポートしようと考えている。(self) (parent) (enclosure) (repeater) つまり、カッコの中に決められたキーワードを記述する。methodとparametersは任意だ。ただ、parametersは(“a”,’b’)なんて書いたときにカッコ内をそのまま渡すのはセキュリティ上の問題が出そうな感じありありであるので、ここは制約を付けることにする。

ターゲットの3つ目に記述する処理のためのオブジェクトINTERMediatorTargetを作っておく。たとえば、そこに、次のように、setInnerHTMLメソッドがあるとしよう。ここでの関数の最初の引数は、フィールドのデータであるというルールを適用することにする。

INTERMediatorTarget {
setInnerHTML: function(d) {
this.innerHTML = d;
}
}

ターゲット指定は table@field@(self).setInnerHTML() と記述する。そして、[(self).setInnerHTML()」のエイリアスが「innerHTML」であるというわけである。

ノード展開の処理では、ターゲット指定のあるノードnodeに対して、その指定に従ったフィールドのデータfieldDataが得られている。ということは、以下のようにapplyを使えばいいということになる。これは、INTER-Mediator内部の話であって、アプリケーションを作る側は気にしなくてもいい。

INTERMediatorTarget.setInnerHTML.apply( node, [fieldData] );

このノードに展開する仕上げに相当する値をセットする部分が現状ではひどくifの応酬となっているので、まずはそれを緩和したい。また、上記のような仕組みだと、INTERMediatorTargetオブジェクトへのメソッド追加や既存メソッドの書き換えにより、フレームワークの動作を改変することも可能だし、独自のターゲット指定記述を作る事もできる。

INTERMediatorTargetに記述する関数では、前記のように第1引数にフィールド値を設定するのはいいとして、汎用性を高めるために、2、3引数はフィールド名とコンテキスト名にする。これで、コンテキストやフィールドに応じて動作を変える事もできてします。

こういう実装を考えているところである。

[IM]ドラッグ&ドロップでファイルアップロード

ファイルのドラッグ&ドロップによるアップロードをINTER-Mediatorに実装しました。動作条件を整える方法はドキュメントを改めて書きますが、ページファイル上は、

class=”IM[testtable@vc1] IM_WIDGET[im_fileupload]”

のように、IM_WIDGETで、利用するコンポーネントを書くだけで、以下のムービーのようなドラッグ&ドロップによるファイルのアップロードができるようになっています。

dragdrop