はじめに
Thymeleafで、バカの一つ覚えみたいにただただth:fieldを多用していたのですが、最近その理解レベルから脱却できたような気がするので、その内容についてまとめたいと思います。
概要としては、Spring Frameworkで値の受け渡しを担っているFormクラスにフォーカスした話になります。最終的にはFormクラスを一切使わないで値の受け渡しを行います。
狙いは以下の通りです。
- Spring Frameworkの値の受け渡しの仕組みを知る。
- Spring Frameworkの実装をより柔軟に考えられるようになる。
理解の仕方の話なので、あんまり実用的ではないです。
前提
Spring Frameworkで作成されたWebプロジェクトが必要です。
テンプレートエンジンはThymeleafを前提とした話になります。
一般的な実装
まずはFormを利用した一般的な実装を考えてみましょう。ここからコードをいじって、最終的にFormクラスを消してしまいます。
BookForm.java
public class BookForm {
private String title;
private int price;
private String summary;
// Getter, Setterは省略
}
SampleController.java
@Controller
public class SampleController {
// 入力画面
@GetMapping("/input")
public String input(@ModelAttribute("bookForm") BookForm bookForm) {
return "input";
}
// 書籍情報画面
@PostMapping("/bookinfo")
public String toBookInfo(@ModelAttribute("bookForm") BookForm bookForm) {
return "bookinfo";
}
}
input.html
<!-- bodyタブ内部のみ -->
<h3>Formクラスを利用した実装</h3>
<form th:action="@{/bookinfo}" method="post" th:object="${bookForm}">
<label>タイトル</label><br>
<input type="text" th:field="*{title}"><br>
<label>値段</label><br>
<input type="text" th:field="*{price}"><br>
<label>概要</label><br>
<input type="text" th:field="*{summary}"><br>
<button>送信</button>
</form>
bookinfo.html
<!-- bodyタブ内部のみ -->
<h3>Formクラスを利用した実装</h3>
<div th:object="${bookForm}">
<label>タイトル</label><br>
<span th:text="*{title}"></span><br>
<label>値段</label><br>
<span th:text="*{price}"></span><br>
<label>メモ</label><br>
<span th:text="*{summary}"></span><br>
</div>
入力した値が出力されるという、ものすごく簡単な実装になります。
input.htmlのth:fieldとth:objectを消す
まず、input.htmlからth:field
とth:object
を消すことを考えてみます。
ここで、th:field
およびth:object
の正体を知るために、Chromeのデベロッパーツールを使って、Thymeleafで記述したinput.htmlがどうなっているのか確認してみましょう。
formタグからth:objectが消えて、th:fieldで記述していたタイトルを入力するテキストボックスは、<input type="text" id="title" name="title" value>
となっていることが分かると思います。
端的に言えば、デベロッパーツールで表示された通りに書けばth:fieldとth:objectを消すことが可能です。
必要な記述だけに絞って書くと以下のようになります。
input.html
<!-- bodyタブ内部のみ -->
<h3>th:objectを消した実装</h3>
<form th:action="@{/bookinfo}" method="post">
<label>タイトル</label><br>
<input type="text" name="title"><br>
<label>値段</label><br>
<input type="text" name="price"><br>
<label>概要</label><br>
<input type="text" name="summary"><br>
<button>送信</button>
</form>
このhtmlの記述でも、一般的な実装で示した「入力した値が出力される」処理を実現することができます。
th:objectの記述がなくなったということは、SampleController.javaのinputメソッドのFormクラスも消すことができます。
// 入力画面
@GetMapping("/input")
public String input() {
return "input";
}
一番大事なことは、
inputタグのneme属性にFormクラスのフィールド名を指定すると、入力した値がそのフィールドに格納される
ということです。
この記事の言いたいことのほぼ全ては、この事実にあります。
(上記はth:field
の振る舞いとして、実は厳密に正しい説明にはなっていません。詳しくはth:field と th:object によるフォームバインディング機能(inputタグ・基本入力系編)を参照して頂けたら幸いです)
Formクラスを入れ替える
上記の仕組みを念頭に置くと、こんなことができます。
例えば、BookFormとは異なる以下のようなMovieFormというFormクラスがあったとします。
MovieForm.java
public class MovieForm {
private String title;
private String summary;
// Getter, Setterは省略
}
(不自然な例で申し訳ないが、)このMovieFormクラスを使って映画化した書籍情報を、映画の情報としても扱いたいとします。bookinfo.htmlに、「映画情報としても送信」ボタンを用意して、あくまでBookFormを送信するように書き換えます。
bookinfo.html(大幅に変更)
<!-- bodyタブ内部のみ -->
<h3>BookFormからMoiveFormに値を入れ替える</h3>
<form th:action="@{/movieinfo}" method="post" th:object="${bookForm}">
<label>タイトル</label><br>
<input type="text" readonly th:field="*{title}"><br>
<label>値段</label><br>
<input type="text" readonly th:field="*{price}"><br>
<label>概要</label><br>
<input type="text" readonly name="summary" th:value="*{summary}"><br>
<button>映画情報としても送信</button>
</form>
SampleController.java(追記)
// 映画情報出力画面(追記分)
@PostMapping("/movieinfo")
public String toMovieinfo(@ModelAttribute("movieForm") MovieForm movieForm) {
return "movieinfo";
}
movieinfo.html
<!-- bodyタブ内部のみ -->
<h3>BookFormからMoiveFormに値を入れ替える</h3>
<div th:object="${movieForm}">
<label>タイトル</label><br>
<span th:text="*{title}"></span><br>
<label>概要</label><br>
<span th:text="*{summary}"></span><br>
</div>
このようにBookFormクラスとして送信した値が、MovieFormに格納され、movieinfo.htmlで出力されます。ちょっとひねくれた実装になっているので、不思議な心持ちになる人もいるかもしれません。
ポイントは、BookForm、MovieFormにtitle
やsummary
といった同じ名前のフィールドが存在しているところにあります。
bookinfo.htmlでは二種類書きましたが、今回の処理結果においては
<input type="text" readonly th:field="*{title}">
は<input type="text" readonly name="title" value="あいうえお">
<input type="text" readonly name="summary" th:value="*{summary}">
は<input type="text" readonly name="summary" value="テスト">
と同じ意味です。つまりname属性の値がFormのフィールド名と同じとなっているので、辻褄があっている訳です。
##buttonに値を指定して、Formクラスで受け取る
あれ、th:field使わなくても実装できるのでは?
とそろそろ思えてきたのではないのでしょうか?
さてさて、今度は、buttonに指定された値を受け取ることを考えてみます。
MovieFormに以下のようなcategoryフィールドを追加して、bookinfo.htmlにカテゴリーごとのボタンを増やします。押したボタンに応じてmovieinfo.htmlにテキストが表示されます。
MovieForm.java
public class MovieForm {
private String title;
private String summary;
// categoryフィールドを追加
private String category;
// Getter, Setterは省略
}
bookinfo.html(categoryに値を指定したbuttonを複数追加)
<!-- bodyタブ内部のみ -->
<h3>buttonに値を指定して、Formクラスで受け取る</h3>
<form th:action="@{/movieinfo}" method="post" th:object="${bookForm}">
<label>タイトル</label><br>
<input type="text" readonly th:field="*{title}"><br>
<label>値段</label><br>
<input type="text" readonly th:field="*{price}"><br>
<label>概要</label><br>
<input type="text" readonly name="summary" th:value="*{summary}"><br>
<button name="category" value="アクション">アクション映画として送信</button>
<button name="category" value="ファンタジー">ファンタジー映画として送信</button>
<button name="category" value="SF">SF映画として送信</button>
</form>
movieinfo.html
<!-- bodyタブ内部のみ -->
<h3>buttonに値を指定して、Formクラスで受け取る</h3>
<div th:object="${movieForm}">
<label>タイトル</label><br>
<span th:text="*{title}"></span><br>
<label>概要</label><br>
<span th:text="*{summary}"></span><br>
カテゴリーは<span th:text="*{category}"></span>です。
</div>
結果は以下の通り
th:field
ばかり使っていると、入力できない項目のvalue値を変更することは難しいですが、nameの値とvalueの値が紐づいていることが分かると、このようにボタンのvalue値によって処理を変更することも可能になります。
Formクラスを消す
最終段階として、Formクラスを消してしまいます。ここで活躍するのはアノテーションの@RequestParam
です。
入力した値が出力される実装は以下のようになります。
input.html
→input.htmlのth:fieldとth:objectを消すを参照
SampleController.java
@Controller
public class SampleController {
// 入力画面
@GetMapping("/input")
public String input() {
return "input";
}
// 書籍情報出力画面
@PostMapping("/bookinfo")
public String toBookInfo(@RequestParam Map<String, String> bookmap, Model model) {
model.addAttribute("bookmap", bookmap);
return "bookinfo";
}
}
bookinfo.html
<!-- bodyタブ内部のみ -->
<h3>Formクラスを消す</h3>
<label>タイトル</label><br>
<span th:text="${bookmap.title}"></span><br>
<label>値段</label><br>
<span th:text="${bookmap.price}"></span><br>
<label>メモ</label><br>
<span th:text="${bookmap.summary}"></span><br>
これでFormクラスを一切使わずに一般的な実装と同じ結果を得ることができます。
@RequestParam
でinput.htmlで入力した値をMapで受け取ると、bookinfoの中では{title=あいうえお, price=1000, summary=テスト}
の形で値が格納されます。この記事で何度も言っていたnameの値とvalueの値が紐づいて送られていることがよく分かるのではないでしょうか?
また、この実装のメリットは入力する値を可変にできることだったりします。
きっかけと補足
そもそも、「th:field
を消したい」っと思ったのは、可変な入力画面を作成する必要に迫られたからでした。
色々やった結論としては、th:field
は使えるんだったら積極的に使った方がいいし、@RequestParam
まで使ってFormクラスを消すべきはないと思っています。
@RequestParam
を使って入力値をMap
で受け取った実装だと、
戻るボタンがあったときの値の処理
エラーハンドリング(Spring Frameworkのアノテーションが当然使えないっ)
などなど大いに困ります。
今回の例についても、buttonに値を指定して受け取るくらいはしていいかもしれませんがFormクラスを入れ替えるをやると可読性は格段に落ちたものになると思います。
##まとめ
th:fieldはHTMLではnameとvalue(とid)属性を意味している
value属性の値はname属性に指定した値に紐づいて送られ、Controller側ではそれを受け取っている
以上です!
参考
Spring MVC populate @RequestParam Map
Spring MVC コントローラの引数
コメントにも記載ありますが、th:field
の振る舞いについては、こちらの記事がより詳しく説明してあります。
th:field と th:object によるフォームバインディング機能(inputタグ・基本入力系編)