Skip to content

Commit

Permalink
feat(Todo): Todoアイテムの更新機能の実装 (#469)
Browse files Browse the repository at this point in the history
* feat(Todo): Todoアイテムの更新機能の実装

* fix querySelector

* fix typo

* feat(todoapp): add delete

* add checkbox

* fix: ignore //!

* fix length

* fix

* fix

* fix

* test(todoapp): E2Eテストを追加

* fix lint

* fix

* fix

* test(todoapp): Unit TEstを追加
  • Loading branch information
azu authored Jun 2, 2018
1 parent 5883221 commit 5a5c930
Show file tree
Hide file tree
Showing 39 changed files with 1,190 additions and 3 deletions.
3 changes: 2 additions & 1 deletion config/base.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ module.exports = {
"space-unary-ops": "error",
"spaced-comment": [
"error", "always", {
"exceptions": ["-", "="],
"exceptions": ["-", "=", "!"],
"markers": [
"eslint",
"eslint-env",
Expand All @@ -62,6 +62,7 @@ module.exports = {
"exported",
"globals",
"istanbul",
"!"
],
}
],
Expand Down
1 change: 1 addition & 0 deletions source/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
4 changes: 2 additions & 2 deletions source/use-case/todoapp/final/final/src/view/TodoListView.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ export class TodoItemView {
// 完了済み or 未完了
const checkBox = todoItem.completed
? element`<li>
<input type="checkbox" class="toggle" checked><s>${todoItem.title}</s></input>
<input type="checkbox" checked><s>${todoItem.title}</s></input>
<button class="delete">×</button>
</li>`
: element`<li>
<input type="checkbox" class="toggle">${todoItem.title}</input>
<input type="checkbox">${todoItem.title}</input>
<button class="delete">×</button>
</li>`;
checkBox.querySelector("input").addEventListener("change", () => {
Expand Down
146 changes: 146 additions & 0 deletions source/use-case/todoapp/update-delete/README.md
Original file line number Diff line number Diff line change
@@ -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の[`<input type="checkbox">`](https://developer.mozilla.org/ja/docs/Web/HTML/Element/Input/checkbox)要素を使いチェックボックスを表示し、Todoアイテムごとの完了状態を表現します。

`<input type="checkbox">``checked`属性がない場合はチェックが外れた状態のチェックボックスとなります。
一方`<input type="checkbox" checked>`のように`checked`属性がある場合はチェックがついたチェックボックスとなります。

![input要素のchecked属性の違い](./img/input-checkbox.png)

Todoアイテム要素である`<li>`要素中に次のように`<input>`要素を追加しチェックボックスを表示に追加します。
チェックボックスである`<input>`要素にはスタイルのために`class`属性を`checkbox`とします。
合わせて完了済みの場合は`<s>`要素を使い打ち消し線を表示しています。

[import marker:"checkbox",unindent:"true"](./add-checkbox/src/App.js)

`<input type="checkbox">`要素はクリックするとチェックの表示がトグルします。
しかし、モデルである`TodoItemModel``completed`プロパティの状態は自動では切り替わりません。
これにより表示とモデルの状態が異なってしまうという問題が発生します。

この問題は次のような操作をしてみると確認できます。

1. Todoアイテムを追加する
2. Todoアイテムのチェックボックスにチェックを付ける
3. 別の新しいTodoアイテムを追加する
4. すべてのチェックボックスのチェックがリセットされてしまう

この問題を避けるためにも、`<input type="checkbox">`要素がチェックされたらモデルの状態を更新する必要があります。

`<input type="checkbox">`要素はチェックされたときに`change`イベントを発火します。
この`change`イベントを監視して、TodoItemモデルの状態を更新すればモデルと表示の状態を同期できます。

`input`要素の`change`イベントを監視は次のようにかけます。

まずは`todoItemElement`要素の下にある`input`要素を`querySelector`メソッドで探索します。
以前は`document.querySlector``document`以下からCSSセレクタにマッチする要素を探索していました。
`todoItemElement.querySelector`メソッドを使うことで、`todoItemElement`下にある要素だけを対象に探索できます。

見つけた`input`要素に対して`addEventListener`メソッドで`change`イベントハンドラを登録できます。

<!-- doctest:disable -->
```js
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つのステップで実装できます。

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アイテムの完了状態として`<input type="checkbox">`を表示に追加した
- [x] チェックボックスが更新時の`change`イベントのハンドラでTodoアイテムの更新した
- [x] Todoアイテムを削除するボタンとして`<button class="delete">x</button>`を表示に追加した
- [x] 削除ボタンの`click`イベントのハンドラでTodoアイテムを削除した
- [x] Todoアイテムの追加、更新、削除の機能が動作するのを確認できた

このセクションでTodoアプリに必要な要件が実装できました。

- [x] Todoアイテムを追加できる
- [x] Todoアイテムの完了状態を更新できる
- [x] Todoアプリムを削除できる

最後のセクションでは、`App.js`のリファクタリングを行い継続的に開発できるアプリの作り方についてを見ていきます。
21 changes: 21 additions & 0 deletions source/use-case/todoapp/update-delete/add-checkbox/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Todo App</title>
<link href="https://asciidwango.github.io/js-primer/use-case/todoapp/final/final/index.css" rel="stylesheet" />
</head>
<body>
<div class="todoapp">
<form id="js-form">
<input id="js-form-input" class="new-todo" type="text" placeholder="What need to be done?" autocomplete="off">
</form>
<div id="js-todo-list" class="todo-list">
<!-- 動的に更新されるTodoリスト -->
</div>
<footer class="footer">
<span id="js-todo-count">Todoアイテム数: 0</span>
</footer>
</div>
<script src="./index.js" type="module"></script>
</body>
</html>
3 changes: 3 additions & 0 deletions source/use-case/todoapp/update-delete/add-checkbox/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { App } from "./src/App.js";
const app = new App();
app.mount();
14 changes: 14 additions & 0 deletions source/use-case/todoapp/update-delete/add-checkbox/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
39 changes: 39 additions & 0 deletions source/use-case/todoapp/update-delete/add-checkbox/src/App.js
Original file line number Diff line number Diff line change
@@ -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`<ul />`;
//! [checkbox]
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);
});
//! [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 = "";
});
}
}
Loading

0 comments on commit 5a5c930

Please sign in to comment.