概要
HTMLのform
タグは仕様上入れ子にできない。
例えば以下のようなHTMLを書いてみる。
<form id="outer">
<form id="inner"></form>
</form>
これをChromeで開いてDevToolsを開くとこうなる。
内側のform
タグが消えた。
HTMLの仕様なので仕方がないが、とはいえform
タグを入れ子にしたい局面もある。
たとえば、今自分は以下のようなUIを個人開発で書いている。
日付や科目などを登録するための親フォームと、科目名の変更を行うための子フォームがある。
子フォームのフォーカス時にエンターキーを押したら科目名の変更を行いたいし、親フォームのフォーカス時にエンターキーを押したら日付や科目などの登録を行いたい。
Notionのセレクトとかもこんな感じのUIなので、そこまで特殊なシチュエーションではないかなと思う。
これをHTMLの仕様に怒られないように実装しようと思うと以下のような方法があるらしい。
- フォームを親子関係ではなく並列の関係にして、位置調整はJSで頑張る
- 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が実行されるようにしていく。
<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
という属性だ。
この属性をinput
やselect
に指定すると、それらの要素が属しているform
要素を指定することができる。
つまり直接form
タグで囲まれていないinput
やselect
を、さもform
タグで囲まれているかのように振る舞わせることができる。
そこで外側のform
タグの範囲をギューッと狭めて、Outer Submitボタンだけを囲むようにし、form
の入れ子関係を解消する。
そして各input
やselect
にform
属性を指定して、属するform
を指定する。
<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ではサポートされていないことに注意。
コメント