どうも、おはこんばんは。
spring、thymeleafを使ったことがないメンバーでspringboot×thymeleafのプロジェクト開発をしています.
今回学んだことを忘れない内にThymeleaf.ver3.0.11(2018年10月29日)チュートリアルを参考に自分なりにまとめていきます。間違っている可能性もあるので気になる点等あればコメントください。質問あれば答えます!
ver3.0.12のリリースノートが2020年12月21日に出ていたので気になる人はこちらを。
https://www.thymeleaf.org/releasenotes.html#thymeleaf-3.0.12
まずコントローラーはこれを前提とします。
@controller
public class HelloController {
@GetMapping("/")
public String index(Model model){
model.addAttribute("message", "Hello");
return "index";
}
}
値の表示
<span th:text="${message}"></span>
Helloが表示される。
th:text
でテキストを表示できる。
htmlの属性にth:
を付ければ属性値でthymeleaf式が使えるようになる。
結合
<span th:text="${message} + ' Spring Boot'"></span>
Hello Spring Bootが表示される。
リテラル置換
<span th:text="|${message} Spring Boot|"></span>
Hello Spring Bootが表示される。
|・・・|
でそのまんま置換。結合するための「+」や「’」を省略できて、可読性が高いので好き。
一部分だけに使うこともできる。
th:text="|${message}| + 'Spring Boot'"
プロパティへのアクセス
Usersというbeanクラスに
idとnameプロパティが作られているとする。
コントローラーで渡せば画面からアクセスできる。
model.addAttribute("users",new Users());
<span th:text="${users.name}"></span>
UsersクラスのgetName()を呼び出している。
一応getterさえあればメンバ変数がなくても呼び出せるようだ。
キャメル型でないgetname()はダメでnot foundとエラーが出る。
lombokを導入していれば@Getterか@Dataを使えばgetterを省略できるからおすすめ。
選択変数式
<div th:object="${users}">
<span th:text="*{name}"></span>
<span th:text="*{id}"></span>
</div>
th:object
を使うことでそのブロック内でオブジェクトを省略できる。
*{・・}
でth:objectを参照する。
${users.name}⇒*{name}
となり、書くのが楽になる。
${・・}は変数式
*{・・}は選択変数式という。
ローカル変数の使用
<div th:object="${users}">
<div th:with="x='名前は',y=*{name}">
<span th:text="|${x}${y}です|"></span>
</div>
</div>
th:with
で変数を作成でき、ブロック内でのみ有効となる。
一度にx,yのように複数定義が可能。
spanタグでリテラル置換を使っているが、
th:withでも次のように使用できた(試したら動いた・・)。
th:with="x=|名前は*{name}です|"
算術演算子
<span th:text="1 + 1"></span>
2が表示される。
使用できる演算子:+、-、*、/、%
if文と比較演算子
<div th:if="${users.id} gt 2">
<span>Hello</span>
</div>
th:if
でif文をいつものように使える。
「>」は>
と記述必要があるが、
文字列エイリアスのgt
を使った方がシンプルだ。
使用できる演算子:gt(>)、lt(<)、ge(>=)、le(<=)、not(!)
if文と等価演算子
<div th:if="${users.id} == 2">
<span>Hello</span>
</div>
等価演算子に関しては比較演算子と違いそのまま使える。
使用できる演算子:eq(==)、neq/ne(!=)。
条件分岐について
1,2行目とも同じ意味である。
<div th:if="${users.id} == 2">
<div th:if="${users.id == 2}">
上記2行目のように{}で全体を囲っても書けるが、変数式と選択変数式を比較するときは、使う式をどちらかに揃える必要がある為注意が必要。
<div th:object="${users}">
<div th:with="x=1">
<!--OK-->
<!--変数式の{}の中に変数式-->
<div th:if="${users.id == x}">
<!--選択変数式の{}の中に数字-->
<div th:if="*{id == 1}">
<!--NG-->
<!--選択変数式の{}の中に変数式が存在する為エラーとなる-->
<div th:if="*{id == x}">
</div>
</div>
th:if
は真偽値条件のみを評価するわけではない。単一値だけの評価時は次の場合trueになる。
値がnullではない場合:
- booleanのtrue
- 0以外の数値
- 0以外の文字
- “false”でも“off”でも“no”でもない文字列
- 真偽値でも、数値でも、文字でも文字列でもない場合
(値がnullの場合はth:ifはfalseと評価します)
else
elseはthymeleafにないので
th:unless
を使うか演算子を反転させる。
<div th:if="${users.id} == 2">
<span>私だ</span>
</div>
<!--th:unlessでelseの代わり-->
<div th:unless="${users.id} == 2">
<span>私じゃない</span>
</div>
<!--演算子反転でelseの代わり-->
<div th:if="${users.id} != 2">
<span>私じゃない</span>
</div>
三項演算子
Usersクラスにboolean型のプロパティflagを作成したとする。
<!--(if) ? (true)-->
<span th:text="${users.flag} ? 'ONです'"></span>
<!--(if) ? (true) : (false)-->
<span th:text="${users.flag} ? 'ONです' : 'OFFです'"></span>
<!--(value) ?: (defaultvalue)-->
<span th:text="${users.name} ?: '設定されていません'"></span>
3つ目の式はデフォルト式といって
${users.name}がnullじゃなかったら${users.name}の値が表示され、
nullだったら「設定されていません」が表示される。
個人的にデフォルト式は、ローカル変数を設定したとき変数名が間違っていてもエラーがでないので少し気を付けないといけないきがす。
※検証wishでも使える。th:with="変数=条件? true : false"
処理なしトークン
<!--(value) ?: (defaultvalue)-->
<span th:text="${users.name} ? _">設定されていません</span>
通常タグ間の文字列よりth:textが優先されるので書いても意味がないが、
アンダースコア「_」を使うことで処理をなかったことにできる。
これをデフォルト式で使うと、${users.name}がnullならth:textをなかったことにして、「設定されていません」が表示される。
繰り返し文
Usersクラスにリンゴ、ミカン、バナナが格納された
配列fruitsが用意されていたとする。
<div th:object="${users}">
<div th:each="fruits :*{fruits}">
<span th:text="${fruits}"></span>
</div>
</div>
リンゴミカンバナナと表示される。
th:each
でループさせることが可能。つまりfor文と同じ。
式は拡張for文のようにth:each="反復変数名 :配列"
で書ける。
反復変数はブロック内でのみ有効なローカル変数。
繰り返しステータスの保持
ステータス変数はth:each属性の中で定義され、次のデータを保持している
- indexプロパティー:0から始まる現在のインデックス
- countプロパティー:1から始まる現在のインデックス
- sizeプロパティー:要素数
- currentプロパティー:現在の要素オブジェクト
- even/odd真偽値プロパティー:現在の繰り返し処理が、偶数か奇数か
- first真偽値プロパティー:現在の繰り返し処理が最初かどうか
- last真偽値プロパティー:現在の繰り返し処理が最後かどうか
<div th:object="${users}">
<div th:each="fruits ,stat :*{fruits}">
<span th:text="${fruits}"></span>
<span th:text="|${stat.count}回目|"></span>
</div>
</div>
th:each="反復変数,ステータス変数:配列"
で書ける。
プリプロセッシング
thymeleaf式にネストして式を書きたいとき、
先に中の式を評価する必要がある。
例えば、json等の多次元配列を表示するときに使ったりする。
<div th:object="${users}">
<div th:each="Data :*{Data}">
<span th:text="${Data[__${stat.index}__].id}"></span>
</div>
</div>
__${}__
の形をプリプロセッシング式という。
アンダースコア2つで囲むと先に評価してくれる。
もちろん選択変数式:*{}でも利用可能。
以下のように無意味に付けても問題はないが
二重で付けるとエラーになったので注意。
<!--無問題-->
<span th:text="__${message}__"></span>
<!--エラー-->
<span th:text="__${Data[__${stat.index}__].id}__">
switch
<div th:switch="${user.role}">
<p th:case="'admin'">administrator</p>
<p th:case="'manager'">manager</p>
<p th:case="*">どれでもない</p>
</div>
th:switch
を使う。
どれでもなければth:case="*"
になる。
特定の属性でthymeleafを使う
例えば、onclick="alert('値');"
という形で
thymeleafで値を取得したいとき。
<input type="button" th:attr="onclick='alert(\'' + ${message} + '\');'">
特定の属性にth:attr
を使えばthymeleafで記述できるが
th:attr="onclick='alert(\'' + ${message} + '\');'"
って複雑。ただのonclickでここまで複雑になるのか。。
「thymeleaf onclick」や動的で検索すると
この式が上位ヒットしてしまう。
ヒット記事:Thymeleaf3で属性値へ動的に値を埋め込み&テキストを追加したい場合
実は、もっと簡潔に書ける。
<input type="button" th:onclick="|alert('__${message}__')|">
th:
を付けるだけでthymeleafが使用可能となる。
それとリテラル置換と、
プリプロセッシングを使用している。
検証:値を${users.id}にすればプリプロしなくてもエラーは起こらないがgetterの戻り値が宣言時の型と違えばエラーとなる。
プリプロすれば自動変換してくれるのかエラーはなくなった。
個人的にとりあえずプリプロにしておけば良いと思う。
古い情報に注意:th:attrの形式からth:onclickだけ有効にしたパターンを記事で見るがthymeleaf.ver3以降ではエラーになるので注意。
th:onclick="'alert(\'' + ${message} + '\');'"
th:blockタグ
<!--ブラウザソースを確認すると<span>Hello</span>だけが出力されてる-->
<th:block th:if="*{id} == 2">
<span>Hello</span>
</th:block>
<!--ブラウザソースを確認するとHelloだけが出力されてる-->
<th:block th:text="Hello"></th:block>
th:block
タグは画面に出力されない。
htmlタグを使いたくないときに使用したりする。
アンエスケープテキスト
<span th:utext="${message}"></span>
th:text
だと「<」や「>」が「<」や「>」に置き換えられるため、scriptとして動作することを防ぐ。
th:utext
にすることでエスケープ(サニタイズ)を無効にできる。
※ただし、XSS(クロスサイトスクリプティング)に注意。
${message}の値がHello<br>Spring<br>Boot
なら<br>が有効となり改行される。
インライン化
<span th:text="${message}"></span>
<span>[[${message}]]</span>
[[・・・]]
で囲むことでタグ外に記述できる。
インライン化はデフォルトで有効の為、th:inline
は必要ない。(適当に必要と書いている記事もあるが個人的には不要なコードはただ邪魔である・・)。
個人的にはインラインの方が直感的で好き。
th:text
を含むタグの入れ子は解釈されなかったりと動きが読めないことがあるので好きじゃない。
アンエスケープ(th:utextの)場合は[(・・・)]
。
リンク
http://localhost:8080/data?id=(users.idの値)&name=ABC
のようなリンクを作成したいとき
<!--1.クエリパラメータを動的に-->
<a href="a.html"
th:href="@{http://localhost:8080/use/data(id=${users.id},name='ABC')}">リンク</a>
<!--2.さらにパスパラメータuseを動的にする-->
<a th:href="@{http://localhost:8080/{use}/data(id=${users.id},name='ABC',use=${use})}">リンク</a>
<!--3.さらにURLを省略する-->
<a th:href="@{/{use}/data(id=${users.id},name='ABC',use=${use})}">リンク</a>
-
href
を記載しておくとthymeleafが使えない環境で有効になる。なくても良い。
th:href
でthymeleafが使えるので値を動的にできる。 -
パスパラメータを動的にするにはクエリ部分に追加すれば良い。
パスパラメータを{}で囲い、パス=${値}で代入される。
{}で使用されたものはクエリには含まれない。
(クエリに含めず直接プリプロ式__${use}__
でも動いたので個人的にはこの方がわかりやすそう。) -
/
始まりでコンテキストパスまでを省略可能。
application.propertiesにserver.servlet.context-path=/api/v1
が設定されていれば 「http://localhost:8080/api/v1 」を省略する。
インクルード
外部ファイルを作成し共通化したいコードを使いまわす。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="header">
ヘッダー
</div>
<div th:fragment="footer">
フッター
</div>
</body>
</html>
<div th:insert="~{fragment::header}"></div>
<div th:insert="fragment::footer"></div>
<div>
<div>
ヘッダー
</div>
</div>
<div>
<div>
フッター
</div>
</div>
th:insert="~{ファイル名 :: th:fragmentの属性値}"
で
th:fragment
で作成した部分をインクルードできる。
・ファイル名はtemplateからのパス。
・~{}
を付けるかは任意(index.htmlの1行目参考)。
・内部ファイルを参照したい時は"::th:fragment属性値"
or"this::th:fragment属性値"
。
th:fragmentを使わない
th:fragment
を使わずcssセレクタのようにid,class属性で呼べる。
<div class="header" id="header">
ヘッダー
</div>
<div th:insert="::#header"></div>
<div th:insert="::.header"></div>
th:insert、th:replace、th:includeの違い
th:insert
、th:replace
、th:include
3つの類似するタグがある。
<span th:fragment="hello">こんにちは</span>
1<div th:insert="::hello"></div><!--divタグ内に挿入する-->
2<div th:replace="::hello"></div><!--divタグごと置換-->
3<div th:include="::hello"></div><!--divタグ内にコンテンツのみを挿入する-->
1<div>
<span>こんにちは</span>
</div>
2<span>こんにちは</span>
3<div>
こんにちは
</div>
th:insert
とth:include
の違いはコンテンツの外枠を含めるかどうか。
ここでいうhelloのあるspanタグのこと。
th:include
はver3.0より非推奨となった為、th:insert
が追加された経緯がある。th:insert
でコンテンツのみを挿入したい場合th:blockを使用すれば同じ結果になる。
<th:block th:fragment="hello">こんにちは</th:block>
fragmentへ引数を渡す
<div th:fragment="header(m)">
<p>[[${m}]]</p>
<p>[[${users.id}]]</p>
</div>
<div th:insert="fragment::header(${message})"></div>
参照先で親の変数も使用可能(fragment.htmlの2行目参考)。
fragmentへタグごと渡す
画面固有のタグごと渡せば簡単に外部ファイルベースで構築できる。
headタグ共通化のサンプル。
<head th:fragment="common_title(t)">
<meta charset="UTF-8">
<title th:replace="${t}">各画面のタイトル</title>
</head>
<head th:replace="fragment :: common_title(~{::title})">
<title>index画面</title>
</head>
index.htmlで画面固有の値のみを記述する。
ファイル名:: th:fragment属性値(~{::タグ名})
でタグごと渡せる。
th:fragment
でtitleを受け取り、th:replace
で置換する。
fragment.htmlで完成されたheadタグをindex.html上へ置換し完成。
ユーティリティオブジェクト
<!--現在日時の日付オブジェクトを作成する-->
<p th:text="${#dates.createNow()}"></p>
<!--リストが空かどうかをチェック-->
<p th:if="${#lists.isEmpty(users.list)}"></p>
<!--もちろん選択変数式でも記述可能-->
<p th:if="*{#lists.isEmpty(list)}"></p>
ユーティリティオブジェクトは紹介するにはボリュームが多すぎるので割愛します。
thymeleafコメント
<!--htmlのコメントブロック
パースされる為、内容が間違っていればエラーが発生する-->
<!-- <p th:text="${message}"></p> -->
<!--パーサーレベルのコメントブロック
パースされない為、内容が間違っていてもエラーは発生しない-->
<!--/* <p th:text="${message}"></p> */-->
<!--プロトタイプのみのコメントブロック
エディタ等のファイル上でのみコメントアウト。パース後アンコメントされ出力される-->
<!--/*/ <p th:text="${message}"></p> /*/-->
th:fieldについて
th:field
はbeanクラスのプロパティを設定してやるとid,name,th:valueに展開される。
input,select,option,textareaタグで使用できる。
<div th:object="${users}">
<input type="text" th:field="*{name}">
<input type="text" id="name" name="name" th:value="*{name}">
</div>
nameプロパティの初期値が空の場合。
出力結果:<input type="text" id="name" name="name" value="">
2行目、3行目は同意なので同じ出力結果となる。
th:field
使用時の注意点はnameとvalueはth:field
の値が優先される。
idのみ上書き可能。
<div th:object="${users}">
<input type="text" th:field="*{name}" th:value="あ" id="abc">
</div>
上記出力結果:<input type="text" id="abc" name="name" value="">
th:field
にはローカル変数を使用できない(おそらく...)。
よくあるエラーパターンはth:each
内で使おうとするとき。
<div th:object="${users}">
<div th:each="f ,stat :*{fruitsList}">
<input type="text" th:field="${f.name}">
</div>
</div>
反復変数fはローカル変数の為エラーになる。
th:field="*{fruitsList[__$stat.index__].name}"
or
th:field="${users.fruitsList[__$stat.index__].name}
にすれば良い。
またインデックスを含むときのth:field
のid、nameの展開形式が違う為注意。
<input ~ id="fruits0" name="fruits[0]"
ドロップダウンのサンプル
<div th:object="${users}">
<select name="fruits">
<option th:each="f :*{fruitsList}" th:value="${f}">[[${f}]]
</option>
</select>
</div>
<!--Java側で初期値を設定したい場合はfruitsに値を入れて
th:selected="*{fruits} == ${value}"とかにすれば良い。-->
<div th:object="${users}">
<input type="text" name="name" autocomplete="off" list="keywords">
<datalist id="keywords">
<option th:each="fruits :*{fruitsList}" th:value="${fruits}">[[${fruits}]]
</option>
</datalist>
</div>
JavaScriptでthymeleafを扱う
scriptタグ内でもthymeleafを使える。
<script th:inline="javascript">
var message = [[${message}]];
</script>
scriptタグにth:inline="javascript"
を追加すると
インライン式で簡単に値の取得が可能となる。
<script th:inline="javascript">
var message = /*[[${message}]]*/ "メッセージ";
</script>
html単体で表示するときはvar message = [[${message}]];
だとエラーが出るのでその対策。
thymeleaf使用時は[[${message}]]
、html単体などthymeleaf未使用時はメッセージ
が有効になる。
<script th:inline="javascript">
/*<![CDATA[*/
var message = [[${message}]];
/*]]>*/
</script>
xmlで記述するときはCDATAセレクションで囲えば良い。
html5しか使っていなければ関係はない。
(html5は21年に廃止されたので現在はHTML Living standardがデファクトスタンダードだが...)
<script th:inline="javascript">
[#th:block th:unless="${message}"]
var username = [[${message}]];
[/th:block]
</script>
thymeleaf式を書き込むこともできる。
開始タグは[#・・・]
で囲む。
終了タグは[/th:block]
を短縮して[/]
と記述することもできる。
Javaメソッドの呼び出し
Data Data = new Data(); //DataはgetAメソッドを持つとする。
model.addAttribute("Data",Data);
<p th:text="${Data.getA('a')}"></p>
オブジェクトにインスタンスを追加すれば所持するメソッドを使用可能。
@beanNameを使う例
Thymeleafは、@beanNameでBeanアクセスを許可するのでそれを使う。
@Configuration
public class Config {
@Bean
public Data Data() {
return new Data();
}
}
<p th:text="${@Data.getA('a')}"></p>
@BeanでDataクラスをSpringのDIコンテナ管理下に登録している。
なので、わざわざビューを返すときにオブジェクトへ追加する必要がなくなる。
@Configurationを付けるとSpring起動時に自動で探しにくる。
なので、独立したファイルでまとめて管理できる。
Beanにアクセスするときは${@Data.getA('a')}
のように@を付ける。
@Bean&@Configurationの代わりにクラスに@Componentを付けるだけでも良い。
使い分けは、ソースコードを手を入れられないサードパーティライブラリのクラスをDI対象にしたいときは@Componentを使えないので、@Bean&@Configurationを使うことになる。@Beanならメソッド単位でDI可能
参考
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html
https://casual-tech-note.hatenablog.com/entry/2018/10/10/224250