13.3 実践演習: TODO 管理アプリ

ここまで学んだ知識を総動員して、簡易的な TODO 管理アプリケーションを作成します。
このアプリでは以下の機能を実装します:

  1. TODO 一覧の表示
  2. 新しい TODO の追加
  3. 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: })