#author("2020-07-03T04:31:18+00:00","","")
#author("2021-04-28T02:41:07+00:00","","")
[[SpringBoot]]

* Thymeleaf を使ったView層の開発 [#ee213c4b]

2021-04-05 中田 コントローラクラスのテンプレを追記

2020-06-21 中村

Thymeleafは,SpringBootで推奨されている''テンプレートエンジン''である.~
サーバサイドで画面 (View層)を生成することができる.


REST-APIを呼び出して,JavaScriptで動的に画面を書き換えるやり方とは~
アプローチが異なることに注意.
- こちらはクライアントサイドで画面を生成するやり方
- VueやAngularもこちらのアプローチになる.

** テンプレートエンジンとは [#d5922d83]
アプリケーションは,あらかじめ画面のひな型となるHTML~
(''テンプレート'')を用意しておく.

テンプレートは,ところどころ穴あき(変数)のHTMLになっており,~
@ControllerがDTOとテンプレートを指定すれば,ThymeleafがDTOの~
フィールド値をテンプレートの変数に代入して穴埋めし,完全なHTML~
にレンダリングしてブラウザに返却する.

** Thymeleaf の概要 [#xe97162e]

&attachref(thymeleaf.png);

@Controllerオブジェクト(@RestControllerではない)が処理結果を~
含んだDTOと画面のひな型となるHTMLテンプレートを指示すれば,~
ThymeleafがDTOの値をテンプレートの指定された場所に組み込んで~
レンダリングする.

テンプレートは,Spring Boot プロジェクトのresources/templates/ ~
の下に,Thymeleafの変数を埋め込んだHTML形式で保存する.

なお,Spring Boot プロジェクトは,変数を含まないHTML(静的なWeb~
ページ)を持つこともでき,その場合には,resources/static/ の下に~
保存する.

** 1. 準備 [#l42d9e14]

*** build.gradle に依存性を確認 [#v54a471d]
- spring initializrでthymeleafを選択している場合は自動で入っている.
- 無い場合はbuild.gradle のdependenciesに以下を追記.
*** build.gradle に依存性を追加 [#v54a471d]
build.gradle のdependenciesに以下を追記.

 implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'


[[SpringBoot/依存]]を参考にして,他のライブラリとの前後関係に注意すること.

** 2. 画面設計 [#e005334d]
*** 2.1 画面と画面遷移 [#m0ad9a4c]
コーディングする前に,まずは画面と画面遷移を大まかでよいので,
ノート等にスケッチしておくとよいだろう.

画面
- どんな画面があるか?
- それぞれのユースケースごとに画面があるはず

画面遷移
- 画面間の遷移はどうなっているか?


画面遷移の処理の流れを,最初の図を参照しながら,確認しよう

&attachref(./thymeleaf.png,50%);

+ ユーザがブラウザ上のボタンを押す
+ ControllerにGETまたはPOSTが発行される
+ ControllerがServiceを経由してビジネスロジックを実行し,結果を得る
+ Controllerが結果をModelに属性としてセットする.addAttribute() 
+ Controllerがreturnでテンプレートを''文字列''で指定する(例えばhogeとする)
+ Thymeleafがtemplatesの中からテンプレート''文字列.html'' (hoge.html)を探しだす.
+ Thymeleafがhoge.htmlにModelの属性値を流しこみ完全なHTMLを生成して,クライアントに返す
+ ユーザのブラウザ上に画面が現れる

*** 2.2 例題:健康診断Webアプリ [#mcfa36c0]
健康診断アプリ([[ソフトウェア工学第4回課題:http://www27.cs.kobe-u.ac.jp/~masa-n/lecture/newse/04/enshuu04.html]])の画面設計を行う
- エンティティは[[SpringBoot/JPA]]を参照のこと

*** 画面の例 [#k9142c95]

&attachref(screens.png);

*** 画面遷移の例 [#vf8a3232]
&attachref(transition.png);

- &color(red){赤字}; は画面に対応するURLパスを表す
- 画面間の遷移のラベルは,[ボタン・リンク押下] / [コントローラへのアクション] を表している
- redirect:/persons は,アクション終了後にリダイレクトを行う

** 3. @Controller クラスの構成 [#mffd1943]
サーバサイドで画面を構成する場合,@RestController クラスではなく,~
@Controller クラスを定義する (両方あっても構わないが,URLパスが~
重ならないように気を付けること)

PersonController.java
 @Controller
 public class PersonController {
     @Autowired
     PersonManager pm; //サービスクラス
 
   (以降メソッド定義)
 
 }

各メソッドの定義について,2.2の例題にそって説明する.

*** 3.1 受診者リストの表示 (GET /persons) [#td8773fc]
登録されているすべての受診者のデータを画面に表示する
+ Serviceに問い合わせて,全受診者のリストを取得し personListとする
+ 画面モデルに,属性plistを定義し,personListを紐づける
+ テンプレート list.html を呼び出す.

 @GetMapping("/persons")
 public String showAllPersons(Model model) {
     List<PersonDto> personList = pm.getAllPersons();
     model.addAttribute("plist", personList);
     return "list";
 }

*** 3.2 登録フォームの表示 (GET /persons/register) [#pe4dff18]
登録フォームを表示する
+ 空のフォームオブジェクトPersonFormを作成する
+ 画面モデルに,属性PersonFormを定義し,オブジェクトを紐づける
+ テンプレート register.html を呼び出す.

 @GetMapping("/persons/register")
 public String showPersonForm(Model model) {
   model.addAttribute("PersonForm", new PersonForm());
     return "register";
 }

*** 3.3 受診者情報の登録 (POST /persons) [#a9e3d4cb]
受診者情報をフォームから受け付けてDBに追加する.その後,一覧に戻る
+ ページからPOSTされた属性と値のペアを,@ModelAttribute のformに受け取る
+ @Vaidatedでバリデーションする
+ エンティティに変換して,登録サービスに渡す addPerson()
+ /personsにリダイレクトし,一覧ページを表示する

 @PostMapping("/persons")
 public String addPerson(@ModelAttribute @Validated PersonForm form, BindingResult result, Model model) {
     pm.addPerson(form.toEntity());
     return "redirect:/persons";
 }

*** 3.4 更新フォームの表示 (GET /persons/{number}/update) [#xda4e9a8]
更新フォームを表示する
+ パス・パラメータnumberから番号がnumberの受診者pを検索する
+ 画面モデルに,属性PersonFormを定義し,pを紐づける
+ テンプレート update.html を呼び出す

 @GetMapping("/persons/{number}/update")
 public String showUpdateForm(@PathVariable Long number, Model model) {
     PersonDto p = pm.getPerson(number);
     model.addAttribute("PersonForm", p);
     return "update";
 }

*** 3.5 受診者情報を更新する (POST /persons/{number}/update) [#t2789157]
画面で更新された受診者情報をフォームから受け付けてDBに追加する.~その後,一覧に戻る
- 注意: 更新なのでPUTで呼び出したい所だが,htmlの<form>ではPOST,GETしか指定できないのであえてPOSTで.

+ ページからPOSTされた属性と値のペアを,@ModelAttribute のformに受け取る
+ @Vaidatedでバリデーションする
+ パスパラメータから,受診者番号numberを取得する
+ formをエンティティに変換して,受診者番号numberとともに更新サービスに渡す updatePerson()
+ /personsにリダイレクトし,一覧ページを表示する


 @PostMapping("/persons/{number}/update")
 public String updatePerson(@ModelAttribute @Validated PersonForm form, @PathVariable Long number, Model model) {
     pm.updatePerson(number, form.toEntity());
     return "redirect:/persons";
 }

*** 3.6 受診者情報を削除する (GET /persons/{number}/delete) [#p17e6509]
受診者情報を削除する (確認ルーチンは省略している)
- 注意: 削除なのでDELETEで呼び出したい所だが,クリックで簡単に削除したいのでGETで.

+ パスパラメータから,受診者情報numberを取得する
+ 削除サービスを呼び出す deletePerson
+ /personsにリダイレクトし,一覧ページを表示する

 @GetMapping("/persons/{number}/delete")
 public String deletePerson(@PathVariable Long number, Model model) {
     pm.deletePerson(number);
     
     return "redirect:/persons";
 }

** 4. HTMLテンプレートを構成する [#p57092b5]

*** 4.1 扉ページ (static/index.html) [#g69a446e]
何の変哲もない,普通のHTML.ボタンを押すとpersonsに移動(GET)する

 <!DOCTYPE html>
 <html lang="ja">
 
 <head>
     <title>健康診断Webアプリケーション</title>
     <link rel="stylesheet" type="text/css" href="style.css">
     <meta charset="UTF-8">
 </head>
 
 <body>
     <h1>健康診断Webアプリケーション</h1>
     <div class="title">
         <img src="img/shinchou.png" width="250px">
         <img src="img/taijuu.png" width="250px">
     </div>
     <div>
         <input type="button" class="simple_square_btn5" onclick="location.href='persons'" value="受信者一覧へ" />
     </div>
 </body>
 
 </html>

*** 4.2 受診者一覧 (templates/list.html) [#u81ade37]
Thymeleafのテンプレート.重要なポイントが含まれる

 <!DOCTYPE html>
 <html lang="ja" xmlns:th="http://www.thymeleaf.org">
 
 <head>
     <meta charset="UTF-8">
     <title>受診者一覧</title>
     <link rel="stylesheet" type="text/css" th:href="@{/style.css}">
 </head>
 
 <body>
     <h1>受診者一覧</h1>
     <input type="button" class="simple_square_btn5" th:onclick="|location.href='@{/persons/register}'|" value="新規受診者登録" />
     <input type="button" class="simple_square_btn5" th:onclick="|location.href='@{/}'|" value="タイトル画面へ" />
     <table class="tablist">
         <tr>
             <th>No.</th>
             <th>受診者氏名</th>
             <th>生年月日</th>
             <th>身長</th>
             <th>体重</th>
             <th>BMI</th>
             <th>肥満度</th>
             <th>コマンド</th>
         </tr>
         <tr th:each="p: ${plist}">
             <td> [[${p.number}]] </td>
             <td> [[${p.name}]] </td>
             <td> [[${p.birthday}]] </td>
             <td> [[${p.height}]] cm </td>
             <td> [[${p.weight}]] kg </td>
             <td> [[${#numbers.formatDecimal(p.bmi == null ? 0 : p.bmi, 0, 1)}]] </td>
 
             <th:block th:switch="${p.obesity}">
                 <td th:case="'低体重'" style="background-color: #7c7cfc;">[[${p.obesity}]]</td>
                 <td th:case="'普通体重'" style="background-color: #7cfc7c;">[[${p.obesity}]]</td>
                 <td th:case="'肥満 (1度)'" style="background-color: #fcfc7c;">[[${p.obesity}]]</td>
                 <td th:case="'肥満 (2度)'" style="background-color: #fcbb91;">[[${p.obesity}]]</td>
                 <td th:case="'肥満 (3度)'" style="background-color: #fc9191;">[[${p.obesity}]]</td>
                 <td th:case="'肥満 (4度)'" style="background-color: #c08080;">[[${p.obesity}]]</td>
                 <td th:case="*">[[${p.obesity}]]</td>
             </th:block>
 
             <td>
                 <a th:href=@{/persons/{num}/update(num=${p.number})}>編集</a> |
                 <a th:href=@{/persons/{num}/delete(num=${p.number})}>削除</a>
             </td>
         </tr>
     </table>
 </body>

*** 解説 [#x1a6b6bd]
- ''<html>タグ:'' xmlns:th="http://www.thymeleaf.org" の指定
-- Thymeleafの名前空間 th を定義する.これは必須のおまじない.
- ''th: がついている部分''は,Thymeleafが動的に書き換える部分
-- (テンプレート) th:属性名 = "[[Thymeleaf式:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html#%E3%82%B9%E3%82%BF%E3%83%B3%E3%83%80%E3%83%BC%E3%83%89%E5%BC%8F%E6%A7%8B%E6%96%87]]" と書いておくと...
-- (実行時) 属性名 = [[Thymeleaf式:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html#%E3%82%B9%E3%82%BF%E3%83%B3%E3%83%80%E3%83%BC%E3%83%89%E5%BC%8F%E6%A7%8B%E6%96%87]]が評価された値 に展開される
- ''変数式:'' Contollerから渡された変数の値を取り出す
-- <td th:text="${p.name}"></td> と書いておくと (pはControllerから渡されたPersonオブジェクト),
-- <td>仲村 匡日出</td> と展開される.
- ''インライン式:'' 式をhtmlの中に直接書く
-- <td> [ [${p.name}] ]</td>
- ''リンク式:'' リンクのコンテキストパスをあんじょうやってくれる
-- パス・パラメータ{}は()の中で指定する(C言語のprintf()的な感じ)
-- <a th:href="@{/persons/{num}/update(num=${p.number})}">編集</a> と書いておくと
-- <a href="/person/1/update">編集</a> と展開される
- ''リテラル置換'': |・・・| で囲まれた部分を文字列に展開する
- ''三項演算子式:'' 条件に応じて,値を切り替える
-- 式1 ? 値1: 値2
-- 式1がtrueの場合,値1に評価される.falseの場合,値2に評価される
- ''ブロック''
-- <th:block> </th:block>でテンプレート内のブロックを定義できる
-- テンプレートを構造化する用途に使う→(繰り返し,分岐,スコープ制限など) 
-- 実行時には消える
- ''繰り返し'' : リストから一つずつ取り出して表示
-- <タグ th:each="p: ${plist}"> ... </タグ> と書いておくと,
-- plistから要素p0,p1,...,pnを一つずつ取り出して,
-- <タグ> p_0を展開した内容 </タグ> <タグ> p_1を展開した内容</タグ>...<タグ> p_nを展開した内容</タグ> に展開する
- ''条件'': 式が成立した時だけ表示
-- <タグ th:if="式">式がtrueのときだけこの内容が出ます</タグ>
-- <タグ th:unless="式">式がfalseのときだけこの内容が出ます</タグ>
- ''スイッチ'': 式の値によって分岐
-- <th:block th:switch="式1">
---  <タグ th:case="式2">式1==式2の時だけこの内容が出ます</タグ>

** 4.3 受診者登録 (templates/register.html) [#ma1e1871]
登録フォーム.

 <!DOCTYPE html>
 <html lang="ja" xmlns:th="http://www.thymeleaf.org">
 
 <head>
     <meta charset="UTF-8">
     <title>受診者情報登録</title>
     <link rel="stylesheet" type="text/css" th:href="@{/style.css}">
 </head>
 
 <body>
     <h1>受診者情報登録</h1>
 
     <form role="form" th:action="@{/persons}" th:object="${PersonForm}" method="post">
         <table class="tabform large">
             <tr>
                 <td><label for="number">No: </label></td>
                 <td><span th:text="${number}"></span></td>
             </tr>
             <tr>
                 <td><label for="name">氏名: </label></td>
                 <td><input class="large" type="text" required th:field="*{name}" /></td>
             </tr>
             <tr>
                 <td><label for="birthday">生年月日:</label></td>
                 <td><input class="large" type="text" required th:field="*{birthday}" /></td>
             </tr>
             <tr>
                 <td><label for="height">身長:</label></td>
                 <td><input class="large" type="number" required min="0" value="160" step=".1" th:field="*{height}" /> cm</td>
             </tr>
             <tr>
                 <td><label for="weight">体重:</label></td>
                 <td><input class="large" type="number" required min="0" value="60" step=".1" th:field="*{weight}" /> kg</td>
             </tr>
             <tr>
                 <td colspan="2">
                     <input type="button" class="large simple_square_btn5" onclick="history.back()" value="戻る" />
                     <input type="submit" class="large simple_square_btn5" value="登録する" />
                 </td>
             </tr>
         </table>
     </form>
 
 </body>
 
 </html>
 
*** 解説 [#a467ef52]
- Controllerから空のフォームを''PersonForm''という属性名で受け取っている
- <form>の中のth:object="${PersonForm}" で,このフォーム内で参照するオブジェクトを指定
- <input>の中の"*{name}"は,現在参照しているオブジェクトのnameメンバを参照するという意味
-- "${PersonForm.name}" と同じ意味
- <form ... th:object="${フォーム変数}"> ~<input ... th:field=*{属性名}/> 
-- <input>の中に,id, name, valueを勝手に展開してくれる
-- [[フォームバインディング:https://casual-tech-note.hatenablog.com/entry/2018/10/10/224250]]といって,
Thymeleaf でフォームを書く時の鉄板らしい
- 最下部の<input type="submit" ..> ボタンを押すと入力が<form>の定義にしたがって情報がPOSTされる
-- メソッドはPOST /persons
-- 送信されるデータは,各inputのnameとvalueのペア
- 3.3 のコントローラに渡され,登録後,一覧にリダイレクトされる

** 4.4 受診者更新 (templates/update.html) [#oa7db494]
4.3とほぼ同じなので割愛

** 完全なコード [#y2d30352]
- GitLab: Medical CheckApp
-- http://teamserv.cs.kobe-u.ac.jp:443/masa-n/medicalcheckapplication
-- Zipアーカイブ &attachref(medicalcheckapplication-master.zip);

** 参考文献 [#eb8d78c6]
- Thymeleaf チュートリアル
-- https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html

- Spring Boot で Thymeleaf 使い方メモ
-- https://qiita.com/opengl-8080/items/eb3bf3b5301bae398cc2

- [[Spring Boot 2実践入門:簡単なWEBアプリを一から作成チュートリアル:https://www.microstone.info/spring-boot-2%E5%AE%9F%E8%B7%B5%E5%85%A5%E9%96%80%EF%BC%9A%E7%B0%A1%E5%8D%98%E3%81%AAweb%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E4%B8%80%E3%81%8B%E3%82%89%E4%BD%9C%E6%88%90%E3%83%81%E3%83%A5%E3%83%BC/]]

- 【Spring Boot】ModelクラスとModel And Viewクラス
-- https://pointsandlines.jp/java/model-and-view

- Thymeleafを使用した入力フォームのサンプルコード
-- https://qiita.com/rubytomato@github/items/387d46ea34eb92071065

** コントローラクラステンプレ(by中田) [#ld3fde9d]

 @Controller
 class MyController {
   
   @GetMapping("/page")
   public String getSomething(
     Model model,
     RedirectAttributes attributes,
     @ModelAttribute
     @Validated
       MyForm form,
     BindingResult bindingResult){
     
     if(bindingResult.hasErrors()){
       attributes.addFlashAttribute("isError", true);
       attributes.addFlashAttribute("arg", "hoge");
       return "redirect:/";
     }
     
     model.addAttribute("arg", "fuga");
     return "next";
   }
 }

- @Controller
-- ThymeleafのControllerクラスにつけるアノテーション
-- コンポーネントスキャンの対象となる
- Model
-- Viewで使える変数
-- addAttribute : Viewで使える変数を設定する
- RedirectAttributes
-- リダイレクトに使う
-- addAttribute : リダイレクト先に渡す引数の設定
-- addFlashAttribute : リダイレクト先でmodel.addAttributeを実行し、値が消える(flash)
- @ModelAttribute
-- リクエストボディの内容を受け取る
- @Validated
-- バリデーションを有効化する
- BindingResult
-- 直前の引数のバリデーション結果を格納する
- return
-- "page"だと、page.htmlを返す
-- redirect : 指定したURLに移動して読み込み直す


トップ   編集 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS