はじめに
まず、form
要素はネストできません。
http://www.w3.org/TR/html5/forms.html#the-form-element
Content model:
Flow content, but with no form element descendants.
コンテントモデル:
フローコンテントだよ。でも form 要素を子孫に持つことはできないよ。
しかし、世の中にはそんなルールなんてお構いなしで稼働しているサービスもあります。
そのようなサービスを不幸にも修正・改修しなければいけない状況も出てくるでしょう。
この記事では、そんなつらい場面でも冷静に対処できるよう、ネストした form
要素が一体どんな動きをするのかを見てみます。
実験サンプルを用意する
こんなフォームを用意します。
document.forms.foo
の中に 3 つのフォームを入れてみました。
<div id="wtf-form">
<form name="foo">
<input type="hidden" name="wtf" value="foo-wtf">
<form name="bar">
<input type="hidden" name="wtf" value="bar-wtf">
</form>
<form name="buzz">
<input type="hidden" name="wtf" value="buzz-wtf">
</form>
<form name="fuga">
<input type="hidden" name="wtf" value="fuga-wtf">
</form>
</form>
</div>
とりあえず Javascript で調べてみる
さっそく各フォームにアクセスしてみましょう。
ブラウザは Google Chrome v47 を使いました。
typeof document.forms.foo // "object"
typeof document.forms.bar // "undefined"
typeof document.forms.buzz // "object"
typeof document.forms.fuga // "object"
document.forms.bar
の霊圧が ... 消えた ...?
DOM がどうなっているのか innerHTML
を見てみましょう。
document.getElementById('wtf-form').innerHTML
// <form name="foo">
// <input type="hidden" name="wtf" value="foo-wtf">
//
// <input type="hidden" name="wtf" value="bar-wtf">
// </form>
// <form name="buzz">
// <input type="hidden" name="wtf" value="buzz-wtf">
// </form>
// <form name="fuga">
// <input type="hidden" name="wtf" value="fuga-wtf">
// </form>
ネストしていたフォームがバラバラになっています。
よく見ると document.forms.bar
がいなくなり、その中にいた input[name="wtf"]
が document.forms.foo
の中にいます。
この状態のフォーム要素の値は取れるのでしょうか。
document.forms.foo.wtf.value // ""
document.forms.bar.wtf.value // Uncaught TypeError: Cannot read property 'wtf' of undefined
document.forms.buzz.wtf.value // "buzz-wtf"
document.forms.fuga.wtf.value // "fuga-wtf"
はい。
document.forms.buzz.wtf
と document.forms.fuga.wtf
は取れますが、document.forms.foo.wtf
は空文字列、document.forms.bar.wtf
はやはり霊圧存在が消えています。
もっと Javascript で調べてみる
document.forms.foo.wtf.value
が空文字列とは一体何なのでしょうか。
もう少し詳しく document.forms.foo
の子要素を見てみましょう。
document.forms.foo.elements.length // 2
typeof document.forms.foo.elements.wtf // "object"
document.forms.foo.elements.wtf[0].value // "foo-wtf"
document.forms.foo.elements.wtf[1].value // "bar-wtf"
4 つの input[name="wtf"]
があるのを想定したフォームでしたが、ネストがなくなった結果 document.forms.foo
に 2 つの input[name="wtf"]
が存在する形になってしまいました。
document.forms.foo.elements.wtf
が object だとか言っているので添字でアクセスしてみるときちんと値が入っていますね。
このフォームを送信してみると以下のパラメータが飛んでいきます。
?wtf=foo-wtf&wtf=bar-wtf
ふーん。
さて、この document.forms.foo.elements.wtf
は何者なのか聞いてみます。
document.forms.foo.elements.wtf.toString() // [object RadioNodeList]
typeof document.forms.foo.elements.wtf.value // "string"
RadioNodeList
だそうです。うん。なにそれ。
RadioNodeList とは一体
interface RadioNodeList : NodeList {
attribute DOMString value;
};
NodeList を継承したインターフェイスですね。
value
プロパティを持っています。これが何かというと、
radioNodeList . value [ = value ]
Returns the value of the first checked radio button represented by the object.
オブジェクト中の最初のチェック付きラジオボタンの値を返すよ。
とのこと。
この辺りの仕様を読むと、今回のケースは hidden な要素だから value = ""
となってしまうようです。
まとめ
form
要素をネストすると子 form
が小要素ごと親に巻き取られてしまう、という知見が得られました。
form
要素のネストはダメ。ゼッタイ。
そんな実装に出会ったら全力で逃げましょう。
番外編 (ここで Internet Explorer ですよ)
稀によくあることですが、今回も IE だけは注意が必要です。
Google Chrome, Firefox, Safari はどれも上記の通りの動作ですが、同じ条件で document.forms.foo.elements.wtf
を IE で参照した時に事件は起こります。
document.forms.foo.elements.wtf.toString() // [object HTMLCollection]
typeof document.forms.foo.elements.wtf.value // "undefined"
RadioNodeList
かと思いきや HTMLCollection
なのです。そして value
プロパティを持っていないので undefined
になります。
これはマジでやばい。例えば以下の様なケース。
if (document.forms.foo.wtf.value === '') {
// wtf の値が空の時の処理
}
IE だと value = undefined
なので if ブロックをスルーしてしまいますね。
極端な例ですが実際にこんなコードが実在するとかしないとか。
この挙動は IE8, 9, 10, 11, Microsoft Edge どれも同じでした。
素直に
if (!document.forms.foo.wtf.value) {
// wtf の値が存在しなかったり空の時の処理
}
としましょう。
以上、番外編でした。