Ajaxでエクスプローラみたいなツリーを作ってみる

さて、本日はRogueをお休みします。その代わり、Ajaxなるものを書いてみることにしました。聞いた話によりますと、Ajaxがあれば何でもできるそうで、私の「新世界の神になる」という野望もかなえられるかと期待しております。

最近は、Ajaxを使うといっても、ライブラリを経由して使うというものなのだそうですが、まあ、基礎からやっておくということにいたします。

Windowsエクスプローラみたいなのを作る

とりあえず、やったことがない私でも一日でできそうな課題を選ぶことにいたしました。Windowsエクスプローラみたいなのを作ります。

あれですね。ツリーが開いていくやつ。

  • [book]我輩は猫である
    • [chapter]猫登場
      • [paragraph]我輩は猫である
      • [paragraph]名前は三毛だ
      • [paragraph]いびき先生の家にすんでいる
    • [chapter]猫、ビールで死ぬ
  • [book]ドラえもん
    • [chapter]さようならドラえもん
      • [paragraph]未来へ帰る?
      • [paragraph]けんかならドラえもん抜きで
      • [paragraph]いてて俺の負けだ
      • [paragraph]安心して帰れるだろ?
      • [paragraph](そして朝)

こういう形をトグルボタンで開いていく(閉じられる)ことにします。で、開くときに動的にAjaxでデータを取得することにいたしましょう。

XMLHTTPRequest

何のことやら全く分かりませんが、XMLHttpRequsetというものを使うのだそうです。ということで、とりあえずパクリで一つの関数を書いておきました。

function createAjaxRequest(){
   if(window.ActiveXObject){
      try{
         return new ActiveXObject("Msxml2.XMLHTTP");
      }catch(e1){
         try{
            return new ActiveXObject("Microsoft.XMLHTTP");
         }catch(e2){
            return null;
         }
      }
   }else{
      // Firefox/Mozilla/Opera/Safari
      return new XMLHttpRequest();
   }
}

Nodeクラス

NodeをJavaScriptのクラスとして持つことにしましょう。

  • テンプレート名(book, chapterなど)
  • ID(テンプレートごとに一意)
  • テキスト(説明文、「ドラえもん」など)

という要素を持つといたします。

var g_nodes = new Object(); // グローバルにノードを連想配列で持つ

function Node(template, id, text){
   this.id = id;
   this.template = template;
   this.text = text;
   this.open = false;

   this.getHTMLID = function(){
      return this.template + '_' + this.id;
   }

   this.append = function(e){
      var button = document.createElement('a');
      button.setAttribute('href',
                          "javascript:node_click('" + this.template +"', '" + this.id + "');void(0);");
      button.className = 'button';
      button.appendChild(document.createTextNode('+'));
      e.appendChild(button);
      this.button = button;
      e.appendChild(document.createTextNode("[" + this.template + "]"));
      var link = document.createElement('a');
      link.setAttribute('href', 'show_data.php?template=' + this.template + '&id=' + this.id); // これは適当なリンク。こういうのがあると仮定している
      link.setAttribute('target', '_blank');
      link.appendChild(document.createTextNode(this.text));
      e.appendChild(link);
      var div = document.createElement('ul');
      div.className = 'nodes';
      div.setAttribute('id', this.getHTMLID());
      e.appendChild(div);
   }

   this.toggleTree = function(){
      if(this.open){
         this.closeTree();
      }else{
         this.openTree();
      }
   }

   this.openTree = function(){
      this.children = getChildren(); // 子ノードの配列を返す関数
      list = document.getElementById(this.getHTMLID());
      text = "";
      for(i in this.children){
         l_item = document.createElement('li');
         l_item.className = 'node';
         this.children[i].append(l_item);
         list.appendChild(l_item);
      }
      this.open = true;
      if(this.button){
         this.button.innerHTML = '-';
      }
   }
   this.closeTree = function(){
      e = document.getElementById(this.getHTMLID());
      e.innerHTML = '';
      this.open = false;
      this.button.innerHTML = '+';
   }
   g_nodes[this.getHTMLID()] = this; // グローバル変数にいれる
}
function node_click(template, id){
   var node = g_nodes[template + '_' + id];
   node.toggleTree();
}

こんな感じですね。CSSはあとでこれに合わせて定義することにします。

ここで実装していないのはgetChildren()ですが、これはAjaxを使ってサーバからとってくることにいたしましょう。

  1. 最初に一つだけnew Node()しておく。
  2. node_click()でそのノードのテンプレート名、IDを指定してtoggleTree()を呼び出す
  3. getChildren()で子供ノードの配列を取得
  4. そのノードをHTML化してDOMでくっつける
  5. それらのノードのHTMLにはリンクが貼られており、クリックするとそのノードに対してtoggleTree()できるようになっている

という設計です。

HTML/CSSを合わせて定義

<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Ajax Test</title>
</head>
<script type="text/javascript" src="tree.js">
</script>
<script type="text/javascript">
var id_num = 0;
var top;
function init_tree(){
	top = new Node('top', 1, ''); 
	top.toggleTree();
}
</script>
<style type="text/css">
ul.nodes{
	margin-left:5px;
	padding-left:5px;
}
li.node{
	margin-left:5px;
	list-style-type:none;
}
a.button{
	border-style:solid;
	border-width:1px;
	border-color:#888;
	color:#222;
	background:#ffa;
	font-family: 'courier';
	font-size:10pt;
	text-decoration:none;
	padding-left:2px;
	padding-right:2px;
	margin-right:8px;
}
</style>
</head>
<body bgcolor="#ffffff" leftmargin="0" topmargin="0" marginwidth="0"
marginheight="0" onLoad='init_tree();'>
<ul id='top_1' class='nodes'></ul>
</body>
</html>

こんな感じにしてみました。JavaScriptはtree.jsに入れておきます。

Ajaxでデータを取得してくる時はXMLがよいらしい

Ajaxで取得してくるデータはXMLがよいそうです。で、ここではデータベースとか用意していないので、適当にXMLデータを返すPHPだけを用意することにしました。本来はデータベースにつないできちんと作れることでしょう。

XMLのフォーマット定義

<nodes>
 <node template='paragraph' my_id='1'>我輩は猫である</node>
 <node template='paragraph' my_id='2'>名前は三毛だ</node>
 <node template='paragraph' my_id='3'>いびき先生の家にすんでいる</node>
</nodes>

適当に返すためのPHP

<?php
header('Content-type: text/xml; charset=Shift_JIS');
switch($_GET['template']){
case "top":
     print "<nodes>";
     print "<node template='book' my_id='1'>我輩は猫である</node>";
     print "<node template='book' my_id='2'>ドラえもん</node>";
     print "</nodes>";
     break;
case "book":
     switch($_GET['id']){
     case 1:
        print "<nodes>";
        print "<node template='chapter' my_id='1'>猫登場</node>";
        print "<node template='chapter' my_id='2'>猫、ビールで死ぬ</node>";
        print "</nodes>";
        break;
     case 2:
        print "<nodes>";
        print "<node template='chapter' my_id='3'>さようならドラえもん</node>";
        print "</nodes>";
        break;
     }
     break;
case "chapter":
     switch($_GET['id']){
     case 1:
        print "<nodes>";
        print "<node template='paragraph' my_id='1'>我輩は猫である</node>";
        print "<node template='paragraph' my_id='2'>名前は三毛だ</node>";
        print "<node template='paragraph' my_id='3'>いびき先生の家にすんでいる</node>";
        print "</nodes>";
        break;
     case 2:
        print "<nodes>";
        print "<node template='paragraph' my_id='4'>ビールは苦い</node>";
        print "<node template='paragraph' my_id='5'>ぶくぶくぶく。溺れた。</node>";
        print "</nodes>";
        break;
     case 3:
        print "<nodes>";
        print "<node template='paragraph' my_id='6'>未来へ帰る?</node>";
        print "<node template='paragraph' my_id='7'>けんかならドラえもん抜きで</node>";
        print "<node template='paragraph' my_id='8'>いてて俺の負けだ</node>";
        print "<node template='paragraph' my_id='9'>安心して帰れるだろ?</node>";
        print "<node template='paragraph' my_id='10'>(そして朝)</node>";
        print "</nodes>";
        break;
     }
case "paragraph":
}
?>

これはひどい…。という感じですが、あくまでテストです。get_children.phpに保存しました。

get_children.php?template=book&id=1 で

<nodes>
 <node template='chapter' my_id='1'>猫登場</node>
 <node template='chapter' my_id='2'>猫、ビールで死ぬ</node>
</nodes>

というXMLが帰ってきます。

XMLオブジェクトがJavaScriptで取得できるそうな

JavaScriptでは、XMLオブジェクトが取得でき、そのあつかいはDOMと同じだそうで。ということは、次のようなJavaScriptを返せば、XMLがNode配列に変換されます。

function parseXML(xml){
   var nodes_xml = xml.getElementsByTagName("node");
   var nodes = new Array();
   for(i = 0; i < nodes_xml.length; i++){
      var child = nodes_xml[i];
      var text = child.firstChild.nodeValue;
      var id = child.getAttribute('my_id');
      var template = child.getAttribute('template');
      nodes[i] = new Node(template, id, text);
   }
   return nodes;
}

ちょっとずるしてますが、まあよいでしょう。

Ajaxで実際にとってくる

噂によると、Ajaxのオブジェクトに対してopen(メソッド名, URL, true)でよいそうです。で、とってきたときに実行する関数を指定するのだそうです。

というわけで、少し悩んだ揚げ句に、Nodeクラスを次のように書いてみました。

function Node(template, id, text){
   this.id = id;
   this.template = template;
   this.text = text;
   this.open = false;

   this.toggleTree = function(){
      if(this.open){
         this.closeTree();
      }else{
         var req = createAjaxRequest();
         req.open('get',
                  './get_children.php?id=' + this.id + '&template=' + this.template, true);
         var node = this;
         req.onreadystatechange = function(){
            if(req.readyState == 4){
               node.openTree(req);
            }
         }
         req.send('');
      }
   }

   this.getHTMLID = function(){
      return this.template + '_' + this.id;
   }

   this.append = function(e){
      var button = document.createElement('a');
      button.setAttribute('href',
                          "javascript:node_click('" + this.template +"', '" + this.id + "');void(0);");
      button.className = 'button';
      button.appendChild(document.createTextNode('+'));
      e.appendChild(button);
      this.button = button;
      e.appendChild(document.createTextNode("[" + this.template + "]"));
      var link = document.createElement('a');
      link.setAttribute('href', 'show_data.php?template=' + this.template + '&id=' + this.id);
      link.setAttribute('target', '_blank');
      link.appendChild(document.createTextNode(this.text));
      e.appendChild(link);
      var div = document.createElement('ul');
      div.className = 'nodes';
      div.setAttribute('id', this.getHTMLID());
      e.appendChild(div);
   }

   this.openTree = function(req){
      this.children = parseXML(req.responseXML);
      list = document.getElementById(this.getHTMLID());
      text = "";
      for(i in this.children){
         l_item = document.createElement('li');
         l_item.className = 'node';
         this.children[i].append(l_item);
         list.appendChild(l_item);
      }
      this.open = true;
      if(this.button){
         this.button.innerHTML = '-';
      }
   }
   this.closeTree = function(){
      e = document.getElementById(this.getHTMLID());
      e.innerHTML = '';
      this.open = false;
      this.button.innerHTML = '+';
   }
   g_nodes[this.getHTMLID()] = this;
}

toggleTree()が大きく変わっていますが、ここでAjaxでのリクエストを行っております。

toggleTree()で、現在の開閉の状態を取得し、閉じられていた場合には、Ajaxでリクエストを行い、データが入手されたときに、openTree()を呼び出します。

まあ、こんなところで無事に動きました。

感想としては、「そんなに難しくはないが、JavaScriptを使えない人にはきついかもね」という程度でしょうか。そろそろ寝ましょう。