この記事ではThymeleafの基本を説明します。Spring Boot入門:ThymeleafとControllerの続きです。
前回はThymeleafに文字列を出力するだけでしたが、今回はより複雑な機能を説明します。
まだ開発環境が整っていない場合、次の記事を参考にしてください。
この記事のソースコード
この記事のソースコードはGithubに公開しています。
GithubからSpring BootプロジェクトをEclipseにインポートする方法は次の記事を参考にしてください。
Spring Bootプロジェクトを作成
Spring Bootプロジェクトを作成します。基本的には前回と一緒ですが、依存関係にSpring Boot DevToolsを追加します。Spring Boot DevToolsを依存関係に追加すると、プロジェクトを再起動しなくてもソースコードの変更が反映されるので、便利です。
formに初期値を設定する方法
formから値を受け取る方法ではなく、formに初期値を設定する方法です。ControllerからThymeleafに値をわたして、それをformの各項目に初期値として表示します。
表示するデータをThymeleafに渡すControllerです。
package blog.tsuchiya.tutorial.step2.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import blog.tsuchiya.tutorial.step2.controller.form.TestForm;
@Controller
public class FormController {
@GetMapping("form/index")
public String formIndex(Model model) {
model.addAttribute("testForm", createForm());
return "form/index";
}
/**
* フォームに表示するTestFormを生成する。
* 色々変更して遊んでみると練習になるはず。
*
* @return テスト用TestForm
*/
private TestForm createForm() {
var result = new TestForm();
result.setText("テキストサンプル");
result.setTextarea("テキストエリア\nサンプル");
result.setSelect(2);
result.setRadio("radioTest");
result.setCheckBox("one");
return result;
}
/**
* 入力したフォームの内容をそのまま
* JSON形式で出力する。
* ResponseBodyアノテーションは戻り値をJSONで表示するようにする。
*
* @param testForm フォームからの入力
* @return 入力をそのまま返す
*/
@PostMapping("form/input")
@ResponseBody
public TestForm formInput(TestForm testForm) {
return testForm;
}
}
データの受け渡しに使うTestFormはこう。
package blog.tsuchiya.tutorial.step2.controller.form;
import lombok.Data;
/**
* formタグの入力に対応するDTO
*/
@Data
public class TestForm {
private String text;
private String textarea;
private Integer select;
private String radio;
private String checkBox;
}
どちらも難しいことは行っていません。もしわからなかったら、前回の記事を復習してみましょう。
やっていることは、TestFormに適当な値を設定して、Thymeleafに渡しているだけです。
TestFormを渡されるThymeleafの内容は以下の通りとなります。保存場所はsrc/main/resources/templates/form/index.htmlです。
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Formに初期値設定</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
<main class="container">
<!--/* フォームの説明の前にコメントのテスト */-->
<!-- <span th:text="test">Thymeleafとして解釈されないがソースに表示される</span> -->
<!--/* <span th:text="test">こちらは解釈もされず表示もされない</span> */-->
<h1>フォームに初期値設定</h1>
<section class="border p-1 mb-3">
<h2>th:objectを利用</h2>
<!--/*
th:objectでフォームオブジェクトを指定する方法。
この方法がメジャーでだいたいどこでもこっちを説明している。
*/-->
<form method="post" th:action="@{/form/input}"
th:object="${testForm}">
<div class="mb-3">
<!--/*
th:objectで指定したオブジェクトのフィールドを*{フィールド名}という形でth:fieldに指定。
th:fieldはid, name, valueをまとめて設定してくれる。
*/-->
<label for="text" class="form-label">テキスト</label> <input
type="text" class="form-control" th:field="*{text}">
</div>
<div class="mb-3">
<label for="textBox" class="form-label">テキストエリア</label>
<textarea class="form-control" th:field="*{textarea}"></textarea>
</div>
<div class="mb-3">
<!--/*
セレクトの場合はth:fieldを指定するとちゃんと
対応するオプションを初期選択してくれる。
*/-->
<label for="select" class="form-label">セレクト</label> <select
th:field="*{select}" class="form-control">
<option value="1">オプション1</option>
<option value="2">オプション2</option>
<option value="3">オプション3</option>
</select>
</div>
<div class="mb-3">
<label for="radio" class="form-label">ラジオボタン</label> <input
type="radio" th:field="*{radio}" value="radioTest"> <input
type="radio" th:field="*{radio}" value="radioTest2">
</div>
<div class="mb-3">
<label for="checkBox" class="form-label">チェックボックス</label> <input
type="checkbox" th:field="*{checkBox}" value="one">
</div>
<div>
<button type="submit" class="btn btn-primary">送信</button>
</div>
</form>
</section>
<section class="border p-1 mb-3">
<h2>直接変数名を指定</h2>
<!--/*
th:objectを利用しないで、変数名を利用する方法。こっちでも同じことができる。
*/-->
<form method="post" th:action="@{/form/input}">
<div class="mb-3">
<label for="text" class="form-label">テキスト</label> <input
type="text" class="form-control" th:field="${testForm.text}">
</div>
<div class="mb-3">
<label for="textBox" class="form-label">テキストエリア</label>
<textarea class="form-control" th:field="${testForm.textarea}"></textarea>
</div>
<div class="mb-3">
<label for="select" class="form-label">セレクト</label> <select
th:field="${testForm.select}" class="form-control">
<option value="1">オプション1</option>
<option value="2">オプション2</option>
<option value="3">オプション3</option>
</select>
</div>
<div class="mb-3">
<label for="radio" class="form-label">ラジオボタン</label> <input
type="radio" th:field="${testForm.radio}" value="radioTest">
<input type="radio" th:field="${testForm.radio}" value="radioTest2">
</div>
<div class="mb-3">
<label for="checkBox" class="form-label">チェックボックス</label> <input
type="checkbox" th:field="${testForm.checkBox}" value="one">
</div>
<div>
<button type="submit" class="btn btn-primary">送信</button>
</div>
</form>
</section>
</main>
</body>
</html>
こちらは色々説明が必要でしょう。
Spring Bootを起動してからhttp://localhost:8080/form/indexにブラウザでアクセスすると次のような画面が表示されるはずです。
この画面を見ながら説明を読んでいくのが良いと思います。
コメントの種類
ThymeleafにはHTMLとしてブラウザに出力されるコメントと、全く出力されないコメントがあります。
<!-- <span th:text="test">Thymeleafとして解釈されないがソースに表示される</span> -->
<!– –>形式のコメントは、ブラウザに出力されるコメントです。コメント内のThymeleaf属性は解釈されません。
<!--/* <span th:text="test">こちらは解釈もされず表示もされない</span> */-->
一方、<!–/* */–>のコメントはHTMLへの出力はされないコメントです。基本的にはこちらのコメントを使うことになるかと思います。
この部分をブラウザから確認すると、こんなふうになっているはずです。
ブラウザの画面を右クリックして、[ページのソースを表示](Chromeの場合、他のブラウザだとちょっと表現が違うはず)してみてください。
th:object=”${testForm}”
23行目にあるth:objectという属性を使うと、以降変数名を省略することができます。th:objectがあるタグの中では、*{フィールド名}という表現を使うと、${testForm.フィールド名}として評価さるわけです。
th:objectはよくfromタグとセットで使われることが多いですが、他のタグでも利用できます。ある変数名をいちいち書くのが面倒なときに使う属性です。
th:field
formタグを使う上で最も重要な属性です。この属性で指定したフィールドの値をinputタグなどに設定してくれます。
コメントでも書いてあるとおり、th:fieldはHTMLとしては以下のように展開されます。
<input type="text" class="form-control" id="text" name="text" value="テキストサンプル">
idとnameがth:fieldで指定したフィールド名に、valueがフィールドの内容に指定されるわけです。その結果、inputタグには初期値が設定されることになります。
selectタグでのth:field
selectタグでth:fieldを使うと、指定したフィールド名と同じvalueを持つoptionタグを初期値として選択します。今回のサンプルコードだとvalueが2のものが初期値です。
ラジオボタンでのth:field
ラジオボタンではth:fieldで指定したフィールドと同じvalueを持つものを選択済みとします。
もし対応するvalueがなかったら、初期状態では選択されるラジオボタンはありません。
チェックボックスでのth:field
ラジオボタンやselectタグのときと同じく、th:fieldで指定したフィールドと同じvalueを持っていたらチェック済みとなります。
65行目以降
65行目以降では、th:objectを使わないで各項目に初期値を設定しています。*{フィールド名}を使う代わりに、${testForm.フィールド名}を使っているだけです。どちらでも好みの方法を使えば良いでしょう。
制御文(if, unless, each)
Thymeleafでの制御文の説明です。これもまずはソースコードを見てください。
ControllerはThymeleafに適当な値を渡しているだけです。
package blog.tsuchiya.tutorial.step2.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class OtherController {
@GetMapping("other/index")
public String otherIndex(Model model) {
model.addAttribute("forEach", createList());
model.addAttribute("nullValue", null);
model.addAttribute("nonNullValue", "notNull");
return "other/index";
}
private List<String> createList() {
var result = new ArrayList<String>();
result.add("<span>test1</span>");
result.add("<span>test2</span>");
result.add("<span>test3 too long too show</span>");
result.add("<span>test4</span>");
return result;
}
}
Thymeleafでは渡された値を利用してif文などを使い表示を制御しています。保存場所はsrc/main/resources/templates/other/index.htmlです。
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>色々説明</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
<main class="container">
<section class="border p-1 m-1">
<h2>th:eachとth:if, th:utextのサンプル</h2>
<!--/*
th:each属性でListの繰り返しを行う。
statは省略可能。
*/-->
<th:block th:each="item, stat:${forEach}">
<!--/*
th:ifで条件判定。文字列の長さが20文字以下の
場合のみこのdivタグが表示される。
*/-->
<div class="border p-1 m-1" th:if="${item.length() <= 20}">
<p th:classappend="${stat.even} ? text-primary : text-success">
<!--/* th:eachのステータスを参照 */-->
<span th:text="${stat.index}">1</span>番目の要素
</p>
<!--/*
th:textを使うと文字列はサニタイズされる。
th:utextはサニタイズされない。
*/-->
<p>
th:textの場合:<span th:text="${item}">ダミー</span>
</p>
<p>
th:utextの場合:<span th:utext="${item}">ダミー</span>
</p>
</div>
</th:block>
</section>
<section class="border p-1 m-1">
<h2>th:unlessのサンプル</h2>
<!--/* th:ifはnullの場合はfalse、それ以外の場合trueと判定される。 */-->
<p th:if="${nullValue}">th:ifがnullの場合</p>
<p th:if="${nonNullValue}">th:ifが"notNull"の場合</p>
<!--/* th:unlessはth:ifの反対。falseだと表示される。 */-->
<p th:unless="${nullValue}">th:unlessがnullの場合</p>
<p th:unless="${nonNullValue}">th:unlessが"notNull"の場合</p>
</section>
</main>
</body>
</html>
それほど長くはないですが、色々やっていますので解説します。
こちらも画面を見ながらのほうが理解できると思うので、Spring Bootを起動してhttp://localhost:8080/other/indexをブラウザで表示してください。
th:each
Thymeleafでの繰り返し文にはth:each属性を使います。
<th:block th:each="item, stat:${forEach}">
この部分です。(th:blockタグは空っぽのタグ、解釈しても何も表示しないタグです。制御文をThymeleafで使う場合などに利用します)
th:each=”変数名, ステータス : ${ループ対象のコレクション}”
書式としてはこうなります。ステータスの部分は省略可能です。省略した場合は
th:each=”変数名 : ${ループ対象のコレクション}”
こうなります。
「ループ対象のコレクション」にはjava.util.List、java.util.Iterable、java.util.Map(変数にはjava.util.Map.Entryが格納される)が指定可能です。それ以外も一応指定できて、その要素のみを1回繰り返すループとして扱われます。
サンプルコードではforEach変数にStringを4個格納したListがあるので、4回繰り返します。(画面表示上3つしかないのは、後で説明するth:if文で表示していない項目があるからです)
ステータスの部分(サンプルだとstat)にはループに関する様々な情報が格納されていて、必要に応じて使うことが可能です。下のテーブルのような情報があります。
プロパティ | 概要 |
index | 0から始まる繰り返しの回数 |
count | 1から始まる繰り返しの回数 |
size | コレクションの数 |
current | 現在の繰り返し変数、今回の例だとitemと等価 |
even | 繰り返し処理が偶数回目ならtrue |
odd | 繰り返し処理が奇数回目ならtrue |
first | 最初の繰り返しならtrue |
last | 最後の繰り返しならtrue |
th:ifとth:unless
th:eachでの繰り返しのうち、3回目のループは画面に表示されませんでした。これは、23行目で行っている判定が原因です。
th:if=”${item.length() <= 20}”
ここで、itemに格納されている文字列の長さが20以下の場合のみ表示するという制御を行っていました。
3回目のループではitemに「”<span>test3 too long too show</span>”」という長い文字列が格納されます。そのため、20文字以下という条件を満たさずにth:ifがあるタグとその子孫のタグが表示されませんでした。
th:ifは指定した条件がtrueのとき、th:unlessはfalseのときだけそのタグと子孫のタグを表示します。
ただ、このtrueとfalseは若干面倒で、Boolean以外でも以下の条件を満たすとfalseとなります。
- nullの場合
- 数値で0の場合
- 文字列で0、false、no、offの場合
44行目からはth:ifとth:unlessのサンプルです。値がnullの場合はfalseとして、そうでない場合はtrueとして扱われているのがわかると思います。
テキスト出力
あるタグにテキストを表示したい場合はth:textという属性を利用しました。th:textは自動でサニタイズされるので、安全に文字列を使用できます。
しかし、文字列をサニタイズせず、タグをタグとして出力したい場合もあるでしょう。その場合はth:utextを使います。
サンプルコードの32行目から数行でどのようになるのかを確認できます。HTMLのソースを確認すれば、th:textを使った場合はタグがエスケープされていて、th:utextではエスケープされていないことがわかるでしょう。
属性の扱いとth:classappend
今回はあまり使いませんでしたが、属性の値を動的に変更したい場合はHTMLで利用可能な属性にth:をつけるとThymeleafで変数を指定可能な属性にできます。
th:actionなどがその参考です。大抵の属性はth:をつけることで${変数名}などが利用可能になり、値を動的に変更できます。
また、条件に応じてクラスを変更できるのがth:classappend属性です。多くの場合三項演算子を利用して、条件を満たした場合のみクラスを追加するなどの扱いをします。
今回のサンプルコードだと24行目で利用しています。
<p th:classappend="${stat.even} ? text-primary : text-success">
th:eachのステータスを利用して、ループが偶数回目ならtext-primaryを、そうでなければtext-successをクラスとして追加するようにしました。
まとめ
Thymeleafの基本的な使い方を説明しました。このページの内容を理解できていれば、基本的なWebアプリケーションを作成可能になっているはずです。
サンプルコードをGithubに公開してあるので、色々触ってみて理解を深めるのも良いかと思います。
わかりにくところなどがありましたら、Twitterやお問い合わせフォームで質問してください。あなたの質問が、このページをより良いものにしてくれます。
Spring Boot入門:Thymeleafのファイル分割に続きます。
コメント