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 + + + +
    +
    + +
    +
    + +
    +
    + Todoアイテム数: 0 +
    +
    + + + 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`