「HTMLFormElement をネストさせたらどうなるの ?」っと

## はじめに

まず、`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 つのフォームを入れてみました。

```html:test.html
<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 を使いました。

```js
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` を見てみましょう。

```javascript
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` の中にいます。
この状態のフォーム要素の値は取れるのでしょうか。

```js
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` の子要素を見てみましょう。

```js
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` は何者なのか聞いてみます。

```js
document.forms.foo.elements.wtf.toString() // [object RadioNodeList]
typeof document.forms.foo.elements.wtf.value // "string"
```

`RadioNodeList` だそうです。うん。なにそれ。

## RadioNodeList とは一体

http://www.w3.org/TR/html5/infrastructure.html#htmlformcontrolscollection

```idl
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 で参照した時に事件は起こります。

```js
document.forms.foo.elements.wtf.toString() // [object HTMLCollection]
typeof document.forms.foo.elements.wtf.value // "undefined"
```

`RadioNodeList` かと思いきや `HTMLCollection` なのです。そして `value` プロパティを持っていないので `undefined` になります。
~~これはマジでやばい。~~例えば以下の様なケース。

```js
if (document.forms.foo.wtf.value === '') {
  // wtf の値が空の時の処理
}
```

IE だと `value = undefined` なので if ブロックをスルーしてしまいますね。
極端な例ですが実際にこんなコードが実在するとかしないとか。

この挙動は IE8, 9, 10, 11, Microsoft Edge どれも同じでした。
素直に

```js
if (!document.forms.foo.wtf.value) {
  // wtf の値が存在しなかったり空の時の処理
}
```

としましょう。
以上、番外編でした。