はじめに
現在、Web標準をしっかりとサポートすることは当たり前になっています。Microsoft中心の環境を前提にしてアプリケーション開発を進める時代なら、おそらく標準にあまり気を使う必要はなかったでしょう。しかし、Microsoft以外の環境をも目指す現代の職業的Web技術者にとって、最新の標準(CSS、JavaScript、MIMEタイプ)を重視し、それをマスターすることは、最低限必要なスキルです。標準を重視するクライアントがしだいに増えており、標準に準拠しないアプリケーションはしだいに受け入れられなくなっています。ただ、標準が厳しく求められる一方で、標準外でありながら、依然重宝がられている数々のWebブラウザ機能があるのも事実です。XMLデータアイランド(HTMLコンテンツに埋め込まれたXMLコンテンツ)は、そのほんの一例にすぎません。本稿では、標準技術を磨きながら、そうした便利な機能も活用する方法を考えます。
データバインディングとは
データソースとデータバインディングの考え方は、大昔から存在します。何らかの静的コンテンツ(Webページでも、Javaウィンドウでも、4GLアプリケーション画面でもかまいません)を用意し、それをどこか別の場所に格納されているデータと結び付けるプロセスを「バインディング」と呼びます。その狙いは、不変のコンテンツを、時間とともに変化するデータで補完もしくは補強することですが、注意すべきは、そこで使う方法に一般性が求められるという点です。つまり、事前にデータが手許にあることを前提にしてはならず、ユーザからコンテンツ要求があるたびにコンテンツが自分自身を更新し、最新のデータ群を表示するようにしなければなりません。ODBC、JDBC、XULテンプレート、XBLは、いずれもバインディングシステムです。Microsoftのデータバインディングサービスもその一例です。バインディングでは、不完全なコンテンツを先に渡しておき、その後でデータを補完します。それに対して、PHPのようなテンプレート式コンテンツ生成システムでは、コンテンツとデータを混在させ、完成した最終的コンテンツを一気に引き渡します。バインディングシステムでは、普通、必要なデータが基本コンテンツとは別の場所に置かれていて、記述的アドレスによって参照されます。このアドレスは「データソース」と呼ばれます。
XMLデータアイランドは、上記の古臭い物語に新しい展開をもたらす考え方であり、コンテンツが引き渡されるときに、そのコンテンツにデータを付随させるという手法をとります。コンテンツとデータが同じ場所にあって、その2つを結び付ければバインディングは終わりです。少数の断片的情報を共有するだけで、つまりタグ(エレメント)とタグ属性をHTMLページ内に置くだけで、すべてのバインディングロジックを指定でき、プログラミングは不要です。次に示すのは、IEがあれほど奇妙かつ特異な構造でさえなければIEで無事に動くはずのXMLデータアイランドの例です。
<html> <head> <script> function where() { var node = document.getElementById("who"); node = node.getElementsByTagName("location"); alert(node[0].firstChild.nodeValue); } </script> </head> <body> <xml id="who"> <contributor> <name>Nigel McFarlane</name> <location>Somewhere in Cyberspace</location> </contributor> </xml> This article contributed by: <div datasrc="#who" onclick="where()" datafld="name">Nobody</div> </body> </html>
このコードでは、データアイランドが<xml>
タグで囲まれています。この例は、標準から外れたタグスープHTMLドキュメントであることに注意してください。このデータアイランドはHTMLコンテンツとクリックハンドラで一度ずつ使われています。HTMLコンテンツでは、プロプライエタリのdatasrc
属性とdatafld
属性によって<div>
タグのコンテンツを<name>
タグのコンテンツにバインディングしています。クリックハンドラでは、<location>
タグのコンテンツをただのデータとして扱い、それをドキュメントのDOMから取り出して表示しています。<div>
タグの初期コンテンツは「Nobody」ですが、ページがデータソースをロードすると、それが「Nigel McFarlane」で置き換えられます。
Internet Explorerは、Microsoftの独自コードでバインディングを実行します(これをIEの一部と見るかWindowsの一部と見るかは人によって異なります)。このやり方は、コンテンツとデータソースが同じブラウザページにあるという簡単なXMLデータアイランドのケースにとっては明らかに大げさすぎます。おかげで移植不能であることはもちろん、不必要に複雑になっています。その代わりに、JavaScriptを使用して同じ機能を実現できるので、そのソリューションをすぐにお目にかけましょう。最近広く使われるようになったXMLHttpRequest
オブジェクトを追加すれば、JavaScriptでリモートデータソースのバインディングもできますが、それはまた別の機会に譲ります。
さきほど、上記コードはIEで動くはずと言いましたが、実は動きません。データをタグと結び付ける最初のバインディングはスムーズに実行されますが、スクリプトを使ってデータにアクセスする2番目のバインディングは失敗します。これは、IEがアイランドに対して何か標準外の奇妙なことをするからです。本稿の後の方で、この制約を迂回する方法も説明します。
標準に合わせる
標準に合わせようと思うなら、昔風のタグスープHTMLでは通りません。標準を強く意識しているページにデータアイランドを埋め込むには、次の制約を守る必要があります。
- 適切なHTML標準DOCTYPE(HTML 4.01 Strictなど)を使用します。
- HTML標準では、HTMLドキュメントに新しいエレメント(タグ)を追加することを禁止していません。したがって、
<xml>
タグ(または同様のタグ)やその子を追加することは差し支えありません。しかし、そうしたエレメントをドキュメントのDTDに追加することは標準で禁じられており、匿名性の維持が必要です。たとえば、<location>
の代わりに<address>
を使用することは、<address>
が既にHTMLで意味を与えられている以上、できません。 - IEでやるように、データアイランドに
<?xml?>
処理命令を埋め込んではなりません。何よりも、HTMLはXMLではありませんし、そのような処理命令はユーザエージェントへのヒントにすぎず、クライアントブラウザに無視されるかもしれません。 <xml>
タグはドキュメントについてのタグではないので、ドキュメントの<head>
セクションに置いてはなりません。- IEのinnerHTML手法を使用してはなりません。便利ですが、標準外です。
- 標準に準拠しているWebブラウザは、XMLデータアイランドのコンテンツをただのテキストとして表示します。つまり、「Nigel McFarlane Somewhere in Cyberspace」となります。これは望ましくないので、CSSの「
display: none
」スタイルルールを用いて、XMLコンテンツが表示されないようにします。 - 最後に、ここで紹介するソリューションは、JavaScriptとCSSサポートを前提としています。このサポートが得られないときは、最初からデータソースを使用しないフォールバック手法を選んでください。行儀の良いフォールバックについての説明は別の機会に譲りますが、さして困難ではありません。
では、データアイランドの実際の使用例を考えてみましょう。以降では、データドリブンのレポート表示と、データドリブンのフォーム記入について見ていきます。
テストデータを準備する
簡単なHTMLページで実験してみましょう。次のHTMLページは、1つのデータソースと2つのバインディングロケーションを含んでいます。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <style> #who { display : none; } </style> <script src="islands.js"></script> </head> <body> <p id="who"> <contributor> <name>Nigel McFarlane</name> <location>Somewhere in Cyberspace</location> </contributor> <contributor> <name>A. Russell Jones</name> <location>DevX Editorial</location> </contributor> <contributor> <name>J. Random Hacker</name> <location>Everywhere</location> </contributor> </p> <h3>Reviewers of this article <h3> <ul datasource="who"> <li><span datafield="name"></span> , Location: <span datafield="location"></span> </li> </ul> <table border="1" datasource="who"> <tr> <td><input type="text" datafield="name"> </td> <td><input type="text" datafield="location"> </td> </tr> </table> </body> </html>
ドキュメントヘッドには、アイランドをユーザから隠すためのスタイルと、すべての処理を行う埋め込みスクリプトが含まれています。ドキュメントの本体には、3つの重要なコード片が含まれています。まず、<p>
タグで示されるデータアイランドです。このデータアイランドには3つの<contributor>
レコードが含まれ、各レコードにはname
とlocation
という子フィールドがあります。上の例で、データアイランドの定義に<xml>
タグでなく<p>
タグを用いているのは、IEのDOMサポートがお粗末なためです。IEでは、<xml>
タグ(というより、HTMLタグ以外のタグ)に含まれている内容がDOMに反映されません。つまり、このようなデータにはスクリプトでアクセスできないということですが、心配は無用です。代わりに<p>
タグ(または<span>
や<div>
)を使用すればいいのです。
あと2つのコード片は末尾にあります。datasource
属性を含んでいるタグが2つあるので、前述のデータアイランドコンテンツがメインページコンテンツの2か所に組み込まれることになります。datafield
属性は、組み込むべきデータを指定します。datasource
とdatafield
はどちらも私が勝手に作ったカスタム属性で、HTML仕様にもMicrosoftのデータアイランド仕様にもありませんが、HTML 4.01 Strictには従っています。
こんなわずかなマークアップでも、Microsoftによるソリューションの限界を超えています。ここでは、配列不定のリストと、テーブルに埋め込まれたエレメントにデータを組み込んでいますが、どちらもMicrosoftソリューションでは実現困難または実現不可能です。
このHTMLコンテンツは、それ自体ではほとんど何もしません。データ操作のためのスクリプトを追加しなければなりません。
スクリプトを計画する
データバインディングを実装するには、DHTMLの手法を採用し、ドキュメントの内容を操作するスクリプトを使用するとよいでしょう。ただ、スパゲッティのように複雑怪奇なDHTMLの時代は去り、いまは確固たる標準の時代です。DHTMLコードも、すっきりわかりやすいものでなければなりません。最低限、すべてのコードがJavaScriptオブジェクトとしてカプセル化され、他のコードからきれいに分離されていることが必要です。次に示すのはデータバインディングオブジェクトのスケルトンです。「islands.js」ファイルの中身は、ほぼこれですべてです。
var islands = { isMSIE : ..., getElementsByAttribute : function (node, att) { ...}, getEBArecursive : function (list, node, att) { ...}, getFieldDataFromRecord : function (rec,field) { ... }, makeIEtree : function (island) { ... }, merge : function (target, template, record) { ... }, bind : function () { ... } };
これは、7つのプロパティから成るリテラルオブジェクトです(Perlのハッシュに似ています)。具体的には、isMSIE
という1つの単純値と、6個の匿名関数が含まれています。それぞれの関数は1個のプロパティに割り当てられていて、各プロパティが1つのオブジェクトメソッドとして動作します。このように定義された匿名関数は、定義場所であるオブジェクトの中にしか現れないので、そのオブジェクトをどのWebページに組み込んでも、他者の関数名と衝突する心配がなく、便利に使用できます。ただし、アイランドの変数名だけは一意でなければなりません。
省略記号(...)の部分をコードで置き換えれば、このオブジェクトは完成します。その前に、各プロパティについて少し説明しておきましょう。
isMSIE
単なるブラウザ検査フラグです。getElementsByAttribute()
特定の属性を持つノードをドキュメントから探し出します。標準のDOMにはそのような関数がないので、新しく作成しました。getEBArecursive()
getElementsByAttribute()
の再帰バージョンです。コード量を減らすのに役立ちます。getFieldDataFromRecord()
データアイランドからdatafield
項目を取り出します。makeIEtree()
IEの動作の不具合を修正します。詳しくは後述します。merge()
ページコンテンツ、特殊テンプレート、データアイランドのデータから最終コンテンツを作成します。bind()
最初に呼ばれる関数で、ここからすべてが始まります。
さて、バインディングを発生させなければなりません。それには、「islands.js」ファイルの末尾にonload
イベントハンドラを追加します。
window.onload = function () { islands.bind(); };
この行には、スコープチェーンというJavaScriptの便利な機能が使われています。次の構文の方が簡単ですが、こちらの構文は使わないのが無難でしょう。
window.onload = islands.bind();
この簡単な構文を使う場合、bind()
メソッドが実行されるときの現ウィンドウはwindow
オブジェクトとなります。一方、匿名関数の内部で、bind()
メソッドをそれ自身のオブジェクトから呼ぶと、アイランドのオブジェクトが「スコープ内」に置かれます。これなら、特殊なthis
プロパティを用いることでislands
オブジェクト内の他メソッドを簡単に参照できますし、そのオブジェクトメソッドをislands
変数への参照で汚染することもありません。
オブジェクトメソッドを計画する
バインディングの実装方法は次のとおりです。まず、ブラウザ検査を片づけておきましょう。
isMSIE:(window.navigator.userAgent.search('MSIE')!=-1)
bind()
メソッドがすべての力仕事を担います。計画を立てやすいよう、このメソッドで行う作業のリストを示しておきます。
- すべてのデータソースターゲットを探します。各ターゲットについて次の処理を行います。
- そのターゲットの既存のコンテンツを別テンプレートに取り出します。
- それがIEなら、対応するデータアイランドを修正します。
- 対応するデータアイランド中のレコードを探します。各レコードについて次の処理を行います。
- そのレコードとテンプレートをマージします。
- マージしたテンプレートをページ中にコピーします。
必要とされるメソッドのいくつかはきわめて一般的な性格のもので、Webを探してみればあちこちで見つかります。以下は実装方法の一例です。まず、getElementsByAttribute()
関数から見ていきましょう。これは再帰バージョンを包むラッパーにすぎません。
getElementsByAttribute : function (node, att) { var rv = []; this.getEBArecursive(rv, node, att); return rv; },
DOMノードと属性名が与えられると、そのノードの下(内部)にあるすべての該当DOMノードを配列として返します。それを行うのが次の関数です。
getEBArecursive : function (list, node, att) { for (var i=node.childNodes.length-1; i>=0; i--) { var child = node.childNodes.item(i); if ( child.nodeType == 1 ) { if ( child.getAttribute(att) ) { list.push(child); } this.getEBArecursive(list, child, att); } } },
この再帰バージョンは、見つかったノードのリストと現ノードを自分自身に渡します。そして、エレメントノード(タグ)の子ごとに属性の有無を調べ、見つかればそのノードを記録します。見つからなければ、その子に関して再度自分自身を呼びます。こうして、探す対象が何もなくなると、for
ループは何も行わず、リストの完成とともに再帰が終了します。
もう1つ、やはり一般的な性格を持つ関数にgetFieldDataFromRecord()
があります。これはノードとタグ名が与えられると、そのノードのサブツリーを降りていって、与えられた名前を持つタグのコンテンツを取り出してきます。
getFieldDataFromRecord : function (rec,field) { var found; for (var i=0; i < rec.childNodes.length; i++) { if (rec.childNodes.item(i).nodeName. toLowerCase()==field) { found = rec.childNodes.item(i).firstChild; if (found == null ) return ""; else return found.nodeValue; } } },
この関数は、レコードが3層の階層に配列されていることを前提しています。3層とは、「レコードのタグ」「データ項目のタグ」「データ項目の開始タグと終了タグの間に置かれているデータ」です。
以上の準備を終えると、いよいよ核心部分です。
バインディングを実装する
少し先取りをして、最終的なバインディングロジックをお見せします。これがbind()
関数です。
bind : function () { // 1. Find all the datasources in the page. var targets = this.getElementsByAttribute( document,'datasource'); if (!targets || targets.length == 0) return; // Do it for each data binding 'target' in // the page for (var i=0; i < targets.length; i++) { var iid = targets[i].getAttribute('datasource'); var island = document.getElementById(iid); if (!island) return; // 2. Extract a copy of the current content // for this target var template = targets[i].cloneNode(true); // ... and delete the real copy for (var j = targets[i].childNodes.length-1; j>=0; j--) { targets[i].removeChild( targets[i].childNodes.item(j)); } if ( this.isMSIE ) { island = this.makeIEtree(island); } // 4. Apply the template once for each // XML record for (j = 0; j < island.childNodes.length; j++) { // children that are text nodes aren't // real records var record = island.childNodes.item(j); if ( record.nodeName == '#text' ) continue; // 5, 6. Combine page, template and data this.merge(targets[i], template, record); } if ( this.isMSIE ) { delete island; } } }
データソースごとに外部ループがあり、レコードごとに内部ループがあります。datasource
タグが見つかったら、その中身を入れ替えます。具体的な手順としては、ページから当該コンテンツを取り出し、処理してから戻します。最終コンテンツは、テンプレートで指し示されたDOMサブツリーとアイランドを組み合わせたものとなります。データソースごとに複数のレコードがありえるので、その場合は、数回の戻しが必要となります。
ここで問題となるのが、標準の観点からは眉をひそめたくなるIEの動作です。データアイランドを取り出すとき、IEはノードを返してきますが、ここで返されるのは開始タグ、テキスト、終了タグを平坦に並べただけのリストであり、ノードの下のサブツリーはたどれません。そこで、makeIEtree()
関数を使い、あるべきDOMツリーをそのリストから作成します。ステップ4~6の前後で、DOMツリーを作成、破棄していることに注目してください。makeIEtree()
関数のコードは次のとおりです。
makeIEtree : function (island) { var subtree = document.createElement(island.nodeName); var current = subtree; var next; for (var j = 0; j < island.childNodes.length; j++) { var record = island.childNodes.item(j); if ( record.nodeName == '#text' ) { current.appendChild( document.createTextNode(record.nodeValue)); } else if ( record.nodeName.charAt(0) == '/' ) { current = current.parentNode; } else { next = document.createElement( record.nodeName); current.appendChild(next); current = next; } } return subtree; },
この関数は、IEが返してくる平坦リストを単純なループで処理します。subtree
変数が、作成されるDOMツリーの最上部です。current
は、そのサブツリーにおいて現在作成中の部分を指し示し、next
はノード作成に一時的に使用される変数です。コードは新しいタグを見つけるたびにノードを1つ作成し、current
がそのノードを下っていきます。終了タグを見つけると、current
は1ノード引き返します(上へ戻ります)が、それ以外は、見つかったものを現ノードの子ノード群に追加していきます。
これでIEがらみの問題は解決されました。残るは、見つかった情報の処理です。この処理はmerge()
関数で行います。
merge : function (target, template, record) { // dig out the fields requiring update var fields = this.getElementsByAttribute(template,'datafield'); if (!fields || fields.length == 0) next; // update text for target fields in the template for (var k=fields.length-1; k>=0; k--) { var thetag = fields[k]; var thefield = thetag.getAttribute('datafield'); var newtext = this.getFieldDataFromRecord(record,thefield); if (thetag.firstChild) // replace existing text { thetag.firstChild.nodeValue = newtext; } else if ( thetag.value == null ) // not form tag { thetag.appendChild( document.createTextNode(newtext)); } else // a form element { thetag.value = newtext; } } // put the updated content back into the page. for (k=0; k < template.childNodes.length; k++) { target.appendChild( template.childNodes.item(k).cloneNode(true)); } },
merge()
関数は、まず、ターゲット中からdatafield
属性を持つすべてのタグを見つけ出し、それの配列を作ります。次に、タグごとに、与えられたレコードの中から対応するコンテンツを見つけ出します。見つかったコンテンツをテンプレート(基本ページコンテンツ)に入れるとき、3つのケースが考えられます。すなわち、コンテンツタグに既にテキストが含まれている場合(入れ替え)、含まれていない場合(追加)、そしてコンテンツタグがフォームエレメントである場合です。3つ目のケースでは、データをvalue
プロパティに入れなければなりません。最後に、更新されたテンプレートの中身をそっくりページに戻します。これが終われば、あとはページをロードするだけで、すべてが実行されます。ダウンロードサンプル内のページをお気に入りのブラウザで試してみてください。
このDHTMLコードは、従来の心配事の多くを排除しているという点で、ずっとプロ的な仕事と言えます。ページにイベントハンドラを埋め込むこともありません。これでブラウザごとのテストの必要性が減少し、やむを得ない場合にだけテストを行えば済みます。得られる結果も単なる遊びや新しがりでなく、本格的で意義ある成果です。最近、AJAXを始めとするDHTML関連技術の影響が爆発的に広まっています。今後は、Web技術者の実際の仕事でも、クライアントからの要求でも、その比重が高まっていくことになるでしょう。
データアイランドは、わずかな汎用コードを使うだけでクロスブラウザ実装を可能にします。本稿では、データアイランドに基づくコンテンツの表示方法を考え、そのコンテンツをフォーム、テーブル、または非テーブル要素(リストなど)に統合する方法について説明しました。少し努力すれば、洗練されたデータ管理システムに発展させることもできるでしょうし、実際、その方面でいくつかの実験的な仕事がなされてもいます。IE 4.0やNetscape 4.xといった古びたブラウザが姿を消し、MozillaとFirefoxが台頭してきた現在、DHTMLは以前にも増して強力で、汎用性の高い技術に育ちつつあります。IEに拘束されてきたアプリケーションをDHTMLで解き放ってください。