Todoアイテムの更新と削除を実装する
このセクションではTodoアプリの残りの機能である「Todoアイテムの更新」と「Todoアイテムの削除」を実装していきます。
「Todoアイテムの更新」とは、チェックボックスをクリックして未完了だったらチェックをつけて完了済みに、逆に完了済みのアイテムを未完了へとトグルする機能のことです。完了状態をTodoアイテムごとに持ち、それぞれのTodoの進捗を管理できる機能です。
一方の「Todoアイテムの削除」はボタンをクリックしたらTodoアイテムを削除する機能です。 不要となったTodoを削除して完了済みのTodoを取り除くなどに利用できる機能です。
まずは「Todoアイテムの更新」から実装します。その後「Todoアイテムの削除」を実装していきます。
Todoアイテムの更新
現時点ではTodoアイテムが完了済みかどうかの状態が表示されていません。
そのため、まずはTodoアイテムが完了済みかを表示する必要があります。
HTMLの<input type="checkbox">
要素を使ってチェックボックスを表示し、Todoアイテムごとの完了状態を表現します。
<input type="checkbox">
はchecked
属性がない場合はチェックが外れた状態のチェックボックスとなります。
一方<input type="checkbox" checked>
のようにchecked
属性がある場合はチェックがついたチェックボックスとなります。
まずは確認のため、src/App.js
にてTodoListModelのonChange
メソッドで登録したリスナー関数内を書き換え、チェックボックスを表示しています。
Todoアイテム要素である<li>
要素中に次のように<input>
要素を追加してチェックボックスを表示に追加します。
チェックボックスである<input>
要素にはスタイルのためにclass
属性をcheckbox
とします。
合わせて完了済みの場合は<s>
要素を使って打ち消し線を表示しています。
src/App.jsから変更点を抜粋
this.#todoListModel.onChange(() => {
const todoListElement = element`<ul></ul>`;
const todoItems = this.#todoListModel.getTodoItems();
todoItems.forEach(item => {
// 完了済みならchecked属性をつけ、未完了ならchecked属性を外す
// input要素にはcheckboxクラスをつける
const todoItemElement = item.completed
? element`<li><input type="checkbox" class="checkbox" checked><s>${item.title}</s></li>`
: element`<li><input type="checkbox" class="checkbox">${item.title}</li>`;
todoListElement.appendChild(todoItemElement);
});
render(todoListElement, containerElement);
todoItemCountElement.textContent = `Todoアイテム数: ${this.#todoListModel.getTotalCount()}`;
});
<input type="checkbox">
要素はクリックするとチェックの表示がトグルします。
しかし、モデルであるTodoItemModel
のcompleted
プロパティの状態は自動では切り替わりません。
これにより表示とモデルの状態が異なってしまうという問題が発生します。
この問題は次のような操作をしてみると確認できます。
- Todoアイテムを追加する
- Todoアイテムのチェックボックスにチェックをつける
- 別の新しいTodoアイテムを追加する
- すべてのチェックボックスのチェックがリセットされてしまう
この問題を避けるためにも、<input type="checkbox">
要素がチェックされたらモデルの状態を更新する必要があります。
<input type="checkbox">
要素はチェックされたときにchange
イベントをディスパッチします。
このchange
イベントをリッスンして、TodoItemモデルの状態を更新すればモデルと表示の状態を同期できます。
input
要素からディスパッチされるchange
イベントをリッスンする処理は次のように書けます。
まずはtodoItemElement
要素の下にあるinput
要素をquerySelector
メソッドで探索します。
以前はdocument.querySelector
でdocument
以下からCSSセレクタにマッチする要素を探索していました。
todoItemElement.querySelector
メソッドを使うことで、todoItemElement
下にある要素だけを対象に探索できます。
そして、見つけたinput
要素に対してaddEventListener
メソッドでchange
イベントが発生したときに呼ばれるコールバック関数を登録できます。
const todoItemElement = element`<li><input type="checkbox" class="checkbox">${item.title}</li>`;
// クラス名checkboxを持つ要素を取得
const inputCheckboxElement = todoItemElement.querySelector(".checkbox");
// `<input type="checkbox">`のチェックが変更されたときに呼ばれるイベントリスナーを登録
inputCheckboxElement.addEventListener("change", () => {
// チェックボックスの表示が変わったタイミングで呼び出される処理
// TODO: ここでモデルを更新する処理を呼ぶ
});
ここまでをまとめると、Todoアイテムの更新は次の2つのステップで実装できます。
TodoListModel
に指定したTodoアイテムの更新処理を追加する- チェックボックスの
change
イベントが発生したら、モデルの状態を更新する
ここから実際にTodoアイテムの更新をtodoapp
プロジェクトに実装していきます。
TodoListModel
に指定したTodoアイテムの更新処理を追加する
まずは、TodoListModel
に指定したTodoアイテムを更新するupdateTodo
メソッドを追加します。
TodoListModelのupdateTodo
メソッドは、指定したidと一致するTodoアイテムの完了状態(completed
プロパティ)を更新します。
src/model/TodoListModel.jsの変更点を抜粋
// ===============================
// TodoListModel.jsの既存の実装は省略
// ===============================
/**
* 指定したidのTodoItemのcompletedを更新する
* @param {{ id:number, completed: boolean }}
*/
updateTodo({ id, completed }) {
// `id`が一致するTodoItemを見つけ、あるなら完了状態の値を更新する
const todoItem = this.#items.find(todo => todo.id === id);
if (!todoItem) {
return;
}
todoItem.completed = completed;
this.emitChange();
}
}
チェックボックスのchange
イベントが発生したら、Todoアイテムの完了状態を更新する
次にinput
要素のchange
イベントのリスナー関数で、Todoアイテムの完了状態を更新します。
src/App.js
にて、TodoListModelのonChange
メソッドで登録したリスナー関数内を次のように書き換えます。
App.js
でtodoItemElement
の子要素としてcheckbox
というクラス名をつけたinput
要素を追加します。
このinput
要素のchange
イベントが発生したら、TodoListModelのupdateTodo
メソッドを呼び出すようにします。
チェックがトグルするたびに呼び出されるので、completed
には現在の状態を反転(トグル)した値を渡します。
src/App.jsから変更点を抜粋
this.#todoListModel.onChange(() => {
const todoListElement = element`<ul></ul>`;
const todoItems = this.#todoListModel.getTodoItems();
todoItems.forEach(item => {
// 完了済みならchecked属性をつけ、未完了ならchecked属性を外す
const todoItemElement = item.completed
? element`<li><input type="checkbox" class="checkbox" checked><s>${item.title}</s></li>`
: element`<li><input type="checkbox" class="checkbox">${item.title}</li>`;
// チェックボックスがトグルしたときのイベントにリスナー関数を登録
const inputCheckboxElement = todoItemElement.querySelector(".checkbox");
inputCheckboxElement.addEventListener("change", () => {
// 指定したTodoアイテムの完了状態を反転させる
this.#todoListModel.updateTodo({
id: item.id,
completed: !item.completed
});
});
todoListElement.appendChild(todoItemElement);
});
render(todoListElement, containerElement);
todoItemCountElement.textContent = `Todoアイテム数: ${this.#todoListModel.getTotalCount()}`;
});
TodoListModelのupdateTodo
メソッド内ではemitChange
メソッドによって、TodoListModel
の変更が通知されます。
これによってTodoListModelのonChange
で登録したイベントリスナーが呼び出され、表示が更新されます。
これで表示とモデルが同期でき「Todoアイテムの更新処理」が実装できました。
削除機能
次は「Todoアイテムの削除機能」を実装していきます。
基本的な流れは「Todoアイテムの更新機能」と同じです。
TodoListModel
にTodoアイテムを削除する処理を追加します。
そして表示には削除ボタンを追加し、削除ボタンがクリックされたときに指定したTodoアイテムを削除する処理を呼び出します。
TodoListModel
に指定したTodoアイテムを削除する処理を追加する
まずは、TodoListModel
に指定したTodoアイテムを削除するdeleteTodo
メソッドを次のように追加します。
TodoListModelのdeleteTodo
メソッドは、指定したidと一致するTodoアイテムを削除します。
items
というTodoアイテムの配列から指定したidと一致するTodoアイテムを取り除くことで削除しています。
src/model/TodoListModel.jsの変更点を抜粋
// ===============================
// TodoListModel.jsの既存の実装は省略
// ===============================
/**
* 指定したidのTodoItemを削除する
* @param {{ id: number }}
*/
deleteTodo({ id }) {
// `id`に一致しないTodoItemだけを残すことで、`id`に一致するTodoItemを削除する
this.#items = this.#items.filter(todo => {
return todo.id !== id;
});
this.emitChange();
}
}
削除ボタンのclick
イベントが発生したら、Todoアイテムを削除する
次に削除ボタンを追加し、ボタンをクリックしたらTodoアイテムを削除する処理(deleteTodo
)を呼び出す実装をApp.js
へと追加します。
src/App.js
にて、TodoListModelのonChange
メソッドで登録したリスナー関数内を次のように書き換えます。
todoItemElement
の子要素としてdelete
というクラス名をつけたbutton
要素を追加します。
この要素がクリック(click
)されたときに呼び出されるイベントリスナーをaddEventListener
メソッドで登録します。
このイベントリスナーの中でTodoListModelのdeleteTodo
メソッドを呼び、指定したidのTodoアイテムを削除します。
src/App.jsから変更点を抜粋
this.#todoListModel.onChange(() => {
const todoListElement = element`<ul></ul>`;
const todoItems = this.#todoListModel.getTodoItems();
todoItems.forEach(item => {
// 削除ボタン(x)をそれぞれ追加する
const todoItemElement = item.completed
? element`<li><input type="checkbox" class="checkbox" checked>
<s>${item.title}</s>
<button class="delete">x</button>
</li>`
: element`<li><input type="checkbox" class="checkbox">
${item.title}
<button class="delete">x</button>
</li>`;
// チェックボックスのトグル処理は変更なし
const inputCheckboxElement = todoItemElement.querySelector(".checkbox");
inputCheckboxElement.addEventListener("change", () => {
this.#todoListModel.updateTodo({
id: item.id,
completed: !item.completed
});
});
// 削除ボタン(x)がクリックされたときにTodoListModelからアイテムを削除する
const deleteButtonElement = todoItemElement.querySelector(".delete");
deleteButtonElement.addEventListener("click", () => {
this.#todoListModel.deleteTodo({
id: item.id
});
});
todoListElement.appendChild(todoItemElement);
});
render(todoListElement, containerElement);
todoItemCountElement.textContent = `Todoアイテム数: ${this.#todoListModel.getTotalCount()}`;
});
TodoListModelのdeleteTodo
メソッド内ではemitChange
メソッドによって、TodoListModel
の変更が通知されます。
これにより表示がTodoListModel
と同期するように更新され、表示からもTodoアイテムが削除できます。
これで「Todoアイテムの削除機能」が実装できました。
このセクションのチェックリスト
- Todoアイテムの完了状態として
<input type="checkbox">
を表示に追加した - チェックボックスが更新されたときの
change
イベントのリスナー関数でTodoアイテムを更新した - Todoアイテムを削除するボタンとして
<button class="delete">x</button>
を表示に追加した - 削除ボタンの
click
イベントのリスナー関数でTodoアイテムを削除した - Todoアイテムの追加、更新、削除の機能が動作するのを確認した
このセクションでTodoアプリに必要な要件が実装できました。
- Todoアイテムを追加できる
- Todoアイテムの完了状態を更新できる
- Todoアイテムを削除できる
ここまでのTodoアプリは次のURLで確認できます。
最後のセクションでは、App.js
のリファクタリングを行って継続的に開発できるアプリの作り方について見ていきます。