13.3 実践演習: TODO 管理アプリ
ここまで学んだ知識を総動員して、簡易的な TODO 管理アプリケーションを作成します。
このアプリでは以下の機能を実装します:
- TODO 一覧の表示
- 新しい TODO の追加
- TODO の削除
13.3.1 サーバーサイド側の実装
ソースフォルダ:src/main/java/
パッケージ名:servlet
ファイル名:Todo.java, TodoServlet.java
アクセス URL:http://localhost:8080/javascript_basic_webapi/api/todos
Todo.java
package servlet;
public class Todo {
private int id;
private String task;
private boolean completed;
public Todo(int id, String task, boolean completed) {
this.id = id;
this.task = task;
this.completed = completed;
}
public int getId() {
return id;
}
public String getTask() {
return task;
}
public boolean isCompleted() {
return completed;
}
}
TodoServlet.java
package servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import com.fasterxml.jackson.databind.ObjectMapper;
@WebServlet("/api/todos")
public class TodoServlet extends HttpServlet {
// GET:TODO一覧を取得
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession();
ArrayList<Todo> todoList = (ArrayList<Todo>) session.getAttribute("todoList");
if (todoList == null) {
todoList = new ArrayList<Todo>();
session.setAttribute("todoList", todoList);
}
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(todoList);
response.setContentType("application/json; charset=UTF-8");
PrintWriter out = response.getWriter();
out.print(json);
out.flush();
}
// POST:新しいTODOを追加
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String task = request.getParameter("task");
HttpSession session = request.getSession();
ArrayList<Todo> todoList = (ArrayList<Todo>) session.getAttribute("todoList");
if (todoList == null) {
todoList = new ArrayList<Todo>();
}
// 新しいIDを生成(リストのサイズ + 1)
int newId = todoList.size() + 1;
Todo newTodo = new Todo(newId, task, false);
todoList.add(newTodo);
session.setAttribute("todoList", todoList);
// 追加したTODOをJSONで返却
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(newTodo);
response.setContentType("application/json; charset=UTF-8");
PrintWriter out = response.getWriter();
out.print(json);
out.flush();
}
// DELETE:TODOを削除
protected void doDelete(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String idStr = request.getParameter("id");
int id = Integer.parseInt(idStr);
HttpSession session = request.getSession();
ArrayList<Todo> todoList = (ArrayList<Todo>) session.getAttribute("todoList");
if (todoList != null) {
// 通常のfor文で削除対象を探して削除
for (int i = 0; i < todoList.size(); i++) {
if (todoList.get(i).getId() == id) {
todoList.remove(i);
break;
}
}
session.setAttribute("todoList", todoList);
}
response.setContentType("application/json; charset=UTF-8");
PrintWriter out = response.getWriter();
out.print("{\"success\": true}");
out.flush();
}
}
解説
このサーブレットは、HTTP メソッドに応じて異なる処理を実行します。
- GET: TODO 一覧を取得
- POST: 新しい TODO を追加
- DELETE: TODO を削除
23 行目から 29 行目で、セッションから TODO リストを取得しています。 初回アクセス時はリストが存在しないため、新しい ArrayList を作成してセッションに保存します。
23: HttpSession session = request.getSession();
24: ArrayList<Todo> todoList = (ArrayList<Todo>) session.getAttribute("todoList");
26: if (todoList == null) {
27: todoList = new ArrayList<Todo>();
28: session.setAttribute("todoList", todoList);
29: }
24 行目、48 行目、79 行目では、セッションから取得したオブジェクトを ArrayList型にキャストしています。 このキャストにより、ArrayList 特有のメソッドを使用できます。
55 行目と 56 行目で、新しい TODO の ID を生成し、Todo オブジェクトを作成しています。 ここでは簡易的に、リストのサイズ + 1 を ID としています。
55: int newId = todoList.size() + 1;
56: Todo newTodo = new Todo(newId, task, false);
83 行目から 88 行目では、通常の for 文を使って指定された ID の TODO を削除しています。 該当の TODO が見つかったら削除してループを抜けます。
83: for (int i = 0; i < todoList.size(); i++) {
84: if (todoList.get(i).getId() == id) {
85: todoList.remove(i);
86: break;
87: }
88: }
13.3.2 フロントエンド側の実装
ソースフォルダ:src/main/webapp/
ファイル名:todo.html, js/todo.js
todo.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>TODO管理アプリ</title>
<style>
body { font-family: sans-serif; margin: 20px; }
input { padding: 5px; font-size: 16px; }
button { padding: 5px 10px; font-size: 16px; cursor: pointer; }
ul { list-style: none; padding: 0; }
li { padding: 10px; margin: 5px 0; border: 1px solid #ddd; }
.deleteBtn { margin-left: 10px; background-color: #f44336; color: white; }
</style>
</head>
<body>
<h1>TODO管理アプリ</h1>
<div>
<input type="text" id="taskInput" placeholder="タスクを入力">
<button id="addBtn">追加</button>
</div>
<ul id="todoList"></ul>
<script src="js/todo.js"></script>
</body>
</html>
js/todo.js
// todo.js
const CONTEXT_PATH = "/javascript_basic_webapi";
const taskInput = document.querySelector("#taskInput");
const addBtn = document.querySelector("#addBtn");
const todoList = document.querySelector("#todoList");
// ページ読み込み時にTODO一覧を取得
document.addEventListener("DOMContentLoaded", loadTodos);
// TODO一覧を取得して表示
function loadTodos() {
fetch(`${CONTEXT_PATH}/api/todos`)
.then(response => response.json())
.then(todos => {
displayTodos(todos);
})
.catch(error => {
console.error("取得エラー:", error);
});
}
// TODOを画面に表示
function displayTodos(todos) {
todoList.innerHTML = "";
todos.forEach(todo => {
const li = document.createElement("li");
const taskText = document.createElement("span");
taskText.textContent = todo.task;
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "削除";
deleteBtn.className = "deleteBtn";
deleteBtn.addEventListener("click", () => deleteTodo(todo.id));
li.appendChild(taskText);
li.appendChild(deleteBtn);
todoList.appendChild(li);
});
}
// 新しいTODOを追加
function addTodo() {
const task = taskInput.value.trim();
if (task === "") {
alert("タスクを入力してください");
return;
}
// POSTリクエストでサーバーに送信
fetch(`${CONTEXT_PATH}/api/todos`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "task=" + encodeURIComponent(task)
})
.then(response => response.json())
.then(newTodo => {
// 再読み込みして最新の一覧を表示
loadTodos();
taskInput.value = "";
})
.catch(error => {
console.error("追加エラー:", error);
});
}
// TODOを削除
function deleteTodo(id) {
fetch(`${CONTEXT_PATH}/api/todos?id=${id}`, {
method: "DELETE"
})
.then(response => response.json())
.then(result => {
// 再読み込みして最新の一覧を表示
loadTodos();
})
.catch(error => {
console.error("削除エラー:", error);
});
}
// 追加ボタンのイベント
addBtn.addEventListener("click", addTodo);
// Enterキーでも追加できるようにする
taskInput.addEventListener("keydown", function(event) {
if (event.key === "Enter") {
addTodo();
}
});
実行結果

解説
9 行目で、ページが読み込まれたときに loadTodos 関数を自動実行するようにしています。
9: document.addEventListener("DOMContentLoaded", loadTodos);
12 行目から 21 行目の loadTodos 関数では、GET リクエストでサーバーから TODO 一覧を取得し、displayTodos 関数に渡しています。
12: function loadTodos() {
13: fetch(`${CONTEXT_PATH}/api/todos`)
14: .then(response => response.json())
15: .then(todos => {
16: displayTodos(todos);
17: });
21: }
54 行目から 60 行目では、POST リクエストを送って新しい TODO を追加しています。
54: fetch(`${CONTEXT_PATH}/api/todos`, {
55: method: "POST",
56: headers: {
57: "Content-Type": "application/x-www-form-urlencoded"
58: },
59: body: "task=" + encodeURIComponent(task)
60: })
POST リクエストでは、method プロパティに”POST”を指定し、body プロパティに送信するデータを含めます。 今回はフォームデータ形式で送るため、Content-Type を application/x-www-form-urlencoded に設定しています。
58 行目の encodeURIComponent は、日本語などの特殊文字を URL エンコードする関数です
74 行目から 76 行目では、DELETE リクエストを送って TODO を削除しています。
74: fetch(`${CONTEXT_PATH}/api/todos?id=${id}`, {
75: method: "DELETE"
76: })
