【Thymeleaf】POSTで一覧画面から詳細画面の遷移を行う【Spring Boot】

### はじめに
現場ではCSRF対策でトークンを使用するため、POSTで画面遷移を行う必要がありました。
[この記事](http://www.trust-service.co.jp/it/nomihodai/submit01/submit01.shtml)を参考に実装されたものを流用したのですが、今回Thymeleafで少し詰まってしまったのでまとめます。

#### バージョン
Spring Boot 2.7.3
Thymeleaf 3.0.15

### コード例
まずは動作した例です。
※全てのコードはこちらを参照→https://github.com/1noseA/mailnaxx

htmlでは、table td内のaタグに詳細画面遷移のJavaScript関数を入れています。
詳細画面遷移用のformは下に設置（一部抜粋のため省略していますが、formが入れ子にならないように）。
```list.html
// 一覧表示部分のみ抜粋
<div class="container">
    <div class="listArea mx-auto">
        <div class="table-responsive">
            <table class="table table-striped table-hover">
                <thead>
                    <tr class="border-bottom border-dark">
                        <th scope="col" class="text-canter">選択</th>
                        <th scope="col">社員番号</th>
                        <th scope="col">氏名</th>
                        <th scope="col">入社年月</th>
                        <th scope="col">所属</th>
                        <th scope="col">権限区分</th>
                        <th scope="col"><!-- 営業 --></th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>
                            <div class="form-check d-flex justify-content-center align-items-center">
                                <input class="form-check-input" type="checkbox" name="deleted_flg" value="1">
                                <input type="hidden" name="user_id" th:value="${userList.user_id}">
                            </div>
                        </td>
                        <td>
                            <a href="javascript:void(0);" th:onclick="showDetail('[[${userList.user_id}]]')">
                                <span th:text="${userList.user_number}"></span>
                            </a>
                        </td>
                        <td>
                            <a href="javascript:void(0);" th:onclick="showDetail('[[${userList.user_id}]]')">
                                <span th:text="${userList.user_name}"></span>
                            </a>
                        </td>
                        <td>
                            <a href="javascript:void(0);" th:onclick="showDetail('[[${userList.user_id}]]')">
                                <span th:text="${#strings.replace(userList.hire_date, '-', '/')}"></span>
                            </a>
                        </td>
                        <td>
                            <a href="javascript:void(0);" th:onclick="showDetail('[[${userList.user_id}]]')">
                                <span th:text="${userList.affiliation_id}"></span>
                            </a>
                        </td>
                        <td>
                            <a href="javascript:void(0);" th:onclick="showDetail('[[${userList.user_id}]]')">
                                <th:block th:each="roleClass : ${roleClassList}">
                                    <th:block th:if="${userList.role_class == roleClass.value}">
                                        <span th:text="${roleClass.viewName}"></span>
                                    </th:block>
                                </th:block>
                            </a>
                        </td>
                        <td>
                            <a href="javascript:void(0);" th:onclick="showDetail('[[${userList.user_id}]]')">
                                <span th:text="${userList.sales_flg == '1'} ? '営業' : ' '"></span>
                            </a>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</div>
<form method="post" th:action="@{/user/detail}" id="detailForm">
</form>
```

htmlの下に設置したformに対象のユーザIDをhiddenで入れ、submitしています。
```user.js
function showDetail(id) {
    let form = document.getElementById('detailForm');
    let input = document.createElement('input');
    input.setAttribute('type', 'hidden');
    input.setAttribute('name', 'user_id');
    input.setAttribute('value', id);
    form.appendChild(input);
    form.submit();
}
```

```UsersController.java
// 一部抜粋
// 詳細画面初期表示
@PostMapping("/user/detail")
public String detail(int user_id, Model model) {
    Users userInfo = usersMapper.findById(user_id);
    model.addAttribute("userInfo", userInfo);
    return "user/detail";
}
```

### 詰まったこと
①ThymeleafのJavaScript関数の引数にJavaの値を入れる書き方が分からない
jsのデバッグでそのまま文字列として表示されていました。
```list.html
　// JavaScript関数部分のみ抜粋
// NG例①
showDetail('${userList.user_id}')

// NG例②
showDetail('__${userList.user_id}__')

// OK例
showDetail('[[${userList.user_id}]]')
```

②aタグにJavaScript関数を入れる書き方が分からない
```list.html
　// NG例
<td>
    <a href="javascript:showDetail('[[${userList.user_id}]]')">
        <span th:text="${userList.user_name}"></span>
    </a>
</td>

　// OK例
<td>
    <a href="javascript:void(0);" th:onclick="showDetail('[[${userList.user_id}]]')">
        <span th:text="${userList.user_name}"></span>
    </a>
</td>
```

③403になってしまう
トークンのエラーと予想できました。
一覧画面では複数のformがある状態だったので、jsでformを作成しsubmitするようにしていました。
Spring Boot＋Thymeleafではform内の「th:action〜」をつけることでCSRFトークンを設定してくれるのだそうです。
jsでどうトークンを設定するのかは分からず、html上にformを作成することで対処しました。
```user.js
// NG例
function showDetail(id) {
    let form = document.createElement('form');
    form.setAttribute('action', '/user/detail');
    form.setAttribute('method', 'post');
    
    let input = document.createElement('input');
    input.setAttribute('type', 'hidden');
    input.setAttribute('name', 'user_id');
    input.setAttribute('value', id);
    form.appendChild(input);
    
    form.submit();
}
```

### 最後に
最近は現場でも家でもASP.NETをやっており、久しぶりのSpring BootでThymeleafの書き方どうだったっけという感じでした。
Controllerの書き方の違いはそんなに気にならないですが、htmlはテンプレートエンジンの書き方を調べながらになりますね。。

#### 参考
[Thymeleafでjavascriptの関数を呼び出したときに引数がうまく渡せない](https://teratail.com/questions/242117)
[Spring Boot + ThymeleafのCSRFトークン設定は超簡単](https://ti-tomo-knowledge.hatenablog.com/entry/2018/06/11/082319)