カジュアルな技術ノート

小難しい技術のお話をできるだけわかりやすく...

th:field と th:object によるフォームバインディング機能(inputタグ・checkbox編)

th:fieldth:object が提供する機能説明の続きです。
今回は inputタグの type="checkbox" をみていきましょう。

th:fieldth:object の提供機能はタグや属性値によって異なります。
サポートされているタグの一覧はこちら

casual-tech-note.hatenablog.com

inputタグ・type="checkbox" における提供機能

input タグの type="checkbox" における th:fieldth:object の提供機能を次に示します。チェックボックスにおける提供機能は、他と比べても多機能で複雑な部類に入るでしょう。

各機能の詳細をさっと見たいなら、各々リンクをたどるのがおすすめです!

  • id 属性th:field に指定した変数名を 連番付き で代入
    → 詳細は 「連番付きの id 属性

  • name 属性th:field に指定した変数名を代入

  • checked 属性 の設定
    value に指定された値とフィールドに指定した値が同じであるなら checked 属性 が付与される。
    → 詳細は 「checked 属性の設定

  • inputタグ・type="hidden" の付与
    → 詳細は 「inputタグ・type="hidden" の付与

そしてさらに value 属性 の設定にも関与しますが、th:field で指定するフィールドの型が Boolean または boolean かどうかによって提供機能は変化します。

サンプルコードと2種類の checkbox

th:fieldth:object の提供機能はフィールドの型(Boolean または boolean なのか、そうでないのか)によって異なると述べましたが、どのように型を選択するのがいいのでしょうか? 。使い分けもありますが、それぞれの実装イメージを掴むためにもう少し深ぼってみます。

そこで次のようなサンプル画面を作ることを考えましょう。 f:id:shin-kinoshita:20181003234439p:plain

この中に好きな果物はありますか?
何個選んでもいいですよ(選択肢をもっと増やしたい。。)という画面です。

フィールドの型が 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>

結果、実際のレンダリングは次のようになります。
f:id:shin-kinoshita:20181007011255p:plain

要素型による実装

今回のサンプル画面では 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>

レンダリング結果です。 f:id:shin-kinoshita:20181007011322p:plain

二通りの実装を一気にとりあえず書きました(息切れしていないですか?)。
ここからはサンプルコードを通して、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>

f:id:shin-kinoshita:20181014123850p:plain
レンダリング結果

この機能は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>

f:id:shin-kinoshita:20181014123850p:plain
レンダリング結果

ここで実装しているのは 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>

f:id:shin-kinoshita:20181009213829p:plain
レンダリング結果

例えば appleチェックボックスをチェックして submit した時、value 属性の値である true が送信され、フォームオブジェクトの apple フィールドに格納されます。

checked 属性の設定

th:fieldth:object の提供機能として、value 属性 の値をフィールドが保持した状態でレンダリングされると checked 属性が付与されます。

サンプル画面において、appleチェックボックスchecked 属性を付与され、チェックされた状態でレンダリングすることを考えてみます。カテゴリー型と要素型、それぞれで checked 属性を付与するには、

  • カテゴリー型
    フォームオブジェクトの fruits フィールドに文字列 apple を格納
  • 要素型
    フォームオブジェクトの apple フィールドに true を格納

この状態で入力画面をレンダリングすると 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>

f:id:shin-kinoshita:20181009213829p:plain
レンダリング結果

name 属性値はフィールド名の先頭に _ が付け足されたものとなっています。これは 該当するフィールドのデフォルト値を設定するためのタグ になります。このタグは、特定のタグがチェックされていない状態で値が送信されなかった時に効果を発揮します。

例として先ほどの checked 属性が付与された appleチェックボックスをもちいて説明します。まず _apple="on" は submit 時にチェックの有無に関わらず必ず送信されます。一方 apple の値はチェックが入っていれば送信され、そうでなければ送信されません。
f:id:shin-kinoshita:20181010231827p:plain

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 してみましょう。

遷移結果は次のようになるはずです。
f:id:shin-kinoshita:20181004002315p:plain

チェックを外しているのに、apple が好きな食べ物として認識されてしまっています。値が特に何も送信されなければ、フォームオブジェクトのフィールドは値が更新されることもないため初期値のままです。これを防ぐために hidden 属性の input タグでデフォルトの値を送信しているということになります。

まとめ

チェックボックスにおける th:fieldth:object による提供機能、いかがだったでしょうか?
提供機能がてんこ盛りだったので、もうお腹いっぱいかもしれません。。

でもその分、使用した時の効果は大きいです(同じ機能を th:field 使用せずに実装することを考えて見てください。途端に属性値まみれになってしまいます。。)。

だからこそ、ぜひ実際に th:fieldth:object を使用して実装していきたいところでもありますね。
お疲れ様でした〜。