続Element.Methods。問題とちょっと改良

以前のエントリーでprototype.js1.5のElement.Methodsについて書きました。 前回はいいところをプッシュして書いたんですが、皆さんご存じの通り問題児でもあります。

Element.Methodsの問題点

  • elementを拡張するかしないかが選択できない。
  • プロトタイプチェーンがつながっていない。
  • document.createElementが考慮されていない。

■elementを拡張するかしないかが選択できない。

まずわかりやすくこれから。elementは取得された時に拡張されるわけですが、当然パフォーマンスに影響があります。使いたくない人もいるでしょう。しかし、Element.extendによる拡張は$にも$$にも組み込まれていて選択できません。メソッドを置き換えて回避は可能ですが、組み込まれている、ということを考慮すると当然extendされている、ということを前提としたライブラリも出てくるでしょう。となった場合にやっかいです。別名のメソッドを定義してもいいですが・・・やっぱり$って名前がいいよね!(何

■プロトタイプチェーンがつながっていない。

これは当然の問題。Element.extendは文字通りelementにElement.Methodsのメソッドをコピーしているだけ。しかも一回取得すると、_extendedというフラグが立って次回からはもうextendされない(パフォーマンスを考慮してのこと)。つまり Element.Methodsへのメソッド追加が反映されない、メソッドの内容変更も反映されない。  つまり、恐らく$か$$されるだろう、window.onloadイベント前にElement.Methodsへのメソッド追加を完了させ、それ以降変化させない、という事に注意しないといけない。

ではなぜプロトタイプチェーンがつながっていないのだろうか。Elementは拡張できないんだろうか。これはYesでありNoです。 まず、このElement.Methodsによる害をある程度押さえ込むことのできるソースを示します。 これはFirefoxなどMozilla系、safari、Operaで有効です。

javascript code
  1. (function() {
  2. if(!window.HTMLElement) window.HTMLElement = document.createElement("xxx").constructor;
  3. if(window.HTMLElement) {
  4.     var methods = Element.Methods, property;
  5.     var bind = function(name, method) {
  6.       HTMLElement.prototype[name] = function(){
  7.         Array.prototype.unshift.call(arguments, this);
  8.         return method.apply(null, arguments) || this;
  9.       }
  10.     }
  11.     for (property in methods) {
  12.       var value = methods[property];
  13.       if (typeof value == 'function') bind(property, value);
  14.     }
  15.     HTMLElement.prototype._extended = true;
  16. }
  17.  
  18. })();
  19.  

このコードを最後に(window.onloadの直前で)実行すればOKです(scriptaculousとかもElement.Methodsを拡張するので、その拡張が終わった後)。 これでelementがプロトタイプチェーンでつながっているブラウザでは$による負荷を軽減できます。$("test").show()とかも実行できますし、引数操作によるパフォーマンス低下がいやならElement.show("test")と実行すれば良いだけです。しかも地味にreturn method.apply(null, arguments) || this;とすることによって$("test").update("hogehoge").show()とか書けるようにもしてみました(笑

しかしIEはどうしようもありません。そもそもIEはelementがプロトタイプチェーンをもっていません。 document.createElement ("div").constructorとやってもprototypeとやってもundefinedと抜かす強敵です。これは昔から対策が考えられていますが、つまるところElement.Methodsと同様にコピーするだけです。それをhtcをつかってみたり、コンストラクタを擬似的に作ってみたり、とかっていう方法で実装しているだけです。なのでElement prototypingしようと思うとIEでは現状これが限界です。

■ document.createElementが考慮されていない。

先ほど示したソースでelementがプロトタイプチェーンをもつブラウザでは当然document.createElementで作成した elementもElement.Methodsのメソッドが実行できます。しかしIEではそれができません。この document.createElementも考慮して先ほどのElement.Methods対策ソースを改良しましょう。

javascript code
  1. (function() {
  2. if(!window.HTMLElement) window.HTMLElement = document.createElement("xxx").constructor;
  3. if(window.HTMLElement) {
  4.     var methods = Element.Methods, property;
  5.     var bind = function(name, method) {
  6.       HTMLElement.prototype[name] = function(){
  7.         Array.prototype.unshift.call(arguments, this);
  8.         return method.apply(null, arguments) || this;
  9.       }
  10.     }
  11.     for (property in methods) {
  12.       var value = methods[property];
  13.       if (typeof value == 'function') bind(property, value);
  14.     }
  15.     HTMLElement.prototype._extended = true;
  16. }else {
  17.   document._createElement = document.createElement;
  18.   document.createElement = function(tag){
  19.     return Element.extend(document._createElement(tag));
  20.   }
  21. }
  22.  
  23. })();
  24.  

赤色で示した部分が追加部分です。これでIEでもdocument.createElementした後に直接Element.Methodsのメソッドが実行できます。何度も言いますがパフォーマンス低下がいやな人はこの追加部分を省いてください。

というわけで今回はElement.Methodsの問題への対応策を示してみました。要はIEがelementがプロトタイプチェーンを持つようにしてくれれば万事解決なわけなんですよね。でも結局IE7でも実装されていないようで、なかなか・・・

では就職活動にいってきます・・・

07.27.08/12am

prototype.js 1.5のElement.Methods

prototype.js1.5ではみんながこうしたいなーと思っていた機能が実装されました。(via Encytemedia) (SVN co http://dev.rubyonrails.org/svn/rails/spinoffs/prototype して rake distしたもの )

■Element.Methods

1.4まではelementに対する操作は

javascript code
  1. Element.show($("test"));
  2. Element.update($("test"), "hoge");
  3.  

のように書く必要がありました。

これが

javascript code
  1. $("test").show();
  2. $("test").update("hoge");
  3.  

と書けるようになりました。

これはprototype.jsで最も偉大な$メソッドの変更によるモノです。

javascript code
  1. function $() {
  2.   var results = [], element;
  3.   for (var i = 0; i < arguments.length; i++) {
  4.     element = arguments[i];
  5.     if (typeof element == 'string')
  6.       element = document.getElementById(element);
  7.     results.push(Element.extend(element)); //<= ココ!
  8.   }
  9.   return results.length < 2 ? results[0] : results;
  10. }
  11.  

つまり取得したelementに対してElement.extendが適応されるようになった、と。 んでそのElement.extendは

javascript code
  1. Element.extend = function(element) {
  2.   if (!element) return;
  3.  
  4.   if (!element._extended && element.tagName && element != window) {
  5.     var methods = Element.Methods;
  6.     for (property in methods) {
  7.       var value = methods[property];
  8.       if (typeof value == 'function')
  9.         element[property] = value.bind(null, element);
  10.     }
  11.   }
  12.   element._extended = true;
  13.   return element;
  14. }
  15.  

Element.Methodsのメソッドがelementに対して第1引数をそのelementに束縛した状態でセットされます。 つまりElement.Methodsにメソッドを追加すれば自動的に$で取得したelementに対してそのメソッドが追加されます。

ちなみにscriptaculousでも

javascript code
  1. if(Element.Methods) {
  2.   Element.Methods.visualEffect = function(element, effect, options) {
  3.     s = effect.gsub(/_/, '-').camelize();
  4.     effect_class = s.charAt(0).toUpperCase() + s.substring(1);
  5.     new Effect[effect_class](element, options);
  6.     return $(element);
  7.   }
  8. }
  9.  

というコードが追加されています。 クラス名変換のオーバーヘッドはあるものの、$("test").visualEffect('scale')と書けるようになりました。

IEで$$が動くようになっていたりと1.5のリリースが待ち遠しいですね。

07.27.08/12am

埋め込みjavascriptを実装してみました。

naoyaさんのJemplateの記事に触発されて埋め込みjavascriptを実装してみました。

同様の実装としてAjax Pagesがあるのですが、よりシンプルに実装してみました。コードにして50行。

サンプルファイルダウンロード

これは今作っているフレームワークの一部です。シンプルですがわりと使えるかと。 実際の動作はサンプルファイルを見ていただくとして

lyase_view.jsの説明どおり

javascript code
  1. /*
  2. * using innerHTML
  3. * -------------
  4. * //in HTML
  5. * <textarea id="template" style="display:none">
  6. * The value of x is:< %= context.x%>
  7. * </textarea>
  8. * //code
  9. * document.write(Lyase.View.render({element:"template"}, {x : 10}));
  10. *
  11. * using a template file
  12. * -------------
  13. * //in /template.jhtml
  14. * The value of x is:< %= context.x%>
  15. * //code
  16. * document.write(Lyase.View.render({file:"/template.jhtml"}, {x : 10}));
  17. *
  18. * Of course, you can embed more complex codes.
  19. * //in HTML, with prototype.js
  20. * <textarea id="template" style="display:none">
  21. * < % context.list.each(function(pair){%>
  22. * The value of < %= pair.name %> is: < %= pair.value%>
  23. * < % }) %>
  24. * </textarea>
  25. * //code
  26. * document.write(Lyase.View.render({element:"template"},
  27. * {list :[{name : "x", value : 10}, {name : "y", value : 20}]}));
  28. */
  29.  

こんな感じでjavascriptのコードが読み込めます。

また

html code
  1. <h1>< %= context.title %></h1>
  2.  この文章はLyase.Viewを使用して< %= context.fileName %>の内容を表示しています。
  3.  
  4.  
  5. 以下のようにクロージャも問題なく使用できます。
  6.  
  7. < % var c = "," %>
  8. < % context.list.each(function(num){ %>
  9.   < %= num+c %>
  10.  
  11. < % })%>
  12.  
  13. < %= render({file :"./sample2.jhtml"}, context)%> <!--ココに注目--!>
  14.  

このようにテンプレート内で別のテンプレートを呼び出すことも可能です。

テンプレートには文字列、textareaなどのinnerHTML、テンプレートファイルが使用できます。 prototype.jsのAjax.Updaterあたりをハックすれば結構使えるものになるんじゃないかなあ、と思ってみたり。

・2006年2月16日追記 id:brazilさんもjavascriptのテンプレートについて書かれてますね。 実は僕もHTML生成関数は作成しています。DOM版とHTMLタグ生成版で。 今回のライブラリのようなアプローチとどっちがいいか、となるともう少し検討が必要かな・・・

07.27.08/12am

About

Author:yuin(http://inforno.net/)

文学部文化学科卒という生粋の文系趣味プログラマ。

主にRuby、Javascript、PHP、JAVA,Python,C,Scala,Schemeなどを使っています。今はPythonな感じかもしれない。今後作曲活動なども復活するかもしれない。

Pages