開発準備
Spring Inirializrで以下のプロジェクトを作る。
- ビルドツール: Gradle
- 言語: Kotlin
- Spring Bootのバージョン: 1.5.7
- Group: com.example
- Artifact: todolist
- Dependencies: Web, Thymeleaf
タスクの一覧表示
タスクのデータクラスを作成
package com.example.todolist
data class Task(
val id: Long,
val content: String,
val done: Boolean
)
コントローラーの作成
ダミータスクをプレーンテキストを返すコントローラー
package com.example.todolist
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("tasks")
class TaskController {
@GetMapping
fun index(model: Model): String {
val tasks = listOf(
Task(1, "Test Task1", false),
Task(2, "Test Task2", true),
Task(3, "Test Task3", false)
)
return tasks.toString()
}
}
@RequestMapping("tasks")の指定により、このクラスに定義するメソッドへのパスは"/tasks"から始まることを設定している。
@GetMapping("")の指定があるため、/tasksというパスへのGETリクエストに反応する。
動作確認
以下のコマンドでアプリケーションを起動。
./gradlew bootRun
以下のURLにブラウザにアクセス。
http://localhost:8080/tasks
ブラウザに以下の文字列が表示される。
[Task(id=1, content=Test Task1, done=false), Task(id=2, content=Test Task2, done=true), Task(id=3, content=Test Task3, done=false)]
タスク一覧をHTMLで返す
Controllerの修正
package com.example.todolist
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
@Controller
@RequestMapping("tasks")
class TaskController {
@GetMapping
fun index(model: Model): String {
val tasks = listOf(
Task(1, "Test Task1", false),
Task(2, "Test Task2", true),
Task(3, "Test Task3", false)
)
model.addAttribute("tasks", tasks)
return "tasks/index"
}
}
修正点
- importとアノテーションをRestControllerからControllerに変更
- model.addAttributeでテンプレートにデータを引き渡す処理を追加
- returnはテンプレートの名称を返すように変更
テンプレートの追加
- Thymeleafというテンプレートエンジンを使用
- テンプレートファイルはsrc/main/resources/templates/に配置する
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>タスク一覧</title>
</head>
<body>
<ul th:unless="${tasks.isEmpty()}">
<li th:each="task: ${tasks}">
<span th:unless="${task.done}" th:text="${task.content}"></span>
<s th:if="${task.done}" th:text="${task.content}"></s>
</li>
</ul>
</body>
</html>
- Thymeleafのテンプレート要素はth:から始まる属性をつける
- th:if は条件が当てはまるときに表示する
- th:unless は th:if の逆
- th:each は 要素の繰り返し
- th:text は要素の中身を出力する
動作確認
ブラウザで以下のURLにアクセスする。
http://localhost:8080/task
ブラウザに以下が表示される
- Test Task1
Test Task2- Test Task3
保存されているタスクを表示するように
TaskRepositoryインターフェースの定義
package com.example.todolist
interface TaskRepository {
fun create(content: String): Task
fun update(task: Task)
fun findAll(): List<Task>
fun findById(id: Long): Task?
}
メモリ上(変数)にタスクを保存するTaskRepositoryの実装をする。
package com.example.todolist
import org.springframework.stereotype.Repository
@Repository
class InMemoryTaskRepository : TaskRepository {
private val tasks: MutableList<Task> = mutableListOf()
private val maxId: Long
get() = tasks.map(Task::id).max() ?: 0
override fun create(content: String): Task {
val id = maxId + 1
val task = Task(id, content, false)
tasks += task
return task
}
override fun update(task: Task) {
tasks.replaceAll{ t ->
if (t.id == task.id) task
else t
}
}
override fun findAll(): List<Task> = tasks.toList()
override fun findById(id: Long): Task? = tasks.find { it.id == id }
}
上記を使うためにControllerの修正を行う。
package com.example.todolist
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
@Controller
@RequestMapping("tasks")
class TaskController(private val taskRepository: TaskRepository) {
@GetMapping
fun index(model: Model): String {
val tasks = taskRepository.findAll()
model.addAttribute("tasks", tasks)
return "tasks/index"
}
}
テンプレートを修正し、タスクがない旨を表す。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>タスク一覧</title>
</head>
<body>
<p th:if="${tasks.isEmpty()}">タスクがありません。</p>
<ul th:unless="${tasks.isEmpty()}">
<li th:each="task: ${tasks}">
<span th:unless="${task.done}" th:text="${task.content}"></span>
<s th:if="${task.done}" th:text="${task.content}"></s>
</li>
</ul>
</body>
</html>
※ pタグを追加
タスク新規作成
タスクを作成する機能を実装する。
フォームクラス作成
こういうクラスをフォームクラスと呼ぶらしい。
Symfonyのformhelperみたいなもんか?
package com.example.todolist
import org.hibernate.validator.constraints.NotBlank
import javax.validation.constraints.Size
class TaskCreateForm {
@NotBlank
@Size(max = 20)
var content: String? = null
}
コントローラの修正
importの追加
```kotlin:TaskController.kt
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.servlet.mvc.support.RedirectAttributes
import org.springframework.validation.BindingResult
import org.springframework.validation.annotation.Validated
tasks/newページの作成
```kotlin:TaskController.kt
// 以下のメソッドを追加
@GetMapping("new")
fun new(form: TaskCreateForm): String {
return "tasks/new"
}
タスクを新規作成する処理を追加
// 以下のメソッドを追加
@PostMapping("")
fun create(@Validated form: TaskCreateForm, bindingResult: BindingResult): String {
if (bindingResult.hasErrors()) {
return "tasks/new"
}
val content = requireNotNull(form.content)
taskRepository.create(content)
return "redirect:/tasks"
}
テンプレート追加/修正
tasks/new.htmlの追加
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>タスク作成</title>
</head>
<body>
<form th:method="post" th:action="@{./}" th:object="${taskCreateForm}">
<div>
<label>
内容: <input type="text" th:field="*{content}" />
</label>
<p th:if="${#fields.hasErrors('content')}" th:errors="*{content}">エラーメッセージ</p>
</div>
<div>
<input type="submit" value="作成" />
</div>
</form>
</body>
</html>
tasks/index.htmlの修正
<!-- 次の行を追加 -->
<a th:href="@{tasks/new}">作成</a>