javascript中でのHTML表現。
javascriptで動的なUI等を書くと、どうしてもネックになるのが、HTMLノードの表現、その整理法だ。少なくとも私はそうだ。
pagmo書いていたときなど、コードのかなりの量をHTMLノードの記述に使っている。
しかし本質的には重要なコードではないので、なるべくコンパクトに書きたい。ただ単にテキストで作って、それをinnerHTMLとかで流し込むのは、コードが汚くなるし、ソースの見た目もきたなくなるしいやだ。(短い場合は速度が速いらしいが)
common lispやschemeなどでは、S式で表されるリストをつかってデータ構造とし、それをHTMLに変換することをよくするようだ。
たとえば以下のようにする。
(defun standard-page (&key title body) (let ((pbody (cond ((atom body) (list body)) ((atom (car body)) (list body)) (t body)))) `(html (:xmlns "http://www.w3.org/1999/xhtml") (head () (title () ,(str+ "Canvas wiki - " title)) (meta (:http-equiv "content-type" :content "text/html; charset=UTF-8")) (meta (:name "description" :content "Canvas Common Lisp wiki")) (meta (:name "keywords" :content "common lisp,wiki,canvas,nagayoru,smihica,lisp")) (link (:rel "shortcut icon" :href "favicon.ico")) (link (:rel "icon" :href "favicon.ico")) (link (:href "css/main.css" :rel "stylesheet" :type "text/css" :media "all"))) (body () (div (:id "main") (h1 () ,title) ,(get-recently-list-page) (div (:id "content" :class "container") ,@pbody))))))
これは、この前書いていたwikiの一部だが、何がよいかというと、データ構造がテキストでないため、リスト操作として、HTMLノードを自由にいじくれる点である。そのようにできると、テンプレートをつくり、組み合わせながら構築することができるし、関数の組み合わせでいろいろなことができる。
たとえば、ソース中の(get-recently-list-page)は、最近の更新をHTMLリストにして返す関数である。
このような方法を他言語で実現している例もあって、
http://jp.rubyist.net/magazine/?0012-qwikWeb#l8
↑のページの「HTML を配列として扱う」の項に江渡浩一郎氏のqwikWebでのHTMLタグの表現方法が書いてある。
たとえば以下のようなコードとして表現する。
require 'qwik/wabisabi-format-xml' html = [:html, [:head, [:title, 'hello']], [:body, [:h1, 'hello, world!'], [:p, 'This is a ', [:a, {:href=>'hello.html'}, 'hello, world'], ' example.']]] puts html.format_xml
で、以上のような方法をjsではどう表現できるか考えた。
たとえば、こんな風に書けるといいだろう。
js>convert_to_html_node(["div",{id:"outer",style:{width:100,height:100,color:"#FFFFFF"}}, ["a",{href:"http://google.com"},"google"], ["span",{},["br"],["br"],"aho"]]); => htmlDivElement
↑の式はノードが帰ってくるものだが、
js>convert_to_html_text(["div",{id:"outer",style:{width:100,height:100,color:"#FFFFFF"}}, ["a",{href:"http://google.com"},"google"], ["span",{},["br"],["br"],"aho"]]); => <div id="outer" style="width:100px;height:100px;color:#FFFFFF;"><a href="http://google.com">google</a><span><br/><br/>aho</span></div>
↑のように、文字列が帰ってくる関数もほしいだろう。
そんなこんなで以下のような関数を書いた。
まずは基本関数
Array.prototype._map = function(callback,optional){ var this_object = this; if(optional && optional.this_object) this_object = optional.this_object; for(var i=0,res=[],len=this.length;i<len;i++) res[i] = callback.call(this_object,this[i],i,this); return res; }; var add_event = function(context,func,type){ if (window.addEventListener) context.addEventListener(type.replace(/^on/,""), func, false); else if (window.attachEvent) context.attachEvent(type, func); }; var appendChilds = function(elem,childArr){ for(var i=0,l=childArr.length;i<l;i++) elem.appendChild(childArr[i]); return elem; };
mapはmap機能を実現する関数で、あとは名前でわかるだろう。
次にノードを返すバージョン。
var number_styles = ["top","left","right","bottom","width","height","margin","padding","size"]; var styles_reg_exp = new RegExp(number_styles.join("|"),"i"); var add_style = function(elem,data){ for(var i in data){ if(i.match(styles_reg_exp) && typeof data[i] == 'number') data[i] = data[i].toString() + 'px'; else data[i] = data[i].toString(); elem.style[i] = data[i]; } return elem; }; var add_attr = function(elem,data){ for(var i in data){ if(typeof data[i] == 'function'){ add_event(elem,data[i],i); } else { if(i == 'style') add_style(elem,data[i]); else elem[i] = data[i].toString(); } } return elem; }; var convert_to_html_node = function(arr){ if(arr instanceof Array) { if(arr.length == 0) return null; var elem = document.createElement(arr[0]); if(arr.length == 1) return elem; add_attr(elem,arr[1]); arr.shift(); arr.shift(); if (arr.length == 0) return elem; return appendChilds( elem , arr._map(convert_to_html_node)); } else return document.createTextNode(arr); };
これでできた。
convert_to_html_node(["div",{id:"outer",style:{width:100,height:100,color:"#FFFFFF"}}, ["a",{href:"http://google.com"},"google"], ["span",{},["br"],["br"],"aho"]]); => htmlDivElement
firebugで確認してみたが、中身までうまくできている。
ちなみに、ノードを返すバージョンではonmouseoverなどに関数をそのまま書き込める。
var xx = function(){alert("mouseover!");}; convert_to_html_node(["div",{id:"outer", onmouseover:function(){xx()}; },"on mouseover do xx."]);
後述の文字列を返すバージョンでは、関数だった場合値は捨てられる。
文字列を返すバージョン
var empty_elements = ["area","base","br","col","hr","img","input","link","meta","param"]; var elems_reg_exp = new RegExp(empty_elements.join("|"),"i"); var tag_to_str = function(tag_name,attr){ return function(cont){ return ((tag_name.match(elems_reg_exp))? ["<",tag_name,attr_to_str(attr),"/>"]: ["<",tag_name,attr_to_str(attr),">",cont,"</",tag_name,">"]).join(""); } }; var style_to_str = function(style){ var ret = ""; for(var i in style) ret = ret + i + ":" + style[i].toString() + (((i.match(styles_reg_exp) && typeof style[i] == 'number'))?"px;":";"); return ret; }; var attr_to_str = function(attr){ var ret = ""; for(var i in attr) if(typeof attr[i] != 'function') ret = ret + " " + i + "=\"" + ((i == 'style')? style_to_str(attr[i]): attr[i].toString()) + "\""; return ret; }; var convert_to_html_text = function(arr){ if(arr instanceof Array) { if(0==arr.length) return ""; var f = tag_to_str(arr[0],((arr.length==1)?{}:arr[1])); arr.shift(); arr.shift(); if(0==arr.length) return f(""); return f(arr._map(convert_to_html_text).join("")); } else return arr; };
以上でできる。
convert_to_html_text(["div",{id:"outer",style:{width:100,height:100,color:"#FFFFFF"}}, ["a",{href:"http://google.com"},"google"], ["span",{},["br"],["br"],"aho"]]); => <div id="outer" style="width:100px;height:100px;color:#FFFFFF;"><a href="http://google.com">google</a><span><br/><br/>aho</span></div>
JavaScript-C 1.6 2006-11-19で動作確認した。
このようにすると、テンプレートがつくれて、動的生成のHTMLオブジェクトが作りやすくなった。また、配列データとなるので、よりコードがきれいになる。データを配列に格納して、それを操作してHTMLノード用の配列に変換し、それをさらにHTMLノードに変換し、書き込むといった操作ができるようになり、タグを直で書く必要がほとんどなくなる。