概要

HTMLのformタグは仕様上入れ子にできない。

例えば以下のようなHTMLを書いてみる。

form.html
<form id="outer">
    <form id="inner"></form>
</form>

これをChromeで開いてDevToolsを開くとこうなる。

DevToolsの画面

内側のformタグが消えた。

HTMLの仕様なので仕方がないが、とはいえformタグを入れ子にしたい局面もある。

たとえば、今自分は以下のようなUIを個人開発で書いている。

個人開発中のUI

日付や科目などを登録するための親フォームと、科目名の変更を行うための子フォームがある。

子フォームのフォーカス時にエンターキーを押したら科目名の変更を行いたいし、親フォームのフォーカス時にエンターキーを押したら日付や科目などの登録を行いたい。

Notionのセレクトとかもこんな感じのUIなので、そこまで特殊なシチュエーションではないかなと思う。

これをHTMLの仕様に怒られないように実装しようと思うと以下のような方法があるらしい。

  1. フォームを親子関係ではなく並列の関係にして、位置調整はJSで頑張る
  2. submitボタンだけをformで囲み、その他の要素はform属性を使って属するフォームを指定する

方法1はイメージしやすいと思う。

例えばAフォームの下にBフォームを出したいなら、Aフォームの座標をDOMなどを利用して取得し、その値を利用してBフォームの表示場所を調節するのだ。

ただJSで位置計算のロジックを書くのは少し面倒だし、Reactなんかを使っていると並列になっているコンポーネント間でステートの共有をするのが少し面倒だ。

例えば並列関係にある2つのコンポーネントの両方で利用するステートがあるとしたら、2つのコンポーネントの共通の祖先のコンポーネントにステートを定義するかuseContextなどを使ってグローバルなステートを定義する必要がある。

親子のままならposition: relative;position: absolute;で簡単に位置調整ができるし、ステートの共有もpropsのバケツリレーをするだけで良いから簡単だ。

以上の理由から今回は方法1は見送り。

ちなみに、Notionはおそらく方法1を採用しているっぽいので、これも全然悪くはないと思う。単純に自分の好みではなかっただけ。

方法

というわけで今回採用した方法2の紹介。

以下のコードはformが入れ子になっているので現状はアウトだが、これをHTMLの仕様に反しないように成立させたい。

今からOuter Inputをフォーカス時にエンターキーを押したらOuter Submitが実行され、Inner Inputをフォーカス時にエンターキーを押したらInner Submitが実行されるようにしていく。

ng-form.html
<form id="outer" onsubmit="alert('Outer Submit')">
    <input type="text" value="Outer Input"/>
    <form id="inner" onsubmit="alert('Inner Submit')">
        <input type="text" value="Inner Input"/>
        <button type="submit">Inner Submit</button>
    </form>
    <button type="submit">Outer Submit</button>
</form>

この方法で利用するのはHTML5で追加されたformという属性だ。

この属性をinputselectに指定すると、それらの要素が属しているform要素を指定することができる。

つまり直接formタグで囲まれていないinputselectを、さもformタグで囲まれているかのように振る舞わせることができる。

そこで外側のformタグの範囲をギューッと狭めて、Outer Submitボタンだけを囲むようにし、formの入れ子関係を解消する。

そして各inputselectform属性を指定して、属するformを指定する。

ok-form.html
<input type="text" value="Outer Input" form="outer"/>
<form id="inner" onsubmit="alert('Inner Submit')">
    <input type="text" value="Inner Input" form="inner"/>
    <button type="submit">Inner Submit</button>
</form>
<form id="outer" onsubmit="alert('Outer Submit')">
    <button type="submit">Outer Submit</button>
</form>

こうすることで、Outer Inputフォーカス時にエンターキーを押したらOuter Submitが実行され、Inner Inputフォーカス時にエンターキーを押したらInner Submitが実行されるようになる。

Outer Inputはform要素に囲まれていないがform="outer"を指定することで、<form id="outer">にさも囲まれているかのように振る舞う。

あまり綺麗なコードではないが、こうすることで親子関係を維持したままformタグを(実質)入れ子にすることができた。

なお、form属性は割と新し目の機能なのでIEではサポートされていないことに注意。

参考

formタグは入れ子にできない&その対処法