カジュアルな技術ノート

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

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

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

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

casual-tech-note.hatenablog.com

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

input タグの type="radio" における th:fieldth:object の提供機能を示していきたいと思います。実は inputタグ・type="radio" のカテゴリー型における提供機能と、とても似ています。
→ 参考: inputタグ・type="checkbox" における提供機能

f:id:shin-kinoshita:20181027180045p:plain

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

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

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

  4. value 属性 の設定
    別途 value 属性で指定することが必須となる。
    → 詳細は 「カテゴリー型における value 属性値の設定」を参考

サンプルコード

毎回のことながら、簡単なサンプル画面とコードをみていきましょう!
f:id:shin-kinoshita:20181021165714p:plain

名前と性別を尋ねます。
性別によって色と敬称を変えてみました!というページになります。
Input Page と Output Page の実際のコードは次の通りです。

<!-- Input Page -->
<h1>Input Page</h1>
<form action="/binding/input/radio" method="post" th:object="${radioForm}">
  <p>name</p>
  <input type="text" th:field="*{name}"/>
  <p>gender</p>
  <input type="radio" th:field="*{gender}" value="male"/>
  <label th:for="${#ids.prev('gender')}" th:text="male"></label>
  <input type="radio" th:field="*{gender}" value="female"/>
  <label th:for="${#ids.prev('gender')}" th:text="female"></label>
  <br/>
  <input type="submit" value="submit"/>
</form>
<!-- Output Page -->
<h1>Output Page</h1>
<p>Welcome for visiting this page,
  <span th:text="${radioForm.gender == 'male' ? 'Mr.' : 'Ms.'} + ${radioForm.name}"
        th:style="${radioForm.gender == 'male' ? 'color: blue;' : 'color: red;'} + 'font-size: 20px'"></span>
</p>

結果 Input Page のレンダリング結果は次のようになります。
f:id:shin-kinoshita:20181021163511p:plain

デフォルトで checked 属性が male のラジオボタンに付与されていますね。
これは、フォームオブジェクトの gender フィールドに "male" を初期値として与えることで付与されます。

public class RadioForm {
    private String name;
    // これで male のラジオボタンはチェックされた状態でレンダリング
    private String gender = "male";  

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getGender() {
        return gender;
    }
}

今回も念のため、完全版フロントエンドバックエンドコード。

カテゴリー型チェックボックスとの違い

今回紹介している inputタグ・type="radio" における提供機能は本当にカテゴリー型のチェックボックスと似ています。なのでここでは異なる点を取り上げてみます。
ぜひカテゴリー型のチェックボックスと比較してみてください!

対応するフィールドの型

すでに少しバックエンドのコードも見ましたが、性別を受け取るためのフィールド gender は String 型を採用しています。カテゴリ型チェックボックスであれば、ここは List などの Collection を採用していました。

// ラジオボタンでのフィールド例
private String gender = "male"; 
// チェックボックスでのフィールド例
private List<String> favoriteFruits = Collections.singletonList("apple");

なぜ型が異なるかという理由は単純で、ラジオボタンは必ず一つの値しか格納しないためです。

今回の例では、性別は男と女しかなく、必ず片方が選択されることを想定しています。

デフォルト値の設定

「対応するフィールドの型」での話とも関連しますが、ラジオボタンでは必ず一つの値が格納されていることを想定しています。複数の値を格納することもないですが、同時に空の状態も想定していません。

そのため 必ずデフォルト値を格納しておくことが望ましいです。

初期値を特に設定していなければ gender の値には null が格納されます。 つまり gender の値が "male" でも "female" でもないため、どちらのラジオボタンもチェックされていない状態でレンダリングされることになります。バリデーションの実装をしていない状態ではこのまま選択せずに送信することも可能です。そのままではヌルポなど、危険な状態になりえるかもしれません。

また、カテゴリー型チェックボックスでは提供されていた type="hidden" の input タグもラジオボタンでは提供されていません。
参考: type="hidden" の input タグ

この input タグは特定のフィールドの値が送信されないことを想定して付与されるというものでした。このタグが付与されないということは、必ずフィールドの値が送信されることを想定しているからだと思われます。

まとめ

チェックボックスラジオボタンは、実用面でも似ていますが、使い分けはしっかり存在します。
複数項目を選択するとか、しないとか。。

実装も似ているんだけど、微妙に違っていて、それはちょうど使い分けの違いと対応するようになっていると、この記事を書いてて思いました。

ラジオボタンにおける提供機能の紹介でしたが、ラジオボタンチェックボックスの違いがより明確になればなお嬉しいです。

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 を使用して実装していきたいところでもありますね。
お疲れ様でした〜。

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

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

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

casual-tech-note.hatenablog.com

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

パスワード入力を受け付けるinputタグにおける th:fieldth:object の提供機能は以下の三つです。

  • id 属性th:field に指定した変数名を代入
  • name 属性th:field に指定した変数名を代入
  • value 属性空文字 を代入

inputタグ・基本入力系での提供機能と似ていますが、微妙に違いますね。

value 属性 にはフィールドの値は代入されずに必ず空文字が入ります。このような仕様になっている背景をサンプルコードを通して見ていきましょう。

シンプルなサンプルコード

inputタグ・password における th:fieldth:object の提供機能を確認するために次のようなサンプル画面を考えていきましょう。 f:id:shin-kinoshita:20180923193836p:plain

名前とパスワードを入力として受け付け、受け取った値を出力するだけのシンプル画面です。
th:fieldth:object を使用した実際のコードは次の通りです。

<!-- Input Page -->
<h1>Input page</h1>
<form action="/binding/input/password" method="post" th:object="${passwordForm}">
  <p>name</p>
  <input type="text" th:field="*{name}"/>
  <p>password</p>
  <input type="password" th:field="*{password}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>
<!-- Output Page -->
<h1>Output Page</h1>
<p>Thank you for your submitting</p>
<p>Your name is 「<span th:text="${passwordForm.name}"></span></p>
<p>Your password is 「<span th:text="${passwordForm.password}"></span></p>

名前とパスワードの入力値を受け取るためのフォームオブジェクトとして passwordForm を用意しています。パスワード入力のレンダリング結果を見ると、id 属性name 属性th:field で指定したフィールド名、password が代入されていることが確認できます。
f:id:shin-kinoshita:20181007005620p:plain

一方 value 属性 は空になっています( 仮に password フィールドに値が入っていたとして空になります)。

完全なフロントエンドのコードはこちらに用意しておきました。

パスワードのバリデーションチェック

今回の Inputタグの type="password" では、なぜ value 属性 に空文字を代入するのでしょうか?。実はこれによって パスワードのバリデーションチェックに引っかかった際に、フィールド値を空にする手間を省いています。

ここでは例として、 パスワードの入力値は8〜16文字でなければならない という制限をかけることを考えて見ましょう。つまり入力のパスワード長がこの範囲外なら、バリデーションエラーにより再度入力画面に戻されるという仕様をイメージしてください。
f:id:shin-kinoshita:20180923195007p:plain

バリデーションを施している実装を見て見ましょう。
こちらはバックエンドにて実装します(完全版はこちら)。

public class PasswordForm {
    private String name;

    // ここでバリデーションを定義
    @Size(min = 8, max = 16)
    private String password;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

フォームクラスの password フィールドに @Size を使用することで、文字数制限を実現しています。

そしてバリデーションエラーの有無によって遷移先を変更するする必要があります。これはコントローラーの output メソッドで設定します。

@Controller
@RequestMapping("/binding/input/password")
public class PasswordController {
    @GetMapping
    public String input(Model model) {
        model.addAttribute("passwordForm", new PasswordForm());
        return "binding/input/password/input";
    }

    @PostMapping
    public String output(@Validated PasswordForm passwordForm, BindingResult br) {
        if (br.hasErrors()) {
            // Input Page へ遷移
            return "binding/input/password/input";
        }
        // Output Page へ遷移
        return "binding/input/password/output";
    }
}

バリデーションエラーが存在するときは Input Page へ、存在しないときは Output Page へ遷移するように分岐しています。

またエラーメッセージを表示するために、Input Page の view の実装も修正します。

<h1>Input page</h1>
<form action="/binding/input/password" method="post" th:object="${passwordForm}">
  <p>name</p>
  <input type="text" th:field="*{name}"/>
  <p>password</p>
  <!-- バリデーションエラーはここで表示だよー -->
  <ul th:if="${#fields.hasErrors('password')}">
    <li th:each="e : ${#fields.detailedErrors('password')}" th:text="${e.message}" style="color: red;"></li>
  </ul>
  <!------------------------->
  <input type="password" th:field="*{password}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>

さて、今回のようにバリデーションエラーによってユーザに入力値の修正を求める場合、すでに入力してもらった全項目の値を再入力する手間を防ぐためにそのまま保持する必要があります(今回の例では name の入力値ですね)。具体的には passwordForm を output メソッドの引数で受け取った時点で model に格納されているので、これによって保持しています。

しかしパスワードは中でもデリケートな項目であるため、パスワードの入力値のバリデーション結果に関係なく再入力を促すのが普通です。つまり password フィールドだけ値を初期化したい。こういう事情で、value 属性だけには空文字を設定してくれているというわけです。

まとめ

input タグ・type="password" における th:fieldth:object における提供機能は、value 属性における扱い以外は基本入力系と同じでいわばマイナーチェンジです。
なかなか使っていても見落としがちな機能ですが意外と仕事をしている、そんな黒子役のパスワード入力におけるお話でした。

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

Spring + Thymeleaf でよく見かける th:fieldth:object
フォームで入力を受け取る際に、ほぼ必ずと言っていいほどセットで使用します。

しかし入門者にとっては、とりあえず書いておけばフォームで値を受け取ることができる構文に見えかねない気もします(少なくとも、自分が最初にお目にかかった時はそんなイメージでした。。。)。

とりあえずルールとして th:object にフォームオブジェクトを、 th:field に対応するフォームオブジェクトのフィールドを書けばいいのはなんとなくわかります。
でもそれ以降の仕組みが一目ではわからず気持ち悪い。。
そんな風に感じていたので、 th:fieldth:object が実際にはどんな機能を提供しているのか、主な機能を解説します。

th:field と th:object がサポートしているタグ一覧

th:fieldth:object はどのタグにでも使用できるわけではありません。
現在デフォルトでサポートされているのは次の4つです。

  • input タグ
  • select タグ
  • option タグ
  • textarea タグ

そして タグによって th:fieldth:object が提供している機能はいくつか異なります。 また input タグに関しては type 属性によっても提供される機能が変わってきます (input タグに関しては type 属性によってはサポートされていないものもあります)。

一方で input タグでサポートしている type 属性における提供機能は大半が同じ だったりします。 今回はこの大半の type 属性群を 基本入力系 と呼ぶこととしましょう。

まずどの type 属性の input タグがこの基本入力系に分類されるのか、以下にまとめてみました(Thymeleaf 3.0現在)。

input タグの type 属性による th:field と th:object の提供機能分類
パターン type 属性一覧
基本入力系 text, hidden, detetime, datetime-local, date, time, month, week, number, range, email, url, search, tel, color
その他 password, checkbox, radio, file

上記の表に含まれない type 属性はサポート外です。
基本入力系のへの提供機能は他のタグとその他に分類されているものと共通している点も多いので、以降こちらを取り上げていきます(その他に分類されるものにも重要なものが多いのでまたの機会に。。)。

基本入力系以外における提供機能は、別記事で紹介します。

サンプルコード

実際にフォームで入力を受け取るための UI にはいろんなものがありますが、最も基本的なものの一つがテキスト入力です。 今回はテキスト入力を受け取り、その内容を出力する単純なプログラムを通して th:fieldth:object の提供機能を見ていきましょう。
つまり基本入力系の中でも input タグの type 属性が text のものを扱っていきます。

具体的には次のような入力画面と出力画面を作ることを考えてみます。

f:id:shin-kinoshita:20180904014409p:plain

例として Input Page にて name に「subaru」、text に「I love india curry」と入力し submit すると Output Page はこのようになります。

これ以上ないほどシンプルな実装です。
以下に実際のコードを書いていきましょう。

<!-- Input Page -->
<h1>Input page</h1>
<form action="/binding/input/text" method="post" th:object="${textForm}">
  <p>name</p>
  <input type="text" th:field="*{name}"/>
  <p>text</p>
  <input type="text" th:field="*{text}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>
<!-- Output Page -->
<h1>Output Page</h1>
<p>Thank you for your submitting</p>
<p>Your name is 「<span th:text="${textForm.name}"></span></p>
<p>Your text is 「<span th:text="${textForm.text}"></span></p>

ここで登場している textForm はコントローラで model に追加した入力値を受け取るためのオブジェクトです。 一応実際のバックエンドフロントエンドの完全版実装も準備して見ました(見るにしてもあまりに単純かもしれませんが。。。)。

th:field と th:object による提供機能

公式のチュートリアルhttps://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#inputs)によると、th:field は 次のような機能を提供しているとのことです(厳密にはこれ以外にもありますが、本質から外れるので割愛)。

  • id 属性th:field に指定した変数名を代入
  • name 属性th:field に指定した変数名を代入
  • value 属性th:field に指定した変数に格納された値を代入

このことから Input Page の form タグは以下と概ね同じで、書き換えてあげることが可能です。

<!-- Input Page -->
<form action="/binding/input/text" method="post" th:object="${textForm}">
  <p>name</p>
  <input type="text" id="name" name="name" th:value="*{name}"/>
  <p>text</p>
  <input type="text" id="text" name="text" th:value="*{text}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>

元々の Input Page のレンダリングを確認しても、一致することが確認できます。
f:id:shin-kinoshita:20181010223803p:plain

これが th:fieldth:object による基本的な提供機能となります。
他の多くのタグや input の属性値でも 共通しています。

th:field と name, id, value 属性の併用

th:fieldth:object によって id 属性name 属性value 属性 が生成されることはわかりました。 ここで一つ疑問に思うこととして th:field と一緒にこれらの属性を併用して記述した場合はどうなるのでしょうか? どちらかの値が上書きされることは予想できますが、実はここの優先度は属性値によって異なります。 検証用に、以下のような view を用意してみました。

<h1>Input page</h1>
<form action="/binding/input/text" method="post" th:object="${textForm}">
  <p>name</p>
  <input type="text" th:field="*{name}" name="manualName" id="manualId" value="manualValue"/>
  <p>text</p>
  <input type="text" th:field="*{text}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>

レンダリングの結果は次のようになります。
f:id:shin-kinoshita:20181010223503p:plain
この結果からそれぞれの属性についてみてみると、

  • name 属性
    値が name であることから、th:field 属性が優先。
  • id 属性
    値が manualId であることから、id 属性が優先。
  • value 属性
    値が 空の入力値 になっていることから、th:field が優先。

実は id 属性だけ優先度が違うのです。
実際フォームバインディングに使用されるのは name 属性value 属性 のみです(詳しくは後述します)。 id 属性は通常のセレクタとして使用されることを想定しているため、このような優先度になっているものと推測されます。

まとめ

今回は th:fieldth:object を使用するケースの中でも基本的なものを紹介してみました。 th:fieldth:object は構文的に使用しがちということを言いましたが、実際の挙動はタグや属性値によって異なっています。 しっかり挙動を押さえることは、短く可読性よく記述することにも繋がるかと思うので参考になればと思います。