基礎から学ぶJavaScript入門 90日コース | Webページを操作するDOM - Day 45:DOM総合制作

JavaScript 90日で身につけるJavaScript
スポンサーリンク
スポンサーリンク
  1. Day 45:DOM総合制作(前半)
  2. インタラクティブToDoアプリ Ver.1 の全体像をイメージする
    1. どんなUI・機能を持つアプリにするか
  3. HTMLでToDoアプリの土台を作る
    1. 入力欄・追加ボタン・リスト・件数表示を用意する
  4. 要素取得:アプリの“操作対象”を変数にまとめる
    1. getElementByIdで必要な要素を取得する
  5. 件数表示の仕組みを先に作る
    1. 全件数・未完了件数を数えて表示する関数
  6. 要素追加:ToDo1件分の要素を作る
    1. createElementでli・チェックボックス・テキスト・削除ボタンを作る
  7. イベント:ToDo追加の流れを作る
    1. 「追加」ボタンのクリックイベントでToDoを追加する
  8. イベントの応用:EnterキーでToDoを追加する
    1. キーボード操作にも対応して使いやすくする
  9. 完成コード:インタラクティブToDoアプリ Ver.1
  10. 練習用テンプレート:自分で機能を足してみるためのベース
  11. Day 45 前半のまとめ
  12. Day 45:DOM総合制作(後半)
  13. アプリの構造をもう一度整理する
    1. どんな要素と機能が関係し合っているか
  14. HTMLの土台を再確認する
    1. 前半のHTMLをそのまま使いつつ、役割を意識する
  15. 要素取得と“状態更新”の関数を先に用意する
    1. 操作対象をまとめて取得し、件数更新を1か所に集約する
  16. ToDo1件分の要素を作る関数を“きれいに”書く
    1. createTodoItem:要素追加+イベント+クラス操作をまとめる
  17. ToDo追加の処理を“安全に・シンプルに”書く
    1. 入力チェック → 要素作成 → リスト追加 → 入力リセット → 件数更新
  18. Enterキー対応で“インタラクティブ感”を高める
    1. キーボード操作にも対応する
  19. 完成コード:整理されたインタラクティブToDoアプリ Ver.1
  20. 練習用テンプレート:自分で機能を足してみるためのベース
  21. Day 45 後半のまとめ

Day 45:DOM総合制作(前半)

「インタラクティブToDoアプリ Ver.1 を通して“DOM操作の一連の流れ”を形にする」

Day 45前半では、これまで学んできたDOM操作を総動員して、 インタラクティブToDoアプリ Ver.1 を制作していきます。

このVer.1では、次の機能を実装します。

  • ToDo追加
  • ToDo削除
  • 完了状態の切り替え
  • 件数表示(全件数・未完了件数)

ここまでできると、 「シンプルだけれど実用的なToDoアプリ」を自分の手で作れるようになります。 ステップバイステップで、初心者の方にも分かりやすく解説していきます。

インタラクティブToDoアプリ Ver.1 の全体像をイメージする

どんなUI・機能を持つアプリにするか

今回作るToDoアプリ Ver.1 は、次のような構成にします。

  • テキスト入力欄:やること(ToDo)を入力する
  • 「追加」ボタン:入力したToDoをリストに追加する
  • ToDoリスト:追加されたToDoが一覧表示される
    • 各ToDoには「完了チェックボックス」と「削除ボタン」が付く
  • 件数表示:
    • 全ToDo件数
    • 未完了のToDo件数

イメージとしては、

「やることを追加して、終わったらチェックを付けて、いらなくなったら削除する」 という、日常的に使えるシンプルなToDoアプリ

です。

HTMLでToDoアプリの土台を作る

入力欄・追加ボタン・リスト・件数表示を用意する

まずは、アプリのUIとなるHTMLを用意します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Day 45 前半:インタラクティブToDoアプリ Ver.1</title>
  <style>
    body {
      font-family: sans-serif;
      padding: 20px;
    }

    #todoInput {
      width: 260px;
      padding: 6px;
    }

    #addTodoButton {
      padding: 6px 12px;
      margin-left: 4px;
      cursor: pointer;
    }

    #todoList {
      margin-top: 16px;
      padding-left: 0;
      list-style: none;
    }

    /* ▼ ToDo1件分のスタイル */
    .todo-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 6px 8px;
      margin-bottom: 4px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }

    .todo-left {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    /* ▼ 完了状態の見た目 */
    .completed {
      text-decoration: line-through;
      color: #888;
    }

    .delete-button {
      padding: 2px 8px;
      font-size: 0.8rem;
      cursor: pointer;
      color: #fff;
      background-color: #dc3545;
      border: none;
      border-radius: 4px;
    }

    /* ▼ 件数表示 */
    #todoCount {
      margin-top: 12px;
      font-size: 0.9rem;
      color: #555;
    }
  </style>
</head>
<body>
  <h1>インタラクティブToDoアプリ Ver.1</h1>

  <!-- ▼ ToDo入力欄 -->
  <input id="todoInput" type="text" placeholder="やることを入力してください">

  <!-- ▼ ToDo追加ボタン -->
  <button id="addTodoButton">追加</button>

  <!-- ▼ 件数表示 -->
  <p id="todoCount">全件数:0件 / 未完了:0件</p>

  <!-- ▼ ToDoリスト表示エリア -->
  <ul id="todoList"></ul>

  <script>
    // ここにJavaScriptを書いていきます
  </script>
</body>
</html>

ここでのポイントは、

  • 入力欄に id="todoInput"、追加ボタンに id="addTodoButton"、リストに id="todoList" を付けていること
  • ToDo1件分の見た目を .todo-item クラスで定義していること
  • 完了状態の見た目を .completed クラスで表現するようにしていること
  • 件数表示用に <p id="todoCount"> を用意していること

です。

要素取得:アプリの“操作対象”を変数にまとめる

getElementByIdで必要な要素を取得する

JavaScriptからDOMを操作するためには、 まず「どの要素を触るか」を特定する必要があります。

// ▼ ToDo入力欄の要素
const todoInputElement = document.getElementById('todoInput');

// ▼ ToDo追加ボタンの要素
const addTodoButtonElement = document.getElementById('addTodoButton');

// ▼ ToDoリスト(ul)の要素
const todoListElement = document.getElementById('todoList');

// ▼ 件数表示の要素
const todoCountElement = document.getElementById('todoCount');
JavaScript

ここでの重要ポイントは、

  • id を付けておくと getElementById() で簡単に取得できること
  • 取得した要素を変数に入れておくことで、後の処理が読みやすくなること

です。

件数表示の仕組みを先に作る

全件数・未完了件数を数えて表示する関数

ToDoを追加・削除・完了状態変更するたびに、 件数表示を更新する必要があります。

// ▼ ToDo件数を数えて表示を更新する関数
function updateTodoCount() {
  // todoList内のli要素(ToDo1件分)をすべて取得します
  const todoItems = todoListElement.querySelectorAll('li');
  const totalCount = todoItems.length;

  // 未完了のToDoだけを数えます
  let incompleteCount = 0;
  todoItems.forEach(function (item) {
    // completedクラスが付いていないものを未完了とみなします
    if (!item.classList.contains('completed')) {
      incompleteCount++;
    }
  });

  // 件数表示を更新します(テキスト変更)
  todoCountElement.textContent = `全件数:${totalCount}件 / 未完了:${incompleteCount}件`;
}
JavaScript

ここでのポイントは、

  • querySelectorAll('li') で、todoList 内のすべてのToDo要素を取得していること
  • classList.contains('completed') で完了状態を判定していること
  • 件数表示の更新を 1つの関数にまとめている ので、 「追加」「削除」「完了状態変更」など、どこからでも呼び出せること

です。

要素追加:ToDo1件分の要素を作る

createElementでli・チェックボックス・テキスト・削除ボタンを作る

ToDo1件分の要素を、JavaScriptで動的に作成します。

// ▼ ToDo1件分の要素を作成して、イベントも設定する関数
function createTodoItem(todoText) {
  // li要素(ToDo1件分)を作成します
  const todoItemElement = document.createElement('li');
  todoItemElement.classList.add('todo-item');

  // 左側のコンテナ(チェックボックス+テキスト)を作成します
  const todoLeftElement = document.createElement('div');
  todoLeftElement.classList.add('todo-left');

  // 完了状態を表すチェックボックス
  const checkboxElement = document.createElement('input');
  checkboxElement.type = 'checkbox';

  // ToDo本文を表示するためのspan要素
  const todoTextElement = document.createElement('span');
  todoTextElement.textContent = todoText; // テキストを設定します

  // 削除ボタンを作成します
  const deleteButtonElement = document.createElement('button');
  deleteButtonElement.textContent = '削除';
  deleteButtonElement.classList.add('delete-button');

  // 左側コンテナにチェックボックスとテキストを追加します
  todoLeftElement.appendChild(checkboxElement);
  todoLeftElement.appendChild(todoTextElement);

  // liの中に左側コンテナと削除ボタンを追加します
  todoItemElement.appendChild(todoLeftElement);
  todoItemElement.appendChild(deleteButtonElement);

  // ▼ チェックボックスのイベント(完了状態の切り替え)
  checkboxElement.addEventListener('change', function () {
    // チェックが付いているかどうかで完了状態を切り替えます
    if (checkboxElement.checked) {
      // 完了状態にする:completedクラスを付けます
      todoItemElement.classList.add('completed');
    } else {
      // 未完了状態に戻す:completedクラスを外します
      todoItemElement.classList.remove('completed');
    }

    // 完了状態が変わったので件数表示を更新します
    updateTodoCount();
  });

  // ▼ 削除ボタンのイベント(要素削除)
  deleteButtonElement.addEventListener('click', function () {
    // このToDo(li要素)をDOMツリーから削除します
    todoItemElement.remove();

    // 削除後に件数表示を更新します
    updateTodoCount();
  });

  return todoItemElement;
}
JavaScript

ここでの重要ポイントは、

  • createElement() で必要な要素を1つずつ作っていること
  • textContent を使ってユーザー入力を安全にテキストとして扱っていること
  • チェックボックスの change イベントで完了状態を切り替えていること
  • 完了状態は .completed クラスで表現し、見た目はCSSに任せていること
  • 削除ボタンのクリックイベントで remove() を使って要素を削除していること

です。

イベント:ToDo追加の流れを作る

「追加」ボタンのクリックイベントでToDoを追加する

次に、ユーザーが「追加」ボタンをクリックしたときの処理を書きます。

// ▼ 「追加」ボタンのクリックイベント
addTodoButtonElement.addEventListener('click', function () {
  // 入力欄の値(文字列)を取得します
  const todoText = todoInputElement.value;

  // 空文字の場合は何もしないようにします
  if (todoText.trim() === '') {
    alert('ToDoを入力してください。');
    return;
  }

  // ToDo1件分の要素を作成します
  const todoItemElement = createTodoItem(todoText);

  // ToDoリスト(ul)に追加します
  todoListElement.appendChild(todoItemElement);

  // 入力欄を空にしておきます
  todoInputElement.value = '';

  // 件数表示を更新します
  updateTodoCount();
});
JavaScript

ここでのポイントは、

  • value プロパティで入力欄のテキストを取得していること
  • trim() で前後の空白を取り除いてから空チェックしていること
  • 入力チェック → 要素作成 → リストに追加 → 入力欄リセット → 件数更新 という流れが明確になっていること

です。

イベントの応用:EnterキーでToDoを追加する

キーボード操作にも対応して使いやすくする

フォーム系のUIでは、 Enterキーで操作できると使いやすくなります。

// ▼ 入力欄でEnterキーが押されたときにToDoを追加します
todoInputElement.addEventListener('keydown', function (event) {
  if (event.key === 'Enter') {
    // 「追加」ボタンと同じ処理を呼び出します
    addTodoButtonElement.click();
  }
});
JavaScript

ここでの重要ポイントは、

  • keydown イベントで event.key をチェックしていること
  • Enterキーが押されたときに、 わざわざ同じ処理をコピーせず、addTodoButtonElement.click() を呼び出していること
    • これにより、「追加処理の本体は常に1か所」に保たれます。

完成コード:インタラクティブToDoアプリ Ver.1

ここまでの内容を1つにまとめた完成コードです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Day 45 前半:インタラクティブToDoアプリ Ver.1</title>
  <style>
    body {
      font-family: sans-serif;
      padding: 20px;
    }

    #todoInput {
      width: 260px;
      padding: 6px;
    }

    #addTodoButton {
      padding: 6px 12px;
      margin-left: 4px;
      cursor: pointer;
    }

    #todoList {
      margin-top: 16px;
      padding-left: 0;
      list-style: none;
    }

    .todo-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 6px 8px;
      margin-bottom: 4px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }

    .todo-left {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .completed {
      text-decoration: line-through;
      color: #888;
    }

    .delete-button {
      padding: 2px 8px;
      font-size: 0.8rem;
      cursor: pointer;
      color: #fff;
      background-color: #dc3545;
      border: none;
      border-radius: 4px;
    }

    #todoCount {
      margin-top: 12px;
      font-size: 0.9rem;
      color: #555;
    }
  </style>
</head>
<body>
  <h1>インタラクティブToDoアプリ Ver.1</h1>

  <input id="todoInput" type="text" placeholder="やることを入力してください">
  <button id="addTodoButton">追加</button>

  <p id="todoCount">全件数:0件 / 未完了:0件</p>

  <ul id="todoList"></ul>

  <script>
    // ▼ 要素取得
    const todoInputElement = document.getElementById('todoInput');
    const addTodoButtonElement = document.getElementById('addTodoButton');
    const todoListElement = document.getElementById('todoList');
    const todoCountElement = document.getElementById('todoCount');

    // ▼ ToDo件数を更新する関数
    function updateTodoCount() {
      const todoItems = todoListElement.querySelectorAll('li');
      const totalCount = todoItems.length;

      let incompleteCount = 0;
      todoItems.forEach(function (item) {
        if (!item.classList.contains('completed')) {
          incompleteCount++;
        }
      });

      todoCountElement.textContent = `全件数:${totalCount}件 / 未完了:${incompleteCount}件`;
    }

    // ▼ ToDo1件分の要素を作成する関数
    function createTodoItem(todoText) {
      const todoItemElement = document.createElement('li');
      todoItemElement.classList.add('todo-item');

      const todoLeftElement = document.createElement('div');
      todoLeftElement.classList.add('todo-left');

      const checkboxElement = document.createElement('input');
      checkboxElement.type = 'checkbox';

      const todoTextElement = document.createElement('span');
      todoTextElement.textContent = todoText;

      const deleteButtonElement = document.createElement('button');
      deleteButtonElement.textContent = '削除';
      deleteButtonElement.classList.add('delete-button');

      todoLeftElement.appendChild(checkboxElement);
      todoLeftElement.appendChild(todoTextElement);

      todoItemElement.appendChild(todoLeftElement);
      todoItemElement.appendChild(deleteButtonElement);

      // ▼ 完了状態の切り替え
      checkboxElement.addEventListener('change', function () {
        if (checkboxElement.checked) {
          todoItemElement.classList.add('completed');
        } else {
          todoItemElement.classList.remove('completed');
        }
        updateTodoCount();
      });

      // ▼ ToDo削除
      deleteButtonElement.addEventListener('click', function () {
        todoItemElement.remove();
        updateTodoCount();
      });

      return todoItemElement;
    }

    // ▼ ToDo追加ボタンのイベント
    addTodoButtonElement.addEventListener('click', function () {
      const todoText = todoInputElement.value;

      if (todoText.trim() === '') {
        alert('ToDoを入力してください。');
        return;
      }

      const todoItemElement = createTodoItem(todoText);
      todoListElement.appendChild(todoItemElement);

      todoInputElement.value = '';
      updateTodoCount();
    });

    // ▼ EnterキーでToDo追加
    todoInputElement.addEventListener('keydown', function (event) {
      if (event.key === 'Enter') {
        addTodoButtonElement.click();
      }
    });

    // ▼ 初期状態の件数表示
    updateTodoCount();
  </script>
</body>
</html>

練習用テンプレート:自分で機能を足してみるためのベース

最後に、少し簡略化した練習用テンプレートを紹介します。 ここから「フィルタ機能(未完了のみ表示)」や「ソート機能」などを自分で足してみるのも良い練習になります。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Day 45 前半:ToDo Ver.1 練習テンプレート</title>
  <style>
    body { font-family: sans-serif; padding: 20px; }
    #todoList { margin-top: 16px; padding-left: 0; list-style: none; }
    .todo-item { display: flex; justify-content: space-between; padding: 6px 8px; border: 1px solid #ccc; margin-bottom: 4px; }
    .todo-left { display: flex; align-items: center; gap: 8px; }
    .completed { text-decoration: line-through; color: #888; }
    .delete-button { padding: 2px 8px; font-size: 0.8rem; cursor: pointer; }
    #todoCount { margin-top: 12px; font-size: 0.9rem; color: #555; }
  </style>
</head>
<body>
  <h1>ToDo Ver.1 練習テンプレート</h1>

  <input id="todoInput" type="text" placeholder="やること">
  <button id="addTodoButton">追加</button>

  <p id="todoCount">全件数:0件 / 未完了:0件</p>

  <ul id="todoList"></ul>

  <script>
    console.log('--- ToDo Ver.1 練習開始 ---');

    const todoInputElement = document.getElementById('todoInput');
    const addTodoButtonElement = document.getElementById('addTodoButton');
    const todoListElement = document.getElementById('todoList');
    const todoCountElement = document.getElementById('todoCount');

    function updateTodoCount() {
      const items = todoListElement.querySelectorAll('li');
      const total = items.length;
      let incomplete = 0;
      items.forEach((item) => {
        if (!item.classList.contains('completed')) {
          incomplete++;
        }
      });
      todoCountElement.textContent = `全件数:${total}件 / 未完了:${incomplete}件`;
    }

    function createTodoItem(text) {
      const li = document.createElement('li');
      li.classList.add('todo-item');

      const left = document.createElement('div');
      left.classList.add('todo-left');

      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';

      const span = document.createElement('span');
      span.textContent = text;

      const button = document.createElement('button');
      button.textContent = '削除';
      button.classList.add('delete-button');

      left.appendChild(checkbox);
      left.appendChild(span);
      li.appendChild(left);
      li.appendChild(button);

      checkbox.addEventListener('change', () => {
        if (checkbox.checked) {
          li.classList.add('completed');
        } else {
          li.classList.remove('completed');
        }
        updateTodoCount();
      });

      button.addEventListener('click', () => {
        li.remove();
        updateTodoCount();
      });

      return li;
    }

    addTodoButtonElement.addEventListener('click', () => {
      const text = todoInputElement.value;

      if (text.trim() === '') {
        alert('ToDoを入力してください。');
        return;
      }

      const li = createTodoItem(text);
      todoListElement.appendChild(li);

      todoInputElement.value = '';
      updateTodoCount();
    });

    todoInputElement.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        addTodoButtonElement.click();
      }
    });

    updateTodoCount();
    console.log('--- ToDo Ver.1 練習テンプレートの準備が完了しました ---');
  </script>
</body>
</html>

Day 45 前半のまとめ

Day 45前半では、

  • インタラクティブToDoアプリ Ver.1 を通して、 「ToDo追加」「ToDo削除」「完了状態の切り替え」「件数表示」という 実用的な機能を、DOM操作で組み立てたこと。
  • 要素取得・テキスト変更・クラス操作・イベント・要素追加・要素削除を、 1つのアプリの中で自然な流れとして使えるようになったこと。
  • 関数に処理をまとめることで、 読みやすくて拡張しやすいコード構造を体験したこと。

を整理しました。

このVer.1を土台にして、後半ではフィルタ機能や保存機能など、 さらにインタラクティブな振る舞いを足していくことができます。


Day 45:DOM総合制作(後半)

「インタラクティブToDoアプリ Ver.1 を“使いやすく・拡張しやすく”整える」

Day 45後半では、前半で作成した インタラクティブToDoアプリ Ver.1 を、 より「使いやすく」「拡張しやすい」形に整えていきます。

機能そのものは前半と同じく、

  • ToDo追加
  • ToDo削除
  • 完了状態の切り替え
  • 件数表示

ですが、後半では次のポイントを意識して深掘りします。

  • コード構造を整理して「読みやすさ」を高める
  • イベントの扱い方を丁寧にする(イベント委譲の入口)
  • 入力チェックやセキュリティ視点を少し意識する
  • 状態(完了/未完了)と件数表示の関係を明確にする

「とりあえず動くコード」から、 「他の人が読んでも分かりやすいコード」 に近づけていきます。

アプリの構造をもう一度整理する

どんな要素と機能が関係し合っているか

インタラクティブToDoアプリ Ver.1 の構成は、次のようになっています。

  • 入力欄#todoInput
  • 追加ボタン#addTodoButton
  • ToDoリスト#todoList(中に li.todo-item が並ぶ)
    • todo-item の中に
      • チェックボックス(完了状態)
      • テキスト(ToDo内容)
      • 削除ボタン
  • 件数表示#todoCount

これらの要素に対して、

  • 要素取得
  • テキスト変更
  • クラス操作
  • イベント
  • 要素追加
  • 要素削除

を組み合わせて使っています。

後半では、この組み合わせを 「役割ごとに整理する」 ことを意識します。

HTMLの土台を再確認する

前半のHTMLをそのまま使いつつ、役割を意識する

HTMLは前半とほぼ同じですが、 ここでもう一度「何のための要素か」を確認しておきます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Day 45 後半:インタラクティブToDoアプリ Ver.1</title>
  <style>
    body {
      font-family: sans-serif;
      padding: 20px;
    }

    #todoInput {
      width: 260px;
      padding: 6px;
    }

    #addTodoButton {
      padding: 6px 12px;
      margin-left: 4px;
      cursor: pointer;
    }

    #todoList {
      margin-top: 16px;
      padding-left: 0;
      list-style: none;
    }

    .todo-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 6px 8px;
      margin-bottom: 4px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }

    .todo-left {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .completed {
      text-decoration: line-through;
      color: #888;
    }

    .delete-button {
      padding: 2px 8px;
      font-size: 0.8rem;
      cursor: pointer;
      color: #fff;
      background-color: #dc3545;
      border: none;
      border-radius: 4px;
    }

    #todoCount {
      margin-top: 12px;
      font-size: 0.9rem;
      color: #555;
    }
  </style>
</head>
<body>
  <h1>インタラクティブToDoアプリ Ver.1</h1>

  <!-- ▼ ToDo入力欄 -->
  <input id="todoInput" type="text" placeholder="やることを入力してください">

  <!-- ▼ ToDo追加ボタン -->
  <button id="addTodoButton">追加</button>

  <!-- ▼ 件数表示 -->
  <p id="todoCount">全件数:0件 / 未完了:0件</p>

  <!-- ▼ ToDoリスト表示エリア -->
  <ul id="todoList"></ul>

  <script>
    // ここにJavaScriptを書いていきます
  </script>
</body>
</html>

このHTMLを前提に、JavaScript側を「役割ごとに整理」していきます。

要素取得と“状態更新”の関数を先に用意する

操作対象をまとめて取得し、件数更新を1か所に集約する

まずは、アプリで頻繁に使う要素を取得し、 件数表示を更新する関数を用意します。

// ▼ 要素取得
const todoInputElement = document.getElementById('todoInput');
const addTodoButtonElement = document.getElementById('addTodoButton');
const todoListElement = document.getElementById('todoList');
const todoCountElement = document.getElementById('todoCount');

// ▼ ToDo件数を更新する関数
function updateTodoCount() {
  // todoList内のli要素(ToDo1件分)をすべて取得します
  const todoItems = todoListElement.querySelectorAll('li');
  const totalCount = todoItems.length;

  // 未完了のToDoだけを数えます
  let incompleteCount = 0;
  todoItems.forEach(function (item) {
    // completedクラスが付いていないものを未完了とみなします
    if (!item.classList.contains('completed')) {
      incompleteCount++;
    }
  });

  // 件数表示を更新します(テキスト変更)
  todoCountElement.textContent = `全件数:${totalCount}件 / 未完了:${incompleteCount}件`;
}
JavaScript

ここでの重要ポイントは、

  • 「件数表示のロジック」を1つの関数にまとめていること
  • 完了状態を .completed クラスで表現し、その有無で未完了を判定していること
  • この関数を 「追加」「削除」「完了状態変更」すべてから呼び出す」 ことで、 状態と表示のズレを防いでいること

です。

ToDo1件分の要素を作る関数を“きれいに”書く

createTodoItem:要素追加+イベント+クラス操作をまとめる

前半でも書いた createTodoItem() を、 後半では「読みやすさ」を意識して整理します。

// ▼ ToDo1件分の要素を作成して、イベントも設定する関数
function createTodoItem(todoText) {
  // li要素(ToDo1件分)を作成します
  const todoItemElement = document.createElement('li');
  todoItemElement.classList.add('todo-item');

  // 左側のコンテナ(チェックボックス+テキスト)を作成します
  const todoLeftElement = document.createElement('div');
  todoLeftElement.classList.add('todo-left');

  // 完了状態を表すチェックボックス
  const checkboxElement = document.createElement('input');
  checkboxElement.type = 'checkbox';

  // ToDo本文を表示するためのspan要素
  const todoTextElement = document.createElement('span');
  todoTextElement.textContent = todoText; // テキストを設定します

  // 削除ボタンを作成します
  const deleteButtonElement = document.createElement('button');
  deleteButtonElement.textContent = '削除';
  deleteButtonElement.classList.add('delete-button');

  // 左側コンテナにチェックボックスとテキストを追加します
  todoLeftElement.appendChild(checkboxElement);
  todoLeftElement.appendChild(todoTextElement);

  // liの中に左側コンテナと削除ボタンを追加します
  todoItemElement.appendChild(todoLeftElement);
  todoItemElement.appendChild(deleteButtonElement);

  // ▼ 完了状態の切り替え(イベント+クラス操作)
  checkboxElement.addEventListener('change', function () {
    if (checkboxElement.checked) {
      // 完了状態にする:completedクラスを付けます
      todoItemElement.classList.add('completed');
    } else {
      // 未完了状態に戻す:completedクラスを外します
      todoItemElement.classList.remove('completed');
    }

    // 完了状態が変わったので件数表示を更新します
    updateTodoCount();
  });

  // ▼ ToDo削除(イベント+要素削除)
  deleteButtonElement.addEventListener('click', function () {
    // このToDo(li要素)をDOMツリーから削除します
    todoItemElement.remove();

    // 削除後に件数表示を更新します
    updateTodoCount();
  });

  return todoItemElement;
}
JavaScript

ここでの重要ポイントは、

  • 「1件分のToDoに関すること」をすべてこの関数に閉じ込めていること
    • 要素追加
    • 完了状態のイベント
    • 削除のイベント
  • これにより、呼び出し側は「ToDoを作ってリストに追加する」だけに集中できること
  • 完了状態の見た目は .completed クラスに任せているため、 デザイン変更はCSS側で完結すること

です。

ToDo追加の処理を“安全に・シンプルに”書く

入力チェック → 要素作成 → リスト追加 → 入力リセット → 件数更新

次に、「追加」ボタンのクリックイベントを整理します。

// ▼ ToDo追加ボタンのクリックイベント
addTodoButtonElement.addEventListener('click', function () {
  // 入力欄の値(文字列)を取得します
  const todoText = todoInputElement.value;

  // ▼ 入力チェック(空文字や空白のみを防ぐ)
  if (todoText.trim() === '') {
    alert('ToDoを入力してください。');
    return;
  }

  // ▼ ToDo1件分の要素を作成します
  const todoItemElement = createTodoItem(todoText);

  // ▼ ToDoリスト(ul)に追加します
  todoListElement.appendChild(todoItemElement);

  // ▼ 入力欄を空にしておきます
  todoInputElement.value = '';

  // ▼ 件数表示を更新します
  updateTodoCount();
});
JavaScript

ここでの重要ポイントは、

  • trim() で前後の空白を取り除いてから空チェックしていること
  • 入力チェックを最初に行い、問題があれば早めに return していること
  • 「何をしているか」が一目で分かるような流れになっていること

Enterキー対応で“インタラクティブ感”を高める

キーボード操作にも対応する

フォーム系のUIでは、 Enterキーで操作できると使いやすくなります。

// ▼ 入力欄でEnterキーが押されたときにToDoを追加します
todoInputElement.addEventListener('keydown', function (event) {
  if (event.key === 'Enter') {
    // 「追加」ボタンと同じ処理を呼び出します
    addTodoButtonElement.click();
  }
});
JavaScript

ここでのポイントは、

  • event.key === 'Enter' でキーを判定していること
  • 処理をコピーせず、addTodoButtonElement.click() を呼び出していること
    • これにより、「追加処理の本体は常に1か所」に保たれます。

完成コード:整理されたインタラクティブToDoアプリ Ver.1

ここまでの内容を1つにまとめた完成コードです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Day 45 後半:インタラクティブToDoアプリ Ver.1</title>
  <style>
    body {
      font-family: sans-serif;
      padding: 20px;
    }

    #todoInput {
      width: 260px;
      padding: 6px;
    }

    #addTodoButton {
      padding: 6px 12px;
      margin-left: 4px;
      cursor: pointer;
    }

    #todoList {
      margin-top: 16px;
      padding-left: 0;
      list-style: none;
    }

    .todo-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 6px 8px;
      margin-bottom: 4px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }

    .todo-left {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .completed {
      text-decoration: line-through;
      color: #888;
    }

    .delete-button {
      padding: 2px 8px;
      font-size: 0.8rem;
      cursor: pointer;
      color: #fff;
      background-color: #dc3545;
      border: none;
      border-radius: 4px;
    }

    #todoCount {
      margin-top: 12px;
      font-size: 0.9rem;
      color: #555;
    }
  </style>
</head>
<body>
  <h1>インタラクティブToDoアプリ Ver.1</h1>

  <input id="todoInput" type="text" placeholder="やることを入力してください">
  <button id="addTodoButton">追加</button>

  <p id="todoCount">全件数:0件 / 未完了:0件</p>

  <ul id="todoList"></ul>

  <script>
    // ▼ 要素取得
    const todoInputElement = document.getElementById('todoInput');
    const addTodoButtonElement = document.getElementById('addTodoButton');
    const todoListElement = document.getElementById('todoList');
    const todoCountElement = document.getElementById('todoCount');

    // ▼ ToDo件数を更新する関数
    function updateTodoCount() {
      const todoItems = todoListElement.querySelectorAll('li');
      const totalCount = todoItems.length;

      let incompleteCount = 0;
      todoItems.forEach(function (item) {
        if (!item.classList.contains('completed')) {
          incompleteCount++;
        }
      });

      todoCountElement.textContent = `全件数:${totalCount}件 / 未完了:${incompleteCount}件`;
    }

    // ▼ ToDo1件分の要素を作成する関数
    function createTodoItem(todoText) {
      const todoItemElement = document.createElement('li');
      todoItemElement.classList.add('todo-item');

      const todoLeftElement = document.createElement('div');
      todoLeftElement.classList.add('todo-left');

      const checkboxElement = document.createElement('input');
      checkboxElement.type = 'checkbox';

      const todoTextElement = document.createElement('span');
      todoTextElement.textContent = todoText;

      const deleteButtonElement = document.createElement('button');
      deleteButtonElement.textContent = '削除';
      deleteButtonElement.classList.add('delete-button');

      todoLeftElement.appendChild(checkboxElement);
      todoLeftElement.appendChild(todoTextElement);

      todoItemElement.appendChild(todoLeftElement);
      todoItemElement.appendChild(deleteButtonElement);

      // ▼ 完了状態の切り替え
      checkboxElement.addEventListener('change', function () {
        if (checkboxElement.checked) {
          todoItemElement.classList.add('completed');
        } else {
          todoItemElement.classList.remove('completed');
        }
        updateTodoCount();
      });

      // ▼ ToDo削除
      deleteButtonElement.addEventListener('click', function () {
        todoItemElement.remove();
        updateTodoCount();
      });

      return todoItemElement;
    }

    // ▼ ToDo追加ボタンのイベント
    addTodoButtonElement.addEventListener('click', function () {
      const todoText = todoInputElement.value;

      if (todoText.trim() === '') {
        alert('ToDoを入力してください。');
        return;
      }

      const todoItemElement = createTodoItem(todoText);
      todoListElement.appendChild(todoItemElement);

      todoInputElement.value = '';
      updateTodoCount();
    });

    // ▼ EnterキーでToDo追加
    todoInputElement.addEventListener('keydown', function (event) {
      if (event.key === 'Enter') {
        addTodoButtonElement.click();
      }
    });

    // ▼ 初期状態の件数表示
    updateTodoCount();
  </script>
</body>
</html>

練習用テンプレート:自分で機能を足してみるためのベース

このVer.1をベースに、次のような機能を自分で追加してみると良い練習になります。

  • 未完了のみ表示するフィルタボタン
  • 完了済みを一括削除するボタン
  • ローカルストレージに保存して、ページを再読み込みしてもToDoを保持する

そのための簡略版テンプレートを紹介します。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Day 45 後半:ToDo Ver.1 練習テンプレート</title>
  <style>
    body { font-family: sans-serif; padding: 20px; }
    #todoList { margin-top: 16px; padding-left: 0; list-style: none; }
    .todo-item { display: flex; justify-content: space-between; padding: 6px 8px; border: 1px solid #ccc; margin-bottom: 4px; }
    .todo-left { display: flex; align-items: center; gap: 8px; }
    .completed { text-decoration: line-through; color: #888; }
    .delete-button { padding: 2px 8px; font-size: 0.8rem; cursor: pointer; }
    #todoCount { margin-top: 12px; font-size: 0.9rem; color: #555; }
  </style>
</head>
<body>
  <h1>ToDo Ver.1 練習テンプレート</h1>

  <input id="todoInput" type="text" placeholder="やること">
  <button id="addTodoButton">追加</button>

  <p id="todoCount">全件数:0件 / 未完了:0件</p>

  <ul id="todoList"></ul>

  <script>
    console.log('--- ToDo Ver.1 練習開始 ---');

    const todoInputElement = document.getElementById('todoInput');
    const addTodoButtonElement = document.getElementById('addTodoButton');
    const todoListElement = document.getElementById('todoList');
    const todoCountElement = document.getElementById('todoCount');

    function updateTodoCount() {
      const items = todoListElement.querySelectorAll('li');
      const total = items.length;
      let incomplete = 0;
      items.forEach((item) => {
        if (!item.classList.contains('completed')) {
          incomplete++;
        }
      });
      todoCountElement.textContent = `全件数:${total}件 / 未完了:${incomplete}件`;
    }

    function createTodoItem(text) {
      const li = document.createElement('li');
      li.classList.add('todo-item');

      const left = document.createElement('div');
      left.classList.add('todo-left');

      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';

      const span = document.createElement('span');
      span.textContent = text;

      const button = document.createElement('button');
      button.textContent = '削除';
      button.classList.add('delete-button');

      left.appendChild(checkbox);
      left.appendChild(span);
      li.appendChild(left);
      li.appendChild(button);

      checkbox.addEventListener('change', () => {
        if (checkbox.checked) {
          li.classList.add('completed');
        } else {
          li.classList.remove('completed');
        }
        updateTodoCount();
      });

      button.addEventListener('click', () => {
        li.remove();
        updateTodoCount();
      });

      return li;
    }

    addTodoButtonElement.addEventListener('click', () => {
      const text = todoInputElement.value;

      if (text.trim() === '') {
        alert('ToDoを入力してください。');
        return;
      }

      const li = createTodoItem(text);
      todoListElement.appendChild(li);

      todoInputElement.value = '';
      updateTodoCount();
    });

    todoInputElement.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        addTodoButtonElement.click();
      }
    });

    updateTodoCount();
    console.log('--- ToDo Ver.1 練習テンプレートの準備が完了しました ---');
  </script>
</body>
</html>

Day 45 後半のまとめ

Day 45後半では、

  • インタラクティブToDoアプリ Ver.1 のコード構造を整理し、 「ToDo追加」「ToDo削除」「完了状態」「件数表示」を 役割ごとに分けて理解し直したこと。
  • 要素取得・テキスト変更・クラス操作・イベント・要素追加・要素削除を、 1つのアプリの中で自然な流れとして組み合わせる感覚を深めたこと。
  • 関数に処理をまとめることで、 読みやすく・拡張しやすいコードに近づけたこと。

を整理しました。

このVer.1は、今後の「Ver.2」「Ver.3」へと発展させていくための、 しっかりした土台になっています。

タイトルとURLをコピーしました