Skip to content

Commit 86a7ceb

Browse files
authored
feat(todo): Todoアプリの課題を追加 (#620)
* fix(todo): JSDocのdestructuringの記法を修正 この書籍の主題ではないので、できるだけコンパクトな方法を採用 - microsoft/TypeScript#24045 - google/closure-compiler#1781 - microsoft/TypeScript#24746 fix #606 * fix(todo): removeEventListerを削除 TODOアプリのユースケースでは解除まで行っていないので削除する fix #607 * feat(todo): 残った課題を追加 * chore(todo): add comment * fix(todo): add <!-- doctest:disable -->
1 parent c486cd0 commit 86a7ceb

File tree

37 files changed

+673
-78
lines changed

37 files changed

+673
-78
lines changed

source/use-case/todoapp/event-model/event-emitter/src/model/TodoListModel.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,9 @@ export class TodoListModel extends EventEmitter {
2828
/**
2929
* TodoListの状態が更新されたときに呼び出されるリスナー関数を登録する
3030
* @param {Function} listener
31-
* @returns {Function} イベントリスナーの登録を解除する関数を返す
3231
*/
3332
onChange(listener) {
3433
this.addEventLister("change", listener);
35-
return () => {
36-
this.removeEventLister("change", listener);
37-
};
3834
}
3935

4036
/**

source/use-case/todoapp/event-model/event-emitter/src/view/html-util.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function htmlToElement(html) {
1515

1616
/**
1717
* HTML文字列からDOM Nodeを作成して返す
18-
* @return {HTMLElement}
18+
* @return {Element}
1919
*/
2020
export function element(strings, ...values) {
2121
const htmlString = strings.reduce((result, string, i) => {
@@ -31,8 +31,8 @@ export function element(strings, ...values) {
3131

3232
/**
3333
* コンテナ要素の中身をbodyElementで上書きする
34-
* @param {HTMLElement} bodyElement コンテナ要素の中身となる要素
35-
* @param {HTMLElement} containerElement コンテナ要素
34+
* @param {Element} bodyElement コンテナ要素の中身となる要素
35+
* @param {Element} containerElement コンテナ要素
3636
*/
3737
export function render(bodyElement, containerElement) {
3838
// rootElementの中身を空にする

source/use-case/todoapp/final/README.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,39 @@ Todoリストの表示は次の2つの部品(コンポーネント)から成
110110

111111
- <https://jsprimer.net/use-case/todoapp/final/final/>
112112

113+
実はこのTodoアプリはまだいくつかアプリケーションとして、完成していない部分があります。
114+
115+
入力欄でEnterキーを連打すると、空のTodoアイテムが追加されてしまうのは意図しない挙動です。
116+
また、`App#mount``TodoListModel#onChange`などのイベントリスナーを登録していますが、そのイベントリスナーを解除していません。
117+
このTodoアプリではあまり問題にはなりませんが、イベントリスナーは登録したら解除しないとメモリリークに繋がります。
118+
119+
そのため、余力がある人は次の残ったTodoを完成させてみてください。
120+
121+
- [ ] タイトルが空の場合は、フォームを送信してもTodoアイテムを追加できないようにする
122+
- [ ] `App#mount`でイベントリスナーを登録に対応して、`App#unmout`を追加しイベントリスナーを解除する
123+
124+
`App#mount`と対応する`App#unmount`を作成するTodoは、アプリケーションのライフサイクルを意識するという課題になります。
125+
ウェブページにも`load`というページ読み込みが完了した時に発生するイベントと、`unload`というページを破棄した時に発生するイベントがあります。
126+
Todoアプリも`mount``unmount`を実装し、次のようにウェブページのライフサイクルに合わせられます。
127+
128+
<!-- doctest:disable -->
129+
```js
130+
const app = new App();
131+
// ページのロードが完了したときのイベント
132+
window.addEventListener("load", () => {
133+
app.mount();
134+
});
135+
// ページがアンロードされたときのイベント
136+
window.addEventListener(" unload", () => {
137+
app.unmount();
138+
});
139+
```
140+
141+
残ったTodoも実装したものは、次のURLで確認できます。
142+
ぜひ、自分で実装してみてウェブページやアプリの動きについて考えてみて下さい。
143+
144+
- <https://jsprimer.net/use-case/todoapp/final/more/>
145+
113146
## Todoアプリのまとめ {#todo-conclusion}
114147

115148
今回は、Todoアプリを構成する要素をModelとViewという単位でモジュールに分けていました。
@@ -120,9 +153,11 @@ Todoリストの表示は次の2つの部品(コンポーネント)から成
120153
さまざまなライブラリを使ったTodoアプリの実装が[TodoMVC][]と呼ばれるサイトにまとめられています。
121154
今回作成したTodoアプリはTodoMVCからフィルター機能などを削ったものをライブラリを使わずに実装しました。[^vanilajs]
122155

123-
現実ではライブラリを使わずウェブアプリケーションを実装することは少なくなってきています。
156+
現実では、ライブラリを全く使わずウェブアプリケーションを実装することは殆どありません。
157+
ライブラリを使うことで、`html-util.js`のようなものは自分で書く必要はなくなったり、最後の課題として残ったライフサイクルの問題などは解決しやすくなります。
158+
124159
しかし、ライブラリを使って開発する場合でも、第一部の基本文法や第二部のユースケースで紹介したようなJavaScriptの基礎は重要です。
125-
なぜならライブラリもこれらの基礎の上に実装されているためです
160+
なぜならライブラリも、これらの基礎の上に実装されているためです
126161

127162
また作るアプリケーションの種類や目的によって適切なライブラリは異なります。
128163
ライブラリによっては魔法のような機能を提供しているものもありますが、それらも何かしらの基礎となる技術があることは覚えておいてください。

source/use-case/todoapp/final/create-view/src/model/TodoItemModel.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ let todoIdx = 0;
33

44
export class TodoItemModel {
55
/**
6-
* @param {string} title Todoアイテムのタイトル
7-
* @param {boolean} completed Todoアイテムが完了済みならばtrue、そうでない場合はfalse
6+
* `title`: Todoアイテムのタイトル
7+
* `completed`: Todoアイテムが完了済みならばtrue、そうでない場合はfalse
8+
* @param {{ title: string, completed: boolean }}
89
*/
910
constructor({ title, completed }) {
1011
// idは自動的に連番となりそれぞれのインスタンス毎に異なるものとする

source/use-case/todoapp/final/create-view/src/model/TodoListModel.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,9 @@ export class TodoListModel extends EventEmitter {
2828
/**
2929
* TodoListの状態が更新されたときに呼び出されるリスナー関数を登録する
3030
* @param {Function} listener
31-
* @returns {Function} イベントリスナーの登録を解除する関数を返す
3231
*/
3332
onChange(listener) {
3433
this.addEventLister("change", listener);
35-
return () => {
36-
this.removeEventLister("change", listener);
37-
};
3834
}
3935

4036
/**
@@ -55,8 +51,7 @@ export class TodoListModel extends EventEmitter {
5551

5652
/**
5753
* 指定したidのTodoItemのcompletedを更新する
58-
* @param {number} id
59-
* @param {boolean} completed
54+
* @param {{ id:number, completed: boolean }}
6055
*/
6156
updateTodo({ id, completed }) {
6257
const todoItem = this.items.find(todo => todo.id === id);
@@ -69,7 +64,7 @@ export class TodoListModel extends EventEmitter {
6964

7065
/**
7166
* 指定したidのTodoItemを削除する
72-
* @param {number} id
67+
* @param {{ id: number }}
7368
*/
7469
deleteTodo({ id }) {
7570
// `id`が一致するTodoItemを`this.items`から取り除き、削除する

source/use-case/todoapp/final/create-view/src/view/TodoItemView.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export class TodoItemView {
66
* @param {TodoItemModel} todoItem
77
* @param {function({id:string, completed: boolean})} onUpdateTodo チェックボックスの更新イベントリスナー
88
* @param {function({id:string)}} onDeleteTodo 削除ボタンのクリックイベントリスナー
9-
* @returns {HTMLElement}
9+
* @returns {Element}
1010
*/
1111
createElement(todoItem, { onUpdateTodo, onDeleteTodo }) {
1212
const todoItemElement = todoItem.completed

source/use-case/todoapp/final/create-view/src/view/TodoListView.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export class TodoListView {
77
* @param {TodoItemModel[]} todoItems TodoItemModelの配列
88
* @param {function({id:string, completed: boolean})} onUpdateTodo チェックボックスの更新イベントリスナー
99
* @param {function({id:string)}} onDeleteTodo 削除ボタンのクリックイベントリスナー
10-
* @returns {HTMLElement} TodoItemModelの配列に対応したリストのHTML要素
10+
* @returns {Element} TodoItemModelの配列に対応したリストのHTML要素
1111
*/
1212
createElement(todoItems, { onUpdateTodo, onDeleteTodo }) {
1313
const todoListElement = element`<ul />`;

source/use-case/todoapp/final/create-view/src/view/html-util.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function htmlToElement(html) {
1515

1616
/**
1717
* HTML文字列からDOM Nodeを作成して返す
18-
* @return {HTMLElement}
18+
* @return {Element}
1919
*/
2020
export function element(strings, ...values) {
2121
const htmlString = strings.reduce((result, string, i) => {
@@ -31,8 +31,8 @@ export function element(strings, ...values) {
3131

3232
/**
3333
* コンテナ要素の中身をbodyElementで上書きする
34-
* @param {HTMLElement} bodyElement コンテナ要素の中身となる要素
35-
* @param {HTMLElement} containerElement コンテナ要素
34+
* @param {Element} bodyElement コンテナ要素の中身となる要素
35+
* @param {Element} containerElement コンテナ要素
3636
*/
3737
export function render(bodyElement, containerElement) {
3838
// rootElementの中身を空にする

source/use-case/todoapp/final/final/src/App.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,15 @@ export class App {
1919

2020
/**
2121
* Todoの状態を更新時に呼ばれるリスナー関数
22-
* @param {number} id
23-
* @param {boolean} completed
22+
* @param {{ id:number, completed: boolean }}
2423
*/
2524
handleUpdate({ id, completed }) {
2625
this.todoListModel.updateTodo({ id, completed });
2726
};
2827

2928
/**
3029
* Todoを削除時に呼ばれるリスナー関数
31-
* @param {number} id
30+
* @param {{ id: number }}
3231
*/
3332
handleDelete({ id }) {
3433
this.todoListModel.deleteTodo({ id });
@@ -56,10 +55,7 @@ export class App {
5655

5756
formElement.addEventListener("submit", (event) => {
5857
event.preventDefault();
59-
this.todoListModel.addTodo(new TodoItemModel({
60-
title: inputElement.value,
61-
completed: false
62-
}));
58+
this.handleAdd(inputElement.value);
6359
inputElement.value = "";
6460
});
6561
}

source/use-case/todoapp/final/final/src/model/TodoItemModel.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ let todoIdx = 0;
33

44
export class TodoItemModel {
55
/**
6-
* @param {string} title Todoアイテムのタイトル
7-
* @param {boolean} completed Todoアイテムが完了済みならばtrue、そうでない場合はfalse
6+
* `title`: Todoアイテムのタイトル
7+
* `completed`: Todoアイテムが完了済みならばtrue、そうでない場合はfalse
8+
* @param {{ title: string, completed: boolean }}
89
*/
910
constructor({ title, completed }) {
1011
// idは自動的に連番となりそれぞれのインスタンス毎に異なるものとする

0 commit comments

Comments
 (0)