th:field と th:object によるフォームバインディング機能(inputタグ・checkbox編)
th:field
と th:object
が提供する機能説明の続きです。
今回は inputタグの type="checkbox" をみていきましょう。
*th:field
と th:object
の提供機能はタグや属性値によって異なります。
サポートされているタグの一覧はこちら
casual-tech-note.hatenablog.com
inputタグ・type="checkbox" における提供機能
input タグの type="checkbox" における th:field
と th:object
の提供機能を次に示します。チェックボックスにおける提供機能は、他と比べても多機能で複雑な部類に入るでしょう。
各機能の詳細をさっと見たいなら、各々リンクをたどるのがおすすめです!
id
属性 にth:field
に指定した変数名を 連番付き で代入
→ 詳細は 「連番付きの id 属性」name
属性 にth:field
に指定した変数名を代入checked
属性 の設定
value
に指定された値とフィールドに指定した値が同じであるならchecked
属性 が付与される。
→ 詳細は 「checked 属性の設定」inputタグ・type="hidden" の付与
→ 詳細は 「inputタグ・type="hidden" の付与」
そしてさらに value
属性 の設定にも関与しますが、th:field
で指定するフィールドの型が Boolean または boolean かどうかによって提供機能は変化します。
フィールドの型が Boolean でも boolean でもない時の
value
属性 の設定
別途value
属性で指定することが必須となる。
→ 詳細は 「カテゴリー型における value 属性値の設定」フィールドの型が Boolean または boolean の時
value
属性 の設定
value
に true を代入
→ 詳細は 「要素型における value 属性値の設定」
サンプルコードと2種類の checkbox
th:field
と th:object
の提供機能はフィールドの型(Boolean または boolean なのか、そうでないのか)によって異なると述べましたが、どのように型を選択するのがいいのでしょうか? 。使い分けもありますが、それぞれの実装イメージを掴むためにもう少し深ぼってみます。
そこで次のようなサンプル画面を作ることを考えましょう。
この中に好きな果物はありますか?
何個選んでもいいですよ(選択肢をもっと増やしたい。。)という画面です。
フィールドの型が Boolean または boolean であるかどうかで大きく変わってくるので、ここではしっかりと次のように区別することにします。
- カテゴリー型: 型が Boolean でも boolean でもないフィールド
- 要素型: 型が Boolean または boolean であるフィールド
なぜこのような名前にしたのか。。
今回の例では次のような対応関係だと考えてください。
- カテゴリー: 好きな果物
- 要素: apple、banana、mango
つまり、入力を受け取り格納するフィールドを各要素にするのか、それともカテゴリーにしてしまうのかということにそれぞれ対応しています。
どちらのフィールド設計でもサンプル画面は実装可能です。
では、実際の実装をそれぞれ見ていきましょう。
カテゴリー型による実装
今回のサンプル画面におけるカテゴリーは「好きな果物」ということでした。
フィールド名を fruits
としてフォームクラスを実装していきます。
public class CheckboxCategoryForm { private List<String> fruits; public void setFruits(List<String> fruits) { this.fruits = fruits; } public List<String> getFruits() { return fruits; } }
チェックボックスは複数選択可能であるため、複数要素を格納できる Collection を fruits
の型として選択するのが良いでしょう(ラジオボタンのように一つしか選択できないなら Collection 型にする必要はないのですが)。ここでは List<String>
を選択、つまり好きな果物の要素(apple、banana、mango)は String 型で受け取るということにしています。
次は入力画面の view の実装です。
<form action="/binding/input/checkbox/category" method="post" th:object="${checkboxCategoryForm}"> <p>What is your favorite fruit?</p> <div> <label th:for="${#ids.next('fruits')}" th:text="apple"></label> <input type="checkbox" th:field="*{fruits}" value="apple"/> </div> <div> <label th:for="${#ids.next('fruits')}" th:text="banana"></label> <input type="checkbox" th:field="*{fruits}" value="banana"/> </div> <div> <label th:for="${#ids.next('fruits')}" th:text="mango"></label> <input type="checkbox" th:field="*{fruits}" value="mango"/> </div> <input type="submit" value="submit"/> </form>
結果、実際のレンダリングは次のようになります。
要素型による実装
今回のサンプル画面では apple、banana、mango の3つの要素を準備する必要があります。 要素型での実装だと、入力を格納するためのフォームクラスは次のようになります。
public class CheckboxElementForm { private Boolean apple; private Boolean banana; private Boolean mango; public void setApple(boolean apple) { this.apple = apple; } public Boolean getApple() { return apple; } public void setBanana(Boolean banana) { this.banana = banana; } public Boolean getBanana() { return banana; } public void setMango(Boolean mango) { this.mango = mango; } public Boolean getMango() { return mango; } }
対して、入力画面はこんな感じでしょうか。
<h1>Input page</h1> <form action="/binding/input/checkbox/element" method="post" th:object="${checkboxElementForm}"> <p>What is your favorite fruit?</p> <div> <label th:for="${#ids.next('apple')}" th:text="apple"></label> <input type="checkbox" th:field="*{apple}"/> </div> <div> <label th:for="${#ids.next('banana')}" th:text="banana"></label> <input type="checkbox" th:field="*{banana}"/> </div> <div> <label th:for="${#ids.next('mango')}" th:text="mango"></label> <input type="checkbox" th:field="*{mango}"/> </div> <input type="submit" value="submit"/> </form>
レンダリング結果です。
二通りの実装を一気にとりあえず書きました(息切れしていないですか?)。
ここからはサンプルコードを通して、inputタグ・type="checkbox" における提供機能を一つ一つ、より深く見ていきます。
連番付きの id 属性
レンダリング結果を順に見ていきます。
まずは id
属性 ですが th:field
に指定した変数名に連番をつけた値 となっています。
<!-- カテゴリー型 apple チェックボックスの実装 --> <div> <label th:for="${#ids.next('apple')}" th:text="apple"></label> <input type="checkbox" th:field="*{apple}"/> </div>
この機能は2種類の checkbox のなかでも カテゴリー型 向けの機能です。
基本入力系では、 name
属性も value
属性も対応するフィールド名が入ってました。そして今回の例ではどのチェックボックスも対応するフィールドは fruits フィールドです。 カテゴリー型のチェックボックスでは、 name
属性がどれも同じで value
属性は異なるものを複数レンダリングしないといけないことがほとんどです。 でも id
属性は重複が許されないので、どのチェックボックスにも同じ fruits を指定するわけにはいきません。これを回避するための連番ということですね。
value 属性の設定
既に述べた通り value
属性値の設定は、カテゴリー型か要素型かによって異なります。
カテゴリー型における value 属性値の設定
カテゴリー型ということはフィールドの型は Boolean でも boolean でもないケースです。
カテゴリー型の場合は th:field
とは別に value
属性値を指定してあげる必要があります。
例としてカテゴリー型によるサンプル画面の実装から apple のチェックボックスの実装を抜粋します。
<!-- カテゴリー型 apple チェックボックスの実装 --> <div> <label th:for="${#ids.next('fruits')}" th:text="apple"></label> <input type="checkbox" th:field="*{fruits}" value="apple"/> </div>
ここで実装しているのは apple のチェックボックスですが th:field
に指定した fruits だけでは apple の情報が一切含まれていません。そのため、value
属性値に apple を別途指定してあげる必要があるのです。
基本入力系では th:field
属性と th:value
属性を併用して実装した場合 th:field
属性 の方が優先されましたがチェックボックスではちゃんと th:value
属性 が優先される仕様になっています。
そしてこの apple のチェックボックスをチェックして submit した時、value
属性の値である apple が送信され、フォームオブジェクトの fruits フィールドに格納されます。
要素型における value 属性値の設定
要素型ということはフィールドの型は Boolean または boolean であるケースです。
要素型の場合は value
属性値はかならず true だと決まっています。 Boolean または boolean であるため、チェックされたときの値は一意に決まってしまうのです。
<!-- 要素型 apple チェックボックスの実装 --> <div> <label th:for="${#ids.next('apple')}" th:text="apple"></label> <input type="checkbox" th:field="*{apple}"/> </div>
例えば apple のチェックボックスをチェックして submit した時、value 属性の値である true が送信され、フォームオブジェクトの apple フィールドに格納されます。
checked 属性の設定
th:field
と th:object
の提供機能として、value
属性 の値をフィールドが保持した状態でレンダリングされると checked
属性が付与されます。
サンプル画面において、apple のチェックボックスに checked
属性を付与され、チェックされた状態でレンダリングすることを考えてみます。カテゴリー型と要素型、それぞれで checked
属性を付与するには、
この状態で入力画面をレンダリングすると checked
属性が付与されます。例としてフォームクラスのフィールドに初期値を与えることで apple のチェックボックスがチェックされた状態で表示されるようにして見ましょう。フォームクラスの apple フィールドに次のように初期値を与えることで、チェックされた状態になります。
// カテゴリー型フォームクラスの apple フィールドの変更 private List<String> fruits = Collections.singletonList("apple");
// 要素型フォームクラスの apple フィールドの変更 private Boolean apple = true;
inputタグ・type="hidden" の付与
今回付与されている type
属性が hidden の input タグを観察して見ましょう。
こちらは要素型の input タグですね。
<!-- 要素型 apple チェックボックスの実装 --> <div> <label th:for="${#ids.next('apple')}" th:text="apple"></label> <input type="checkbox" th:field="*{apple}"/> </div>
name
属性値はフィールド名の先頭に _
が付け足されたものとなっています。これは 該当するフィールドのデフォルト値を設定するためのタグ になります。このタグは、特定のタグがチェックされていない状態で値が送信されなかった時に効果を発揮します。
例として先ほどの checked
属性が付与された apple のチェックボックスをもちいて説明します。まず _apple="on" は submit 時にチェックの有無に関わらず必ず送信されます。一方 apple の値はチェックが入っていれば送信され、そうでなければ送信されません。
POST により apple
と _apple
の両方が送信されるときは、 _apple
は特に意味を持ちませんが、 _apple
属性値のみが送信されたときは、フォームオブジェクトの apple フィールドにデフォルト値を設定してくれます(_apple
の値である "on" は特に意味を持ちません)。
ここでいうデフォルト値とは、チェックボックスにチェックが入っていない時に本来フィールドに入っているはずの値です。
つまり、要素型では false が、カテゴリー型では空の Collection です。
この追加されたタグの効力を見るために、hidden タイプの input タグが付与されない実装に改良して比較して見ましょう。
これを実現するために th:field
属性を使わずに実装して見ます。
<!-- 修正後の要素型の実装 --> <form action="/binding/input/checkbox/element" method="post" th:object="${checkboxElementForm}"> <p>What is your favorite fruit?</p> <div> <label th:for="${#ids.next('apple')}" th:text="apple"></label> <input type="checkbox" id="apple1" name="apple" value="true" th:checked="*{apple}"/> </div> <div> <label th:for="${#ids.next('banana')}" th:text="banana"></label> <input type="checkbox" id="banana1" name="banana" value="true" th:checked="*{banana}"/> </div> <div> <label th:for="${#ids.next('mango')}" th:text="mango"></label> <input type="checkbox" id="mango1" name="mango" value="true" th:checked="*{mango}"/> </div> <input type="submit" value="submit"/> </form>
注)フォームオブジェクトの apple フィールドに true を代入しておいた状態にしてください(checked 属性値の設定で行った処理です)。
レンダリング結果は hidden タイプの input タグが付与されない点以外は同じです。ここで apple のチェックボックスのチェックを外して submit してみましょう。
遷移結果は次のようになるはずです。
チェックを外しているのに、apple が好きな食べ物として認識されてしまっています。値が特に何も送信されなければ、フォームオブジェクトのフィールドは値が更新されることもないため初期値のままです。これを防ぐために hidden 属性の input タグでデフォルトの値を送信しているということになります。
まとめ
チェックボックスにおける th:field
と th:object
による提供機能、いかがだったでしょうか?
提供機能がてんこ盛りだったので、もうお腹いっぱいかもしれません。。
でもその分、使用した時の効果は大きいです(同じ機能を th:field
使用せずに実装することを考えて見てください。途端に属性値まみれになってしまいます。。)。
だからこそ、ぜひ実際に th:field
と th:object
を使用して実装していきたいところでもありますね。
お疲れ様でした〜。