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
属性がある場合はチェックがついたチェックボックスとなります。
Todoアイテム要素である<li>
要素中に次のように<input>
要素を追加しチェックボックスを表示に追加します。
チェックボックスである<input>
要素にはスタイルのためにclass
属性をcheckbox
とします。
合わせて完了済みの場合は<s>
要素を使い打ち消し線を表示しています。
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></input></li>`
: element`<li><input type="checkbox" class="checkbox">${item.title}</input></li>`;
todoListElement.appendChild(todoItemElement);
});
<input type="checkbox">
要素はクリックするとチェックの表示がトグルします。
しかし、モデルであるTodoItemModel
のcompleted
プロパティの状態は自動では切り替わりません。
これにより表示とモデルの状態が異なってしまうという問題が発生します。
この問題は次のような操作をしてみると確認できます。
- Todoアイテムを追加する
- Todoアイテムのチェックボックスにチェックを付ける
- 別の新しいTodoアイテムを追加する
- すべてのチェックボックスのチェックがリセットされてしまう
この問題を避けるためにも、<input type="checkbox">
要素がチェックされたらモデルの状態を更新する必要があります。
<input type="checkbox">
要素はチェックされたときにchange
イベントをディスパッチします。
このchange
イベントをリッスン(listen)して、TodoItemモデルの状態を更新すればモデルと表示の状態を同期できます。
input
要素からディスパッチされるchange
イベントをリッスンする処理は次のようにかけます。
まずはtodoItemElement
要素の下にあるinput
要素をquerySelector
メソッドで探索します。
以前はdocument.querySelector
でdocument
以下からCSSセレクタにマッチする要素を探索していました。
todoItemElement.querySelector
メソッドを使うことで、todoItemElement
下にある要素だけを対象に探索できます。
見つけたinput
要素に対してaddEventListener
メソッドでchange
イベントが発生したときに呼ばれるコールバック関数を登録できます。このaddEventListener
メソッドはXMLHttpRequest
の場合と同じくイベント名とコールバック関数を渡すことで、指定したイベントを受け取れます。(「ユースケース: Ajax通信」を参照)
このようなイベントが発生した際に呼ばれるコールバック関数のことをイベントリスナー(イベントをリッスンするものという意味)と呼びます。またイベントリスナーはイベントハンドラーとも呼ばれることがありますが、この書籍ではこの2つの言葉は同じ意味として扱います。
const todoItemElement = element`<li><input type="checkbox" class="checkbox">${item.title}</input></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
プロパティ)を更新します。
// ===============================
// 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アイテムの完了状態を更新します。
App.js
でtodoItemElement
の子要素としてcheckbox
というクラス名をつけたinput
要素を追加します。
このinput
要素のchange
イベントが発生したら、TodoListModel#updateTodo
メソッドを呼び出すようにします。
チェックがトグルするたびに呼び出されるので、completed
には現在の状態を反転(トグル)した値を渡します。
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></input></li>`
: element`<li><input type="checkbox" class="checkbox">${item.title}</input></li>`;
// チェックボックスがトグルしたときのイベントにリスナー関数を登録
const inputCheckboxElement = todoItemElement.querySelector(".checkbox");
inputCheckboxElement.addEventListener("change", () => {
// 指定したTodoアイテムの完了状態を反転させる
this.todoListModel.updateTodo({
id: item.id,
completed: !item.completed
});
});
todoListElement.appendChild(todoItemElement);
});
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アイテムを取り除くことで削除しています。
// ===============================
// TodoListModel.jsの既存の実装は省略
// ===============================
/**
* 指定したidのTodoItemを削除する
* @param {{ id: number }}
*/
deleteTodo({ id }) {
// `id`が一致するTodoItemを`this.items`から取り除き、削除する
this.items = this.items.filter(todo => {
return todo.id !== id;
});
this.emitChange();
}
}
削除ボタンのclick
イベントが発生したら、Todoアイテムを削除する
次にbutton
要素のclick
イベントのリスナー関数でTodoアイテムを削除する処理を呼び出します。
App.js
でtodoItemElement
の子要素としてdelete
というクラス名をつけたbutton
要素を追加します。
この要素がクリック(click
)されたときに呼び出されるイベントリスナーをaddEventListener
メソッドで登録します。
このイベントリスナーの中でTodoListModel#deleteTodo
メソッドを呼び指定したidのTodoアイテムを削除します。
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>
</input></li>`
: element`<li><input type="checkbox" class="checkbox">
${item.title}
<button class="delete">x</button>
</input></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);
});
TodoListModel#deleteTodo
メソッド内ではemitChange
メソッドによって、TodoListModel
の変更が通知されます。
これにより表示がTodoListModel
と同期するように更新され、表示からもTodoアイテムが削除できます。
これで「Todoアイテムの削除機能」が実装できました。
まとめ
このセクションでは次のことできるようになりました。
- Todoアイテムの完了状態としてを表示に追加した
- チェックボックスが更新時のchangeイベントのリスナー関数でTodoアイテムの更新した
- Todoアイテムを削除するボタンとしてを表示に追加した
- 削除ボタンのclickイベントのリスナー関数でTodoアイテムを削除した
- Todoアイテムの追加、更新、削除の機能が動作するのを確認できた
このセクションでTodoアプリに必要な要件が実装できました。
- Todoアイテムを追加できる
- Todoアイテムの完了状態を更新できる
- Todoアイテムを削除できる
ここまでのTodoアプリは次のURLで確認できます。
最後のセクションでは、App.js
のリファクタリングを行い継続的に開発できるアプリの作り方についてを見ていきます。