diff --git a/config/base.eslintrc.js b/config/base.eslintrc.js
index df9956632c..f0b2008667 100644
--- a/config/base.eslintrc.js
+++ b/config/base.eslintrc.js
@@ -51,7 +51,7 @@ module.exports = {
"space-unary-ops": "error",
"spaced-comment": [
"error", "always", {
- "exceptions": ["-", "="],
+ "exceptions": ["-", "=", "!"],
"markers": [
"eslint",
"eslint-env",
@@ -62,6 +62,7 @@ module.exports = {
"exported",
"globals",
"istanbul",
+ "!"
],
}
],
diff --git a/source/README.md b/source/README.md
index e801e2ebd5..71bf6f7d15 100644
--- a/source/README.md
+++ b/source/README.md
@@ -48,3 +48,4 @@
- [アプリの構成要素](./use-case/todoapp/app-structure/README.md)
- [フォームとイベント](./use-case/todoapp/form-event/README.md)
- [イベントとモデル](./use-case/todoapp/event-model/README.md)
+ - [Todoアイテムの更新と削除を実装する](./use-case/todoapp/update-delete/README.md)
diff --git a/source/use-case/todoapp/cypress/integration/update-delete/add-checkbox/add-checkbox.js b/source/use-case/todoapp/cypress/integration/update-delete/add-checkbox/add-checkbox.js
new file mode 100644
index 0000000000..6f2f8e79af
--- /dev/null
+++ b/source/use-case/todoapp/cypress/integration/update-delete/add-checkbox/add-checkbox.js
@@ -0,0 +1,31 @@
+const URL = "/update-delete/add-checkbox";
+const addNewTodo = require("../../../helper/todo-helper").addNewTodo;
+describe(URL, function() {
+ it("input[type=checkbox]が追加される", function() {
+ cy.visit(URL);
+ // checkbox は 0コ
+ cy.get(".checkbox").should(items => {
+ expect(items).to.have.length(0);
+ });
+ const inputText = "テスト";
+ addNewTodo(inputText).then(() => {
+ // checkbox は 1コ
+ cy.get(".checkbox").should(items => {
+ expect(items).to.have.length(1);
+ });
+ // checkedは 1コ
+ cy.get(".checkbox").check();
+ cy.get(".checkbox").should("be.checked");
+ });
+ addNewTodo(inputText).then(() => {
+ // 新しく追加するとcheckedが消える
+ cy.get(".checkbox[checked]").should(items => {
+ expect(items).to.have.length(0);
+ });
+ // checkbox は 2コ
+ cy.get(".checkbox").should(items => {
+ expect(items).to.have.length(2);
+ });
+ });
+ });
+});
diff --git a/source/use-case/todoapp/cypress/integration/update-delete/delete-feature/delete-feature.js b/source/use-case/todoapp/cypress/integration/update-delete/delete-feature/delete-feature.js
new file mode 100644
index 0000000000..d5e79054e7
--- /dev/null
+++ b/source/use-case/todoapp/cypress/integration/update-delete/delete-feature/delete-feature.js
@@ -0,0 +1,40 @@
+const URL = "/update-delete/delete-feature";
+const addNewTodo = require("../../../helper/todo-helper").addNewTodo;
+describe(URL, function() {
+ it("Todoアイテムは削除できる", function() {
+ cy.visit(URL);
+ // checkbox は 0コ
+ cy.get(".checkbox").should(items => {
+ expect(items).to.have.length(0);
+ });
+ const inputText = "テスト";
+ addNewTodo(inputText).then(() => {
+ // checkbox は 1コ
+ cy.get(".checkbox").should(items => {
+ expect(items).to.have.length(1);
+ });
+ // チェックボックスを削除できる
+ cy.get(".delete").click();
+ // チェックボックスは 0コになる
+ cy.get(".checkbox").should(items => {
+ expect(items).to.have.length(0);
+ });
+ }).then(() => {
+ const titleItems = ["a", "b", "c"];
+ const promise = Promise.all(titleItems.map(item => addNewTodo(item)));
+ promise.then(() => {
+ cy.get(".checkbox").should(items => {
+ expect(items).to.have.length(titleItems.length);
+ });
+ // すべて削除できる
+ titleItems.forEach(() => {
+ cy.get(".delete").first().click();
+ });
+ // チェックボックスは 0コになる
+ cy.get(".checkbox").should(items => {
+ expect(items).to.have.length(0);
+ });
+ });
+ });
+ });
+});
diff --git a/source/use-case/todoapp/cypress/integration/update-delete/update-feature/update-feature.js b/source/use-case/todoapp/cypress/integration/update-delete/update-feature/update-feature.js
new file mode 100644
index 0000000000..be2df36572
--- /dev/null
+++ b/source/use-case/todoapp/cypress/integration/update-delete/update-feature/update-feature.js
@@ -0,0 +1,38 @@
+const URL = "/update-delete/update-feature";
+const addNewTodo = require("../../../helper/todo-helper").addNewTodo;
+describe(URL, function() {
+ it("input[type=checkbox]が追加される", function() {
+ cy.visit(URL);
+ // checkbox は 0コ
+ cy.get(".checkbox").should(items => {
+ expect(items).to.have.length(0);
+ });
+ const inputText = "テスト";
+ addNewTodo(inputText).then(() => {
+ // checkbox は 1コ
+ cy.get(".checkbox").should(items => {
+ expect(items).to.have.length(1);
+ });
+ // checkedは 1コでトグルできる
+ cy.get(".checkbox").check();
+ cy.get(".checkbox").should("be.checked");
+ cy.get(".checkbox").uncheck();
+ cy.get(".checkbox").should("not.be.checked");
+ cy.get(".checkbox").check();
+ });
+ addNewTodo(inputText).then(() => {
+ // 新しく追加してもcheckedは維持される
+ // モデルが更新されているため
+ cy.get(".checkbox[checked]").should(items => {
+ expect(items).to.have.length(1);
+ });
+ // checkbox は 2コ
+ cy.get(".checkbox").should(items => {
+ expect(items).to.have.length(2);
+ });
+ // すべてチェックできる
+ cy.get(".checkbox").check();
+ cy.get(".checkbox").should("be.checked");
+ });
+ });
+});
diff --git a/source/use-case/todoapp/final/final/src/view/TodoListView.js b/source/use-case/todoapp/final/final/src/view/TodoListView.js
index e1e7d1699c..36ef5576f0 100644
--- a/source/use-case/todoapp/final/final/src/view/TodoListView.js
+++ b/source/use-case/todoapp/final/final/src/view/TodoListView.js
@@ -5,11 +5,11 @@ export class TodoItemView {
// 完了済み or 未完了
const checkBox = todoItem.completed
? element`
-${todoItem.title}
+${todoItem.title}
`
: element`
-${todoItem.title}
+${todoItem.title}
`;
checkBox.querySelector("input").addEventListener("change", () => {
diff --git a/source/use-case/todoapp/update-delete/README.md b/source/use-case/todoapp/update-delete/README.md
new file mode 100644
index 0000000000..525c979e65
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/README.md
@@ -0,0 +1,146 @@
+---
+author: azu
+---
+
+# Todoアイテムの更新と削除を実装する {#todo-item-update-and-delete}
+
+このセクションではTodoアプリの残りの機能である「Todoアイテムの更新」と「Todoアイテムの削除」を実装していきます。
+
+「Todoアイテムの更新」とは、チェックボックスをクリックして未完了だったらチェックを付けて完了済みに、逆完了済みのアイテムを未完了へとトグルする機能のことです。完了状態をTodoアイテムごとにもち、それぞれのTodoの進捗を管理できる機能です。
+
+一方の「Todoアイテムの削除」はボタンをクリックしたらTodoアイテムを削除する機能です。
+不要となったTodoを削除して完了済みのTodoを取り除くなどに利用できる機能です。
+
+まずは「Todoアイテムの更新」から実装します。その後「Todoアイテムの削除」を実装していきます。
+
+## Todoアイテムの更新 {#todo-item-update}
+
+現時点ではTodoアイテムの完了済みかが表示されていません。
+そのため、まずはTodoアイテムが完了済みかを表示する必要があります。
+HTMLの[``](https://developer.mozilla.org/ja/docs/Web/HTML/Element/Input/checkbox)要素を使いチェックボックスを表示し、Todoアイテムごとの完了状態を表現します。
+
+``は`checked`属性がない場合はチェックが外れた状態のチェックボックスとなります。
+一方``のように`checked`属性がある場合はチェックがついたチェックボックスとなります。
+
+![input要素のchecked属性の違い](./img/input-checkbox.png)
+
+Todoアイテム要素である``要素中に次のように``要素を追加しチェックボックスを表示に追加します。
+チェックボックスである``要素にはスタイルのために`class`属性を`checkbox`とします。
+合わせて完了済みの場合は``要素を使い打ち消し線を表示しています。
+
+[import marker:"checkbox",unindent:"true"](./add-checkbox/src/App.js)
+
+``要素はクリックするとチェックの表示がトグルします。
+しかし、モデルである`TodoItemModel`の`completed`プロパティの状態は自動では切り替わりません。
+これにより表示とモデルの状態が異なってしまうという問題が発生します。
+
+この問題は次のような操作をしてみると確認できます。
+
+1. Todoアイテムを追加する
+2. Todoアイテムのチェックボックスにチェックを付ける
+3. 別の新しいTodoアイテムを追加する
+4. すべてのチェックボックスのチェックがリセットされてしまう
+
+この問題を避けるためにも、``要素がチェックされたらモデルの状態を更新する必要があります。
+
+``要素はチェックされたときに`change`イベントを発火します。
+この`change`イベントを監視して、TodoItemモデルの状態を更新すればモデルと表示の状態を同期できます。
+
+`input`要素の`change`イベントを監視は次のようにかけます。
+
+まずは`todoItemElement`要素の下にある`input`要素を`querySelector`メソッドで探索します。
+以前は`document.querySlector`で`document`以下からCSSセレクタにマッチする要素を探索していました。
+`todoItemElement.querySelector`メソッドを使うことで、`todoItemElement`下にある要素だけを対象に探索できます。
+
+見つけた`input`要素に対して`addEventListener`メソッドで`change`イベントハンドラを登録できます。
+
+
+```js
+const todoItemElement = element`${item.title}`;
+// クラス名checkboxを持つ要素を取得
+const inputCheckboxElement = todoItemElement.querySelector(".checkbox");
+// ``のチェックが変更されたときに呼ばれるイベントハンドラを登録
+inputCheckboxElement.addEventListener("change", () => {
+ // チェックボックスの表示が変わったタイミングで呼び出される処理
+ // TODO: ここでモデルを更新する処理を呼ぶ
+});
+```
+
+ここまでをまとめると、Todoアイテムの更新は次の2つのステップで実装できます。
+
+1. `TodoListModel`に指定したTodoアイテムの更新処理を追加する
+2. チェックボックスの`change`イベントが発生したら、モデルの状態を更新する
+
+ここから実際にTodoアイテムの更新を`todoapp`プロジェクトに実装していきます。
+
+### `TodoListModel`に指定したTodoアイテムの更新処理を追加する {#TodoListModel-updateTodo}
+
+まずは、`TodoListModel`に指定したTodoアイテムを更新する`updateTodo`メソッドを追加します。
+`TodoListModel#updateTodo`メソッドは、指定したidと一致するTodoアイテムの完了状態(`completed`プロパティ)を更新します。
+
+[import, marker:"add-point",unindent:"true"](./update-feature/src/model/TodoListModel.js)
+
+### チェックボックスの`change`イベントが発生したら、Todoアイテムの完了状態を更新する {#onChange-update-model}
+
+次に`input`要素の`change`イベントのハンドラで、Todoアイテムの完了状態を更新します。
+
+`App.js`で`todoItemElement`の子要素として`checkbox`というクラス名をつけた`input`要素を追加します。
+この`input`要素の`change`イベントが発生したら、`TodoListModel#updateTodo`メソッドを呼び出すようにします。
+チェックがトグルするたびに呼び出されるので、`completed`には現在の状態を反転(トグル)した値を渡します。
+
+[import, marker:"checkbox",unindent:"true"](./update-feature/src/App.js)
+
+`TodoListModel#updateTodo`メソッド内では`emitChange`メソッドによって、`TodoListModel`の変更が通知されます。
+これによって`TodoListModel#onChange`で登録されているイベントハンドラがよびだされ、表示が更新されます。
+
+これで表示とモデルが同期でき「Todoアイテムの更新処理」が実装できました。
+
+## 削除機能 {#delete}
+
+次は「Todoアイテムの削除機能」を実装していきます。
+
+基本的な流れは「Todoアイテムの更新機能」と同じです。
+`TodoListModel`にTodoアイテムを削除する処理を追加します。
+そして表示には削除ボタンを追加し、削除ボタンがクリックされたときの指定したTodoアイテムを削除する処理を呼び出します。
+
+### `TodoListModel`に指定したTodoアイテムの削除する処理を追加する {#TodoListModel-deleteTodo}
+
+まずは、`TodoListModel`に指定したTodoアイテムを削除する`deleteTodo`メソッドを追加します。
+`TodoListModel#deleteTodo`メソッドは、指定したidと一致するTodoアイテムを削除します。
+
+`items`というTodoアイテムの配列から指定したidと一致するTodoアイテムを取り除くことで削除しています。
+
+[import, marker:"add-point",unindent:"true"](./delete-feature/src/model/TodoListModel.js)
+
+### 削除ボタンの`click`イベントが発生したら、Todoアイテムを削除する {#onChange-update-model}
+
+次に`button`要素の`click`イベントのハンドラでTodoアイテムを削除する処理を呼び出します。
+
+`App.js`で`todoItemElement`の子要素として`delete`というクラス名をつけた`button`要素を追加します。
+この要素がクリック(`click`)されたときに呼び出されるイベントハンドラを`addEventListener`メソッドで登録します。
+このイベントハンドラの中で`TodoListModel#deleteTodo`メソッドを呼び指定したidのTodoアイテムを削除します。
+
+[import, marker:"checkbox",unindent:"true"](./delete-feature/src/App.js)
+
+`TodoListModel#deleteTodo`メソッド内では`emitChange`メソッドによって、`TodoListModel`の変更が通知されます。
+これにより表示が`TodoListModel`と同期するように更新され、表示からもTodoアイテムが削除できます。
+
+これで「Todoアイテムの削除機能」が実装できました。
+
+## まとめ {#conclusion}
+
+このセクションでは次のことできるようになりました。
+
+- [x] Todoアイテムの完了状態として``を表示に追加した
+- [x] チェックボックスが更新時の`change`イベントのハンドラでTodoアイテムの更新した
+- [x] Todoアイテムを削除するボタンとして``を表示に追加した
+- [x] 削除ボタンの`click`イベントのハンドラでTodoアイテムを削除した
+- [x] Todoアイテムの追加、更新、削除の機能が動作するのを確認できた
+
+このセクションでTodoアプリに必要な要件が実装できました。
+
+- [x] Todoアイテムを追加できる
+- [x] Todoアイテムの完了状態を更新できる
+- [x] Todoアプリムを削除できる
+
+最後のセクションでは、`App.js`のリファクタリングを行い継続的に開発できるアプリの作り方についてを見ていきます。
diff --git a/source/use-case/todoapp/update-delete/add-checkbox/index.html b/source/use-case/todoapp/update-delete/add-checkbox/index.html
new file mode 100644
index 0000000000..d32ac7d276
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/add-checkbox/index.html
@@ -0,0 +1,21 @@
+
+
+
+ Todo App
+
+
+
+
+
+
+
diff --git a/source/use-case/todoapp/update-delete/add-checkbox/index.js b/source/use-case/todoapp/update-delete/add-checkbox/index.js
new file mode 100644
index 0000000000..c442099580
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/add-checkbox/index.js
@@ -0,0 +1,3 @@
+import { App } from "./src/App.js";
+const app = new App();
+app.mount();
diff --git a/source/use-case/todoapp/update-delete/add-checkbox/package.json b/source/use-case/todoapp/update-delete/add-checkbox/package.json
new file mode 100644
index 0000000000..5c209a2189
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/add-checkbox/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "todoapp",
+ "version": "1.0.0",
+ "main": "index.js",
+ "scripts": {
+ "start": "static . --port 8080"
+ },
+ "keywords": [],
+ "author": "azu",
+ "license": "MIT",
+ "devDependencies": {
+ "node-static": "^0.7.10"
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/add-checkbox/src/App.js b/source/use-case/todoapp/update-delete/add-checkbox/src/App.js
new file mode 100644
index 0000000000..03982cf47d
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/add-checkbox/src/App.js
@@ -0,0 +1,39 @@
+import { TodoListModel } from "./model/TodoListModel.js";
+import { TodoItemModel } from "./model/TodoItemModel.js";
+import { element, render } from "./view/html-util.js";
+
+export class App {
+ constructor() {
+ this.todoListModel = new TodoListModel();
+ }
+ mount() {
+ const formElement = document.querySelector("#js-form");
+ const inputElement = document.querySelector("#js-form-input");
+ const containerElement = document.querySelector("#js-todo-list");
+ const todoItemCountElement = document.querySelector("#js-todo-count");
+ this.todoListModel.onChange(() => {
+ const todoListElement = element``;
+ //! [checkbox]
+ const todoItems = this.todoListModel.getTodoItems();
+ todoItems.forEach(item => {
+ // 完了済みならchecked属性をつけ、未完了ならchecked属性を外す
+ // input要素にはcheckboxクラスをつける
+ const todoItemElement = item.completed
+ ? element`${item.title}`
+ : element`${item.title}`;
+ todoListElement.appendChild(todoItemElement);
+ });
+ //! [checkbox]
+ render(todoListElement, containerElement);
+ todoItemCountElement.textContent = `Todoアイテム数: ${this.todoListModel.totalCount}`;
+ });
+ formElement.addEventListener("submit", (event) => {
+ event.preventDefault();
+ this.todoListModel.addTodo(new TodoItemModel({
+ title: inputElement.value,
+ completed: false
+ }));
+ inputElement.value = "";
+ });
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/add-checkbox/src/EventEmitter.js b/source/use-case/todoapp/update-delete/add-checkbox/src/EventEmitter.js
new file mode 100644
index 0000000000..c70c412583
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/add-checkbox/src/EventEmitter.js
@@ -0,0 +1,53 @@
+export class EventEmitter {
+ constructor() {
+ // 登録する [イベント名, Set(ハンドラ)] を管理するMap
+ this._handlers = new Map();
+ }
+
+ /**
+ * 指定したイベントが実行されたときに呼び出されるハンドラを登録する
+ * @param {string} type イベント名
+ * @param {Function} handler イベントハンドラ
+ */
+ addEventLister(type, handler) {
+ // 指定したイベントに対応するSetを作成しハンドラを登録する
+ if (!this._handlers.has(type)) {
+ this._handlers.set(type, new Set());
+ }
+ const handlerSet = this._handlers.get(type);
+ handlerSet.add(handler);
+ }
+
+ /**
+ * 指定したイベントを発火する
+ * @param {string} type イベント名
+ */
+ emit(type) {
+ // 指定したイベントに対応するSetを取り出し、すべてのハンドラを呼び出す
+ const handlerSet = this._handlers.get(type);
+ if (!handlerSet) {
+ return;
+ }
+ handlerSet.forEach(handler => {
+ handler.call(this);
+ });
+ }
+
+ /**
+ * 指定したイベントに監視するハンドラを解除する
+ * @param {string} type イベント名
+ * @param {Function} handler イベントハンドラ
+ */
+ removeEventLister(type, handler) {
+ // 指定したイベントに対応するSetを取り出し、該当するハンドラを削除する
+ const handlerSet = this._handlers.get(type);
+ if (!handlerSet) {
+ return;
+ }
+ handlerSet.forEach(ownHandler => {
+ if (ownHandler === handler) {
+ handlerSet.delete(handler);
+ }
+ });
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/add-checkbox/src/model/TodoItemModel.example.js b/source/use-case/todoapp/update-delete/add-checkbox/src/model/TodoItemModel.example.js
new file mode 100644
index 0000000000..d31083c655
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/add-checkbox/src/model/TodoItemModel.example.js
@@ -0,0 +1,11 @@
+import { TodoItemModel } from "./TodoItemModel";
+const item = new TodoItemModel({
+ title: "未完了のTodoアイテム",
+ completed: false
+});
+const completedItem = new TodoItemModel({
+ title: "完了済みのTodoアイテム",
+ completed: true
+});
+// それぞれの`id`は異なる
+console.log(item.id !== completedItem.id); // => true
diff --git a/source/use-case/todoapp/update-delete/add-checkbox/src/model/TodoItemModel.js b/source/use-case/todoapp/update-delete/add-checkbox/src/model/TodoItemModel.js
new file mode 100644
index 0000000000..5327c79769
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/add-checkbox/src/model/TodoItemModel.js
@@ -0,0 +1,15 @@
+// ユニークなIDを管理する変数
+let todoIdx = 0;
+
+export class TodoItemModel {
+ /**
+ * @param {string} title Todoアイテムのタイトル
+ * @param {boolean} completed Todoアイテムが完了済みならばtrue、そうでない場合はfalse
+ */
+ constructor({ title, completed }) {
+ // idは自動的に連番となりそれぞれのインスタンス毎に異なるものとする
+ this.id = todoIdx++;
+ this.title = title;
+ this.completed = completed;
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/add-checkbox/src/model/TodoListModel.example.js b/source/use-case/todoapp/update-delete/add-checkbox/src/model/TodoListModel.example.js
new file mode 100644
index 0000000000..b723215567
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/add-checkbox/src/model/TodoListModel.example.js
@@ -0,0 +1,18 @@
+import { TodoItemModel } from "./TodoItemModel";
+import { TodoListModel } from "./TodoListModel";
+// 新しいTodoリストを作成する
+const todoListModel = new TodoListModel();
+// 現在のTodoアイテム数は0
+console.log(todoListModel.totalCount); // => 0
+// Todoリストが変更されたら呼ばれるイベントハンドラを登録する
+todoListModel.onChange(() => {
+ console.log("TodoListの状態が変わりました");
+});
+// 新しいTodoアイテムを追加する
+// => `onChange`で登録したイベントハンドラが呼び出される
+todoListModel.addTodo(new TodoItemModel({
+ title: "新しいTodoアイテム",
+ completed: false
+}));
+// Todoリストにアイテムが増える
+console.log(todoListModel.totalCount); // => 1;
diff --git a/source/use-case/todoapp/update-delete/add-checkbox/src/model/TodoListModel.js b/source/use-case/todoapp/update-delete/add-checkbox/src/model/TodoListModel.js
new file mode 100644
index 0000000000..7ff9895917
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/add-checkbox/src/model/TodoListModel.js
@@ -0,0 +1,55 @@
+import { EventEmitter } from "../EventEmitter.js";
+
+export class TodoListModel extends EventEmitter {
+ /**
+ * @param {TodoItemModel[]} [items] 初期アイテム一覧(デフォルトは空の配列)
+ */
+ constructor(items = []) {
+ super();
+ this.items = items;
+ }
+
+ /**
+ * TodoItemの合計数を返す
+ * @returns {number}
+ */
+ get totalCount() {
+ return this.items.length;
+ }
+
+ /**
+ * 表示できるTodoItemの配列を返す
+ * @returns {TodoItemModel[]}
+ */
+ getTodoItems() {
+ return this.items;
+ }
+
+ /**
+ * TodoListの状態が更新されたときに呼び出されるハンドラを登録する
+ * @param {Function} handler
+ * @returns {Function} イベントハンドラの登録を解除する関数を返す
+ */
+ onChange(handler) {
+ this.addEventLister("change", handler);
+ return () => {
+ this.removeEventLister("change", handler);
+ };
+ }
+
+ /**
+ * 状態が変更されたときに呼ぶ。登録済みのハンドラを呼び出す
+ */
+ emitChange() {
+ this.emit("change");
+ }
+
+ /**
+ * TodoItemを追加する
+ * @param {TodoItemModel} todoItem
+ */
+ addTodo(todoItem) {
+ this.items.push(todoItem);
+ this.emitChange();
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/add-checkbox/src/view/html-util.js b/source/use-case/todoapp/update-delete/add-checkbox/src/view/html-util.js
new file mode 100644
index 0000000000..6ded9b43e2
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/add-checkbox/src/view/html-util.js
@@ -0,0 +1,42 @@
+export function escapeSpecialChars(str) {
+ return str
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+export function htmlToElement(html) {
+ const template = document.createElement("template");
+ template.innerHTML = html;
+ return template.content.firstElementChild;
+}
+
+/**
+ * HTML文字列からDOM Nodeを作成して返す
+ * @return {HTMLElement}
+ */
+export function element(strings, ...values) {
+ const htmlString = strings.reduce((result, string, i) => {
+ const value = values[i - 1];
+ if (typeof value === "string") {
+ return result + escapeSpecialChars(value) + string;
+ } else {
+ return result + String(value) + string;
+ }
+ });
+ return htmlToElement(htmlString);
+}
+
+/**
+ * コンテナ要素の中身をbodyElementで上書きする
+ * @param {HTMLElement} bodyElement コンテナ要素の中身となる要素
+ * @param {HTMLElement} containerElement コンテナ要素
+ */
+export function render(bodyElement, containerElement) {
+ // rootElementの中身を空にする
+ containerElement.innerHTML = "";
+ // rootElementの直下にbodyElementを追加する
+ containerElement.appendChild(bodyElement);
+}
diff --git a/source/use-case/todoapp/update-delete/delete-feature/index.html b/source/use-case/todoapp/update-delete/delete-feature/index.html
new file mode 100644
index 0000000000..d32ac7d276
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/delete-feature/index.html
@@ -0,0 +1,21 @@
+
+
+
+ Todo App
+
+
+
+
+
+
+
diff --git a/source/use-case/todoapp/update-delete/delete-feature/index.js b/source/use-case/todoapp/update-delete/delete-feature/index.js
new file mode 100644
index 0000000000..c442099580
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/delete-feature/index.js
@@ -0,0 +1,3 @@
+import { App } from "./src/App.js";
+const app = new App();
+app.mount();
diff --git a/source/use-case/todoapp/update-delete/delete-feature/package.json b/source/use-case/todoapp/update-delete/delete-feature/package.json
new file mode 100644
index 0000000000..5c209a2189
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/delete-feature/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "todoapp",
+ "version": "1.0.0",
+ "main": "index.js",
+ "scripts": {
+ "start": "static . --port 8080"
+ },
+ "keywords": [],
+ "author": "azu",
+ "license": "MIT",
+ "devDependencies": {
+ "node-static": "^0.7.10"
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/delete-feature/src/App.js b/source/use-case/todoapp/update-delete/delete-feature/src/App.js
new file mode 100644
index 0000000000..d6eb8537e3
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/delete-feature/src/App.js
@@ -0,0 +1,59 @@
+import { TodoListModel } from "./model/TodoListModel.js";
+import { TodoItemModel } from "./model/TodoItemModel.js";
+import { element, render } from "./view/html-util.js";
+
+export class App {
+ constructor() {
+ this.todoListModel = new TodoListModel();
+ }
+ mount() {
+ const formElement = document.querySelector("#js-form");
+ const inputElement = document.querySelector("#js-form-input");
+ const containerElement = document.querySelector("#js-todo-list");
+ const todoItemCountElement = document.querySelector("#js-todo-count");
+ this.todoListModel.onChange(() => {
+ const todoListElement = element``;
+ //! [checkbox]
+ const todoItems = this.todoListModel.getTodoItems();
+ todoItems.forEach(item => {
+ // 削除ボタン(x)をそれぞれ追加する
+ const todoItemElement = item.completed
+ ? element`
+ ${item.title}
+
+ `
+ : element`
+ ${item.title}
+
+ `;
+ // チェックボックスのトグル処理は変更なし
+ 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);
+ });
+ //! [checkbox]
+ render(todoListElement, containerElement);
+ todoItemCountElement.textContent = `Todoアイテム数: ${this.todoListModel.totalCount}`;
+ });
+ formElement.addEventListener("submit", (event) => {
+ event.preventDefault();
+ this.todoListModel.addTodo(new TodoItemModel({
+ title: inputElement.value,
+ completed: false
+ }));
+ inputElement.value = "";
+ });
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/delete-feature/src/EventEmitter.js b/source/use-case/todoapp/update-delete/delete-feature/src/EventEmitter.js
new file mode 100644
index 0000000000..c70c412583
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/delete-feature/src/EventEmitter.js
@@ -0,0 +1,53 @@
+export class EventEmitter {
+ constructor() {
+ // 登録する [イベント名, Set(ハンドラ)] を管理するMap
+ this._handlers = new Map();
+ }
+
+ /**
+ * 指定したイベントが実行されたときに呼び出されるハンドラを登録する
+ * @param {string} type イベント名
+ * @param {Function} handler イベントハンドラ
+ */
+ addEventLister(type, handler) {
+ // 指定したイベントに対応するSetを作成しハンドラを登録する
+ if (!this._handlers.has(type)) {
+ this._handlers.set(type, new Set());
+ }
+ const handlerSet = this._handlers.get(type);
+ handlerSet.add(handler);
+ }
+
+ /**
+ * 指定したイベントを発火する
+ * @param {string} type イベント名
+ */
+ emit(type) {
+ // 指定したイベントに対応するSetを取り出し、すべてのハンドラを呼び出す
+ const handlerSet = this._handlers.get(type);
+ if (!handlerSet) {
+ return;
+ }
+ handlerSet.forEach(handler => {
+ handler.call(this);
+ });
+ }
+
+ /**
+ * 指定したイベントに監視するハンドラを解除する
+ * @param {string} type イベント名
+ * @param {Function} handler イベントハンドラ
+ */
+ removeEventLister(type, handler) {
+ // 指定したイベントに対応するSetを取り出し、該当するハンドラを削除する
+ const handlerSet = this._handlers.get(type);
+ if (!handlerSet) {
+ return;
+ }
+ handlerSet.forEach(ownHandler => {
+ if (ownHandler === handler) {
+ handlerSet.delete(handler);
+ }
+ });
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/delete-feature/src/model/TodoItemModel.example.js b/source/use-case/todoapp/update-delete/delete-feature/src/model/TodoItemModel.example.js
new file mode 100644
index 0000000000..d31083c655
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/delete-feature/src/model/TodoItemModel.example.js
@@ -0,0 +1,11 @@
+import { TodoItemModel } from "./TodoItemModel";
+const item = new TodoItemModel({
+ title: "未完了のTodoアイテム",
+ completed: false
+});
+const completedItem = new TodoItemModel({
+ title: "完了済みのTodoアイテム",
+ completed: true
+});
+// それぞれの`id`は異なる
+console.log(item.id !== completedItem.id); // => true
diff --git a/source/use-case/todoapp/update-delete/delete-feature/src/model/TodoItemModel.js b/source/use-case/todoapp/update-delete/delete-feature/src/model/TodoItemModel.js
new file mode 100644
index 0000000000..5327c79769
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/delete-feature/src/model/TodoItemModel.js
@@ -0,0 +1,15 @@
+// ユニークなIDを管理する変数
+let todoIdx = 0;
+
+export class TodoItemModel {
+ /**
+ * @param {string} title Todoアイテムのタイトル
+ * @param {boolean} completed Todoアイテムが完了済みならばtrue、そうでない場合はfalse
+ */
+ constructor({ title, completed }) {
+ // idは自動的に連番となりそれぞれのインスタンス毎に異なるものとする
+ this.id = todoIdx++;
+ this.title = title;
+ this.completed = completed;
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/delete-feature/src/model/TodoListModel.example.js b/source/use-case/todoapp/update-delete/delete-feature/src/model/TodoListModel.example.js
new file mode 100644
index 0000000000..37c5b56759
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/delete-feature/src/model/TodoListModel.example.js
@@ -0,0 +1,24 @@
+import { TodoItemModel } from "./TodoItemModel";
+import { TodoListModel } from "./TodoListModel";
+// 新しいTodoリストを作成する
+const todoListModel = new TodoListModel();
+// 現在のTodoアイテム数は0
+console.log(todoListModel.totalCount); // => 0
+// Todoリストが変更されたら呼ばれるイベントハンドラを登録する
+todoListModel.onChange(() => {
+ console.log("TodoListの状態が変わりました");
+});
+// 新しいTodoアイテムを追加する
+// => `onChange`で登録したイベントハンドラが呼び出される
+const todoItemModel = new TodoItemModel({
+ title: "新しいTodoアイテム",
+ completed: false
+});
+todoListModel.addTodo(todoItemModel);
+// Todoリストにアイテムが増える
+console.log(todoListModel.totalCount); // => 1
+// アイテムを削除する
+todoListModel.deleteTodo({
+ id: todoItemModel.id
+});
+console.log(todoListModel.totalCount); // => 0
diff --git a/source/use-case/todoapp/update-delete/delete-feature/src/model/TodoListModel.js b/source/use-case/todoapp/update-delete/delete-feature/src/model/TodoListModel.js
new file mode 100644
index 0000000000..7d2c510066
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/delete-feature/src/model/TodoListModel.js
@@ -0,0 +1,86 @@
+import { EventEmitter } from "../EventEmitter.js";
+
+export class TodoListModel extends EventEmitter {
+ /**
+ * @param {TodoItemModel[]} [items] 初期アイテム一覧(デフォルトは空の配列)
+ */
+ constructor(items = []) {
+ super();
+ this.items = items;
+ }
+
+ /**
+ * TodoItemの合計数を返す
+ * @returns {number}
+ */
+ get totalCount() {
+ return this.items.length;
+ }
+
+ /**
+ * 表示できるTodoItemの配列を返す
+ * @returns {TodoItemModel[]}
+ */
+ getTodoItems() {
+ return this.items;
+ }
+
+ /**
+ * TodoListの状態が更新されたときに呼び出されるハンドラを登録する
+ * @param {Function} handler
+ * @returns {Function} イベントハンドラの登録を解除する関数を返す
+ */
+ onChange(handler) {
+ this.addEventLister("change", handler);
+ return () => {
+ this.removeEventLister("change", handler);
+ };
+ }
+
+ /**
+ * 状態が変更されたときに呼ぶ。登録済みのハンドラを呼び出す
+ */
+ emitChange() {
+ this.emit("change");
+ }
+
+ /**
+ * TodoItemを追加する
+ * @param {TodoItemModel} todoItem
+ */
+ addTodo(todoItem) {
+ this.items.push(todoItem);
+ this.emitChange();
+ }
+
+ /**
+ * 指定したidのTodoItemのcompletedを更新する
+ * @param {number} id
+ * @param {boolean} completed
+ */
+ updateTodo({ id, completed }) {
+ const todoItem = this.items.find(todo => todo.id === id);
+ if (!todoItem) {
+ return;
+ }
+ todoItem.completed = completed;
+ this.emitChange();
+ }
+
+ //! [add-point]
+ // ===============================
+ // TodoItemModel.jsの既存の実装は省略
+ // ===============================
+ /**
+ * 指定したidのTodoItemを削除する
+ * @param {number} id
+ */
+ deleteTodo({ id }) {
+ // `id`が一致するTodoItemを`this.items`から取り除き、削除する
+ this.items = this.items.filter(todo => {
+ return todo.id !== id;
+ });
+ this.emitChange();
+ }
+}
+//! [add-point]
diff --git a/source/use-case/todoapp/update-delete/delete-feature/src/view/html-util.js b/source/use-case/todoapp/update-delete/delete-feature/src/view/html-util.js
new file mode 100644
index 0000000000..6ded9b43e2
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/delete-feature/src/view/html-util.js
@@ -0,0 +1,42 @@
+export function escapeSpecialChars(str) {
+ return str
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+export function htmlToElement(html) {
+ const template = document.createElement("template");
+ template.innerHTML = html;
+ return template.content.firstElementChild;
+}
+
+/**
+ * HTML文字列からDOM Nodeを作成して返す
+ * @return {HTMLElement}
+ */
+export function element(strings, ...values) {
+ const htmlString = strings.reduce((result, string, i) => {
+ const value = values[i - 1];
+ if (typeof value === "string") {
+ return result + escapeSpecialChars(value) + string;
+ } else {
+ return result + String(value) + string;
+ }
+ });
+ return htmlToElement(htmlString);
+}
+
+/**
+ * コンテナ要素の中身をbodyElementで上書きする
+ * @param {HTMLElement} bodyElement コンテナ要素の中身となる要素
+ * @param {HTMLElement} containerElement コンテナ要素
+ */
+export function render(bodyElement, containerElement) {
+ // rootElementの中身を空にする
+ containerElement.innerHTML = "";
+ // rootElementの直下にbodyElementを追加する
+ containerElement.appendChild(bodyElement);
+}
diff --git a/source/use-case/todoapp/update-delete/img/input-checkbox.png b/source/use-case/todoapp/update-delete/img/input-checkbox.png
new file mode 100644
index 0000000000..c2979b6011
Binary files /dev/null and b/source/use-case/todoapp/update-delete/img/input-checkbox.png differ
diff --git a/source/use-case/todoapp/update-delete/input-checkbox/index.html b/source/use-case/todoapp/update-delete/input-checkbox/index.html
new file mode 100644
index 0000000000..580a0fcb65
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/input-checkbox/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+ input checkbox
+
+
+
+
+
+ checked属性なし
+ checked属性あり
+
+
+
+
\ No newline at end of file
diff --git a/source/use-case/todoapp/update-delete/update-feature/index.html b/source/use-case/todoapp/update-delete/update-feature/index.html
new file mode 100644
index 0000000000..d32ac7d276
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/update-feature/index.html
@@ -0,0 +1,21 @@
+
+
+
+ Todo App
+
+
+
+
+
+
+
diff --git a/source/use-case/todoapp/update-delete/update-feature/index.js b/source/use-case/todoapp/update-delete/update-feature/index.js
new file mode 100644
index 0000000000..c442099580
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/update-feature/index.js
@@ -0,0 +1,3 @@
+import { App } from "./src/App.js";
+const app = new App();
+app.mount();
diff --git a/source/use-case/todoapp/update-delete/update-feature/package.json b/source/use-case/todoapp/update-delete/update-feature/package.json
new file mode 100644
index 0000000000..5c209a2189
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/update-feature/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "todoapp",
+ "version": "1.0.0",
+ "main": "index.js",
+ "scripts": {
+ "start": "static . --port 8080"
+ },
+ "keywords": [],
+ "author": "azu",
+ "license": "MIT",
+ "devDependencies": {
+ "node-static": "^0.7.10"
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/update-feature/src/App.js b/source/use-case/todoapp/update-delete/update-feature/src/App.js
new file mode 100644
index 0000000000..db12d43e49
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/update-feature/src/App.js
@@ -0,0 +1,47 @@
+import { TodoListModel } from "./model/TodoListModel.js";
+import { TodoItemModel } from "./model/TodoItemModel.js";
+import { element, render } from "./view/html-util.js";
+
+export class App {
+ constructor() {
+ this.todoListModel = new TodoListModel();
+ }
+ mount() {
+ const formElement = document.querySelector("#js-form");
+ const inputElement = document.querySelector("#js-form-input");
+ const containerElement = document.querySelector("#js-todo-list");
+ const todoItemCountElement = document.querySelector("#js-todo-count");
+ this.todoListModel.onChange(() => {
+ const todoListElement = element``;
+ //! [checkbox]
+ const todoItems = this.todoListModel.getTodoItems();
+ todoItems.forEach(item => {
+ // 完了済みならchecked属性をつけ、未完了ならchecked属性を外す
+ const todoItemElement = item.completed
+ ? element`${item.title}`
+ : element`${item.title}`;
+ // チェックボックスがトグルしたときのイベントにハンドラを登録
+ const inputCheckboxElement = todoItemElement.querySelector(".checkbox");
+ inputCheckboxElement.addEventListener("change", () => {
+ // 指定したTodoアイテムの完了状態を反転させる
+ this.todoListModel.updateTodo({
+ id: item.id,
+ completed: !item.completed
+ });
+ });
+ todoListElement.appendChild(todoItemElement);
+ });
+ //! [checkbox]
+ render(todoListElement, containerElement);
+ todoItemCountElement.textContent = `Todoアイテム数: ${this.todoListModel.totalCount}`;
+ });
+ formElement.addEventListener("submit", (event) => {
+ event.preventDefault();
+ this.todoListModel.addTodo(new TodoItemModel({
+ title: inputElement.value,
+ completed: false
+ }));
+ inputElement.value = "";
+ });
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/update-feature/src/EventEmitter.js b/source/use-case/todoapp/update-delete/update-feature/src/EventEmitter.js
new file mode 100644
index 0000000000..c70c412583
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/update-feature/src/EventEmitter.js
@@ -0,0 +1,53 @@
+export class EventEmitter {
+ constructor() {
+ // 登録する [イベント名, Set(ハンドラ)] を管理するMap
+ this._handlers = new Map();
+ }
+
+ /**
+ * 指定したイベントが実行されたときに呼び出されるハンドラを登録する
+ * @param {string} type イベント名
+ * @param {Function} handler イベントハンドラ
+ */
+ addEventLister(type, handler) {
+ // 指定したイベントに対応するSetを作成しハンドラを登録する
+ if (!this._handlers.has(type)) {
+ this._handlers.set(type, new Set());
+ }
+ const handlerSet = this._handlers.get(type);
+ handlerSet.add(handler);
+ }
+
+ /**
+ * 指定したイベントを発火する
+ * @param {string} type イベント名
+ */
+ emit(type) {
+ // 指定したイベントに対応するSetを取り出し、すべてのハンドラを呼び出す
+ const handlerSet = this._handlers.get(type);
+ if (!handlerSet) {
+ return;
+ }
+ handlerSet.forEach(handler => {
+ handler.call(this);
+ });
+ }
+
+ /**
+ * 指定したイベントに監視するハンドラを解除する
+ * @param {string} type イベント名
+ * @param {Function} handler イベントハンドラ
+ */
+ removeEventLister(type, handler) {
+ // 指定したイベントに対応するSetを取り出し、該当するハンドラを削除する
+ const handlerSet = this._handlers.get(type);
+ if (!handlerSet) {
+ return;
+ }
+ handlerSet.forEach(ownHandler => {
+ if (ownHandler === handler) {
+ handlerSet.delete(handler);
+ }
+ });
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/update-feature/src/model/TodoItemModel.example.js b/source/use-case/todoapp/update-delete/update-feature/src/model/TodoItemModel.example.js
new file mode 100644
index 0000000000..d31083c655
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/update-feature/src/model/TodoItemModel.example.js
@@ -0,0 +1,11 @@
+import { TodoItemModel } from "./TodoItemModel";
+const item = new TodoItemModel({
+ title: "未完了のTodoアイテム",
+ completed: false
+});
+const completedItem = new TodoItemModel({
+ title: "完了済みのTodoアイテム",
+ completed: true
+});
+// それぞれの`id`は異なる
+console.log(item.id !== completedItem.id); // => true
diff --git a/source/use-case/todoapp/update-delete/update-feature/src/model/TodoItemModel.js b/source/use-case/todoapp/update-delete/update-feature/src/model/TodoItemModel.js
new file mode 100644
index 0000000000..5327c79769
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/update-feature/src/model/TodoItemModel.js
@@ -0,0 +1,15 @@
+// ユニークなIDを管理する変数
+let todoIdx = 0;
+
+export class TodoItemModel {
+ /**
+ * @param {string} title Todoアイテムのタイトル
+ * @param {boolean} completed Todoアイテムが完了済みならばtrue、そうでない場合はfalse
+ */
+ constructor({ title, completed }) {
+ // idは自動的に連番となりそれぞれのインスタンス毎に異なるものとする
+ this.id = todoIdx++;
+ this.title = title;
+ this.completed = completed;
+ }
+}
diff --git a/source/use-case/todoapp/update-delete/update-feature/src/model/TodoListModel.example.js b/source/use-case/todoapp/update-delete/update-feature/src/model/TodoListModel.example.js
new file mode 100644
index 0000000000..a3706788cd
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/update-feature/src/model/TodoListModel.example.js
@@ -0,0 +1,27 @@
+import { TodoItemModel } from "./TodoItemModel";
+import { TodoListModel } from "./TodoListModel";
+// 新しいTodoリストを作成する
+const todoListModel = new TodoListModel();
+// 現在のTodoアイテム数は0
+console.log(todoListModel.totalCount); // => 0
+// Todoリストが変更されたら呼ばれるイベントハンドラを登録する
+todoListModel.onChange(() => {
+ console.log("TodoListの状態が変わりました");
+});
+// 新しいTodoアイテムを追加する
+// => `onChange`で登録したイベントハンドラが呼び出される
+const todoItemModel = new TodoItemModel({
+ title: "新しいTodoアイテム",
+ completed: false
+});
+todoListModel.addTodo(todoItemModel);
+// Todoリストにアイテムが増える
+console.log(todoListModel.totalCount); // => 1
+// 完了状態を更新する
+todoListModel.updateTodo({
+ id: todoItemModel.id,
+ completed: true
+});
+todoListModel.getTodoItems().forEach(item => {
+ console.log(item.completed); // => true
+});
diff --git a/source/use-case/todoapp/update-delete/update-feature/src/model/TodoListModel.js b/source/use-case/todoapp/update-delete/update-feature/src/model/TodoListModel.js
new file mode 100644
index 0000000000..bb7975c32e
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/update-feature/src/model/TodoListModel.js
@@ -0,0 +1,74 @@
+import { EventEmitter } from "../EventEmitter.js";
+
+export class TodoListModel extends EventEmitter {
+ /**
+ * @param {TodoItemModel[]} [items] 初期アイテム一覧(デフォルトは空の配列)
+ */
+ constructor(items = []) {
+ super();
+ this.items = items;
+ }
+
+ /**
+ * TodoItemの合計数を返す
+ * @returns {number}
+ */
+ get totalCount() {
+ return this.items.length;
+ }
+
+ /**
+ * 表示できるTodoItemの配列を返す
+ * @returns {TodoItemModel[]}
+ */
+ getTodoItems() {
+ return this.items;
+ }
+
+ /**
+ * TodoListの状態が更新されたときに呼び出されるハンドラを登録する
+ * @param {Function} handler
+ * @returns {Function} イベントハンドラの登録を解除する関数を返す
+ */
+ onChange(handler) {
+ this.addEventLister("change", handler);
+ return () => {
+ this.removeEventLister("change", handler);
+ };
+ }
+
+ /**
+ * 状態が変更されたときに呼ぶ。登録済みのハンドラを呼び出す
+ */
+ emitChange() {
+ this.emit("change");
+ }
+
+ /**
+ * TodoItemを追加する
+ * @param {TodoItemModel} todoItem
+ */
+ addTodo(todoItem) {
+ this.items.push(todoItem);
+ this.emitChange();
+ }
+ //! [add-point]
+ // ===============================
+ // TodoItemModel.jsの既存の実装は省略
+ // ===============================
+ /**
+ * 指定したidのTodoItemのcompletedを更新する
+ * @param {number} id
+ * @param {boolean} completed
+ */
+ updateTodo({ id, completed }) {
+ // `id`が一致するTodoItemを見つけ、あるなら完了状態の値を更新する
+ const todoItem = this.items.find(todo => todo.id === id);
+ if (!todoItem) {
+ return;
+ }
+ todoItem.completed = completed;
+ this.emitChange();
+ }
+}
+//! [add-point]
\ No newline at end of file
diff --git a/source/use-case/todoapp/update-delete/update-feature/src/view/html-util.js b/source/use-case/todoapp/update-delete/update-feature/src/view/html-util.js
new file mode 100644
index 0000000000..6ded9b43e2
--- /dev/null
+++ b/source/use-case/todoapp/update-delete/update-feature/src/view/html-util.js
@@ -0,0 +1,42 @@
+export function escapeSpecialChars(str) {
+ return str
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+export function htmlToElement(html) {
+ const template = document.createElement("template");
+ template.innerHTML = html;
+ return template.content.firstElementChild;
+}
+
+/**
+ * HTML文字列からDOM Nodeを作成して返す
+ * @return {HTMLElement}
+ */
+export function element(strings, ...values) {
+ const htmlString = strings.reduce((result, string, i) => {
+ const value = values[i - 1];
+ if (typeof value === "string") {
+ return result + escapeSpecialChars(value) + string;
+ } else {
+ return result + String(value) + string;
+ }
+ });
+ return htmlToElement(htmlString);
+}
+
+/**
+ * コンテナ要素の中身をbodyElementで上書きする
+ * @param {HTMLElement} bodyElement コンテナ要素の中身となる要素
+ * @param {HTMLElement} containerElement コンテナ要素
+ */
+export function render(bodyElement, containerElement) {
+ // rootElementの中身を空にする
+ containerElement.innerHTML = "";
+ // rootElementの直下にbodyElementを追加する
+ containerElement.appendChild(bodyElement);
+}