Yaks

[javascript,prototype]innerHTMLに +=(文字列結合)するのはよろしくないみたい はてなブックマーク - [javascript,prototype]innerHTMLに +=(文字列結合)するのはよろしくないみたい

まさかの連続開発ネタ。(一回、間があきましたが)

なんか prototype.jsElement.extend()関数が IEでうまく動かないと思ったら、意外な事実がわかりました。

prototype.js 1.5から$()が拡張され、要素に ElementForm.Elementのメソッドが追加されるようになりました。

いままで、

var elem = $('hoge');
Element.toggle(elem);

と、Elementクラスを介して行っていた処理を

var elem = $('hoge');
elem.toggle();

と、その要素自身のメソッドとして扱う事ができるようになり、より直感的にコードを書けるようになりました。(HTMLの DOM要素を直接拡張してるのがちょっとビミョウなんですが...)

ところが、時々 IEでこの拡張が効かなくなる症状に遭遇しました。 Script DebuggerIE Developer Toolbarで調べてみると、_extendプロパティ(拡張済み要素に付与される)が trueにも関わらず、メソッドがコピーされていないようでした。

で、色々試したのですが埒があかず、かといって 元のソースをいじるのもイヤだったので、prototypeの(正確には Railsの) Trac を覗いてみると、同じような症状のチケットが見つかりました。

Element.extend doesn't seem to work in IE sometimes

この中のやりとりを追っていくと、「これは不具合ではなく IEのバグだ」と。 具体的には、IEで innerHTML に += で 内容を追加すると、その要素の中身(子要素)が上書きされてしまう(プロパティだけは引継がれ、メソッドは引き継がれない)のだそうです。(も、もしや常識?) 言われてみると、なんか前にどこかでそんな情報を目にした気がしないでもないですが...

とりあえずテストを書いてみました。

HTML

<div id="root"><div id="child">hoge</div></div>

テストコード

new Test.Unit.Runner( {
	testInnerHTML: function(){ with( this ){
		var child = $('child');

		assertEqual( 'div', child.tagName.toLowerCase() );
		assertEqual( 'child', child.id );
		assert( child._extended );
		assertEqual( 'function', typeof child.addClassName, 'まえ' ); 
		assertEqual( 'hoge', child.innerHTML );

		$('root').innerHTML += '<div>hoge</div>';

		var childAfter = $('child' );
		assertEqual( 'div', childAfter.tagName.toLowerCase() );
		assertEqual( 'child', childAfter.id );
		assert( childAfter._extended );
		assertEqual( 'function', typeof childAfter.addClassName, 'あと' ); 
		assertEqual( child, childAfter ); 
	} }
} );

結果

IE6
result_ie.png
Firefox 1.5.0.10
result_ff.png

エラーメッセージに「あと」が表示されているので、+=後に取得した childAfterの addClassName()関数が undefinedになっているのがわかります。でも、_extendedはパスしているので、trueになっているのがわかります。というわけで、冒頭に書いたような状態に見事に陥っています。assertEqualを見ると、childeAfterがなんか [Object]とかになってます...

何気に Firefoxでも failedがあるのですが、どうも _extendedプロパティがセットされない(とれてない?)様子。(これもなんかドキュメントに書いてた気がしますが) あと、childと childAfterの assertEqualもこけているので、実際 Firefoxでも内部的には別のオブジェクトになっているようです。

(実際のテストはこちら)

で、解決策としては、DOM操作関数を使うか、Insertion.Afterを使うようにとのことでした。 正直、Insertionクラスの使いどころがいまいち見出せなかったのですが、こんな形で役立つのですね。これもまた新たな発見でした。

というわけで、prototype.jsを使う際には、この不具合は頭にとどめておくとよいかもしれません。

であ、また。