【デイトラ学習記録】JavaScriptを用いたTODOアプリの実装実演 – フロントエンド開発編

基礎学習編のまとめとして、TODOアプリを作っていきます。

開発リポジトリ

TODOアプリの開発リポジトリです。

①入力欄を作成して、console.logで動作テスト

HTMLでTODO入力欄の型を作る

HTML
<div class="todo-items">
  <div class="todo-item">
    <label for="new-todo-name">TODO名</label>
    <input
      type="text"
      id="new-todo-name"
      placeholder="TODO名を入力"
    />
  </div>
  <div class="todo-item">
    <label for="new-person">担当者</label>
    <input type="text" id="new-person" placeholder="担当者をを入力" />
  </div>
  <div class="todo-item">
    <label for="new-deadline">TODO名</label>
    <input type="date" id="new-deadline" />
  </div>
  <div class="todo-buton">
    <button id="button-register">登録</button>
  </div>
</div>

まずはJavaScriptで値の取得で動作確認

JavaScript
const buttonRegister = document.querySelector("#button-register");

buttonRegister.addEventListener("click", () => {
  const newTodoName = document.querySelector("#new-todo-name");
  const newPerson = document.querySelector("#new-person");
  const newDeadline = document.querySelector("#new-deadline");

  console.log(newTodoName.value);
  console.log(newPerson.value);
  console.log(newDeadline.value);
});

無事に値が取れてることを確認できました。

②入力欄の値を配列に格納する

JavaScript
// データを格納するからの配列を用意する
const todoList = [];

const buttonRegister = document.querySelector("#button-register");

buttonRegister.addEventListener("click", () => {
  const newTodoName = document.querySelector("#new-todo-name");
  const newPerson = document.querySelector("#new-person");
  const newDeadline = document.querySelector("#new-deadline");
 
 // オブジェクトに入力内容を格納
  const newTodoObject = {
    todoName: newTodoName.value,
    person: newPerson.value,
    deadline: newDeadline.value,
  };

  // 用意した空の配列に、オブジェクトのデータをプッシュ
  todoList.push(newTodoObject);
  console.log(todoList);
});

無事に配列に取れてることを確認できました。

③入力結果を表示するエリアを作る

CSSは普段ガッツリ書いているので、見た目はtailwindCSSでサクッと作ります…

HTML
<table
  class="mt-4 min-w-full border-collapse border border-gray-200"
  aria-label="TODO一覧"
>
  <thead>
    <tr>
      <th class="border border-gray-200 px-4 py-2">TODO名</th>
      <th class="border border-gray-200 px-4 py-2">担当者</th>
      <th class="border border-gray-200 px-4 py-2">期限</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td class="border border-gray-300 px-4 py-2">掃除</td>
      <td class="border border-gray-300 px-4 py-2">Yuta</td>
      <td class="border border-gray-300 px-4 py-2">2024.12.23</td>
    </tr>
  </tbody>
</table>

このTODO一覧のテーブルの中に、JSで取得した相対が入ります。

④入力結果をJSで取得する

HTML
<table
  class="mt-4 min-w-full border-collapse border border-gray-200"
  aria-label="TODO一覧"
>
  <thead>
    <tr>
      <th class="border border-gray-200 px-4 py-2">TODO名</th>
      <th class="border border-gray-200 px-4 py-2">担当者</th>
      <th class="border border-gray-200 px-4 py-2">期限</th>
    </tr>
  </thead>
  <tbody id="todo-list">
    <!-- <tr> -->
    <!-- <td class="border border-gray-300 px-4 py-2">掃除</td> -->
    <!-- <td class="border border-gray-300 px-4 py-2">Yuta</td> -->
    <!-- <td class="border border-gray-300 px-4 py-2">2024.12.23</td> -->
    <!-- </tr> -->
  </tbody>
</table>

tbody の中の tr > td タグをコメントアウトして、JSで取得した内容を表示します。
受け取れるように、id属性を追加しておきます。<tbody id="todo-list">

JavaScript
const todoList = [];

const buttonRegister = document.querySelector("#button-register");

buttonRegister.addEventListener("click", () => {
  const newTodoName = document.querySelector("#new-todo-name");
  const newPerson = document.querySelector("#new-person");
  const newDeadline = document.querySelector("#new-deadline");

  const newTodoObject = {
    todoName: newTodoName.value,
    person: newPerson.value,
    deadline: newDeadline.value,
  };

  todoList.push(newTodoObject);
  const tableBodyElement = document.querySelector("#todo-list");
  
  todoList.forEach((todo) => {
    const todoNameElement = document.createElement("td");
    todoNameElement.textContent = todo.todoName;
    todoNameElement.classList.add("border", "border-gray-300", "px-4", "py-2");

    const personElement = document.createElement("td");
    personElement.textContent = todo.person;
    personElement.classList.add("border", "border-gray-300", "px-4", "py-2");

    const deadlineElement = document.createElement("td");
    deadlineElement.textContent = todo.deadline;
    deadlineElement.classList.add("border", "border-gray-300", "px-4", "py-2");

    const trElement = document.createElement("tr");
    trElement.appendChild(todoNameElement);
    trElement.appendChild(personElement);
    trElement.appendChild(deadlineElement);

    tableBodyElement.appendChild(trElement);
    console.log(tableBodyElement);
  });
});

追加することができました。
ただ、1回追加する分には問題ないが、複数のTODOを追加するとバグるので、その対策をします。

⑤ whileを使って、最初の要素を消す

以下のハイライトされたコードは、tableElement という変数に格納された要素のすべての子要素を削除するためのものです。具体的には、以下のように動作します。

  1. while (tableElement.firstChild):
    • tableElement の最初の子要素 (firstChild) が存在する限り、ループを続けます。
    • firstChild が存在しない場合、ループは終了します。
  2. tableElement.firstChild.remove():

このループは、tableElement のすべての子要素が削除されるまで続きます。結果としてtableElement は空になります。

JavaScript
todoList.forEach((todo) => {
  while (tableElement.firstChild) {
    tableElement.firstChild.remove();
  }

  const todoNameElement = document.createElement("td");
  todoNameElement.textContent = todo.todoName;
  todoNameElement.classList.add("border", "border-gray-300", "px-4", "py-2");

  const personElement = document.createElement("td");
  personElement.textContent = todo.person;
  personElement.classList.add("border", "border-gray-300", "px-4", "py-2");

  const deadlineElement = document.createElement("td");
  deadlineElement.textContent = todo.deadline;
  deadlineElement.classList.add("border", "border-gray-300", "px-4", "py-2");

  const trElement = document.createElement("tr");
  trElement.appendChild(todoNameElement);
  trElement.appendChild(personElement);
  trElement.appendChild(deadlineElement);

  tableElement.appendChild(trElement);
  console.log(tableElement);
});

⑥可読性を向上する

処理が長くなってきたので、関数に切り出して可読性をよくします。

JavaScript
/**
 * 変数の初期化
 */
const todoList = [];
const tableBodyElement = document.querySelector("#todo-list");
const buttonRegister = document.querySelector("#button-register");

/**
 * 入力されたTODOを取得する関数
 *
 * @returns {void}
 */
const registerNewTodo = () => {
  const newTodoName = document.querySelector("#new-todo-name");
  const newPerson = document.querySelector("#new-person");
  const newDeadline = document.querySelector("#new-deadline");

  todoList.push({
    todoName: newTodoName.value,
    person: newPerson.value,
    deadline: newDeadline.value,
  });
};

/**
 * 既存のTODO一覧を削除する関数
 *
 * @returns {void}
 */
const deleteTodoList = () => {
  while (tableBodyElement.firstChild) {
    tableBodyElement.firstChild.remove();
  }
};

/**
 * TODOの一覧を描写する関数
 *
 * @param {string[]} classes - クラス属性の配列
 * @returns {void}
 */
const renderTodoListElement = (classes) => {
  todoList.forEach((todo) => {
    const todoNameElement = document.createElement("td");
    todoNameElement.textContent = todo.todoName;
    todoNameElement.classList.add(...classes);

    const personElement = document.createElement("td");
    personElement.textContent = todo.person;
    personElement.classList.add(...classes);

    const deadlineElement = document.createElement("td");
    deadlineElement.textContent = todo.deadline;
    deadlineElement.classList.add(...classes);

    const trElement = document.createElement("tr");
    trElement.appendChild(todoNameElement);
    trElement.appendChild(personElement);
    trElement.appendChild(deadlineElement);

    tableBodyElement.appendChild(trElement);
  });
};

/**
 * 登録ボタンがクリックされたときの処理
 *
 * @returns {void}
 */
buttonRegister.addEventListener("click", () => {
  deleteTodoList();
  registerNewTodo();
  renderTodoListElement(["border", "border-gray-300", "px-4", "py-2"]);
});

⑦絞り込み機能を追加する

処理の流れ

  1. HTMLに絞り込みの入力欄を追加
  2. JSで絞り込み入力欄の文字列の変更を監視し、検索ワードを更新する処理を追加
  3. TODOの一覧を描写する際に、絞り込みワードでフィルタリングする
HTML
<div class="mt-2 flex items-center gap-2">
  <label for="filter">絞り込み</label>
  <input type="text" id="filter" class="border p-2" />
</div>
JavaScript
/**
 * 変数の定義
 */
const filterInputElement = document.querySelector("#filter");
let filterWord = "";

/**
 * TODOの一覧を描写する関数。
 * フィルター機能で絞り込んだTODOの一覧を描写する。
 *
 * @param {string[]} classes - クラス属性の配列
 * @returns {void}
 */
const renderTodoListElement = (classes) => {
  deleteTodoList();

  todoList
    .filter(
      (todo) => 
        todo.todoName.includes(filterWord) || todo.person.includes(filterWord)
    )
    .forEach((todo) => {
      const todoNameElement = document.createElement("td");
      todoNameElement.textContent = todo.todoName;
      todoNameElement.classList.add(...classes);

      const personElement = document.createElement("td");
      personElement.textContent = todo.person;
      personElement.classList.add(...classes);

      const deadlineElement = document.createElement("td");
      deadlineElement.textContent = todo.deadline;
      deadlineElement.classList.add(...classes);

      const trElement = document.createElement("tr");
      trElement.appendChild(todoNameElement);
      trElement.appendChild(personElement);
      trElement.appendChild(deadlineElement);

      tableBodyElement.appendChild(trElement);
    });
};

/**
 * 絞り込み機能に入力された文字列が変更されたときの処理
 *
 * @returns {void}
 */
 filterInputElement.addEventListener("input", () => {
  filterWord = filterInputElement.value;
  renderTodoListElement(["border", "border-gray-300", "px-4", "py-2"]);
});

完成したTODOアプリ

感想

初めてTODOアプリを作りました。

Web制作だとHTML/CSSとJavaScriptの割合が、8:2〜7:3くらいのイメージですが、アプリとなるとJavaScriptの割合がすごいですね。

  • バグらないために何を気を付けるべきか?
  • 可読性を向上するためにはどうするべきか?
  • もっと良い処理はないか?

この辺がまだまだ課題なので、より良いコードを書けるように追求したいです。

ここ数日のフロントエンド基礎編を学習して、かなりJS力は向上したと思います。

Yuta | Code.Yu

WordPressをメインに活動する、フリーランスのWeb制作コーダーです。
React案件を経験したことをきっかけに、さらにフロントエンド開発のスキルを高めるため、JavaScriptやReactの学習を進めています。このブログでは、学習の過程や記録を発信しています。

Web制作に関する情報はこちら
Code.Yu | ホームページ制作・コーディング代行 ↗︎