/
step-final.tsx
148 lines (135 loc) · 4.42 KB
/
step-final.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { useState } from "react";
/** リスト表示の対象となる、個々のToDoを表す型。*/
export type TodoItem = {
/** 表示や操作の対象を識別するために利用する、全ての`TodoItem`の中で一意な値。 */
id: number;
/** ToDoの内容となる文字列。 */
text: string;
/** 完了すると`true`となる。 */
done: boolean;
};
type TodoListItemProps = {
item: TodoItem;
onCheck: (checked: boolean) => void;
onDelete: () => void;
};
/** ToDoリストの個々のToDoとなるReactコンポーネント。 */
function TodoListItem({ item, onCheck, onDelete }: TodoListItemProps) {
return (
<div className="TodoItem">
<input
type="checkbox"
checked={item.done}
onChange={(ev) => onCheck(ev.currentTarget.checked)}
/>
<p style={{ textDecoration: item.done ? "line-through" : "none" }}>
{item.text}
</p>
<button className="button-small" onClick={() => onDelete()}>
×
</button>
</div>
);
}
type CreateTodoFormProps = {
onSubmit: (text: string) => void;
};
/** 新しくToDoを追加するためのフォームとなるReactコンポーネント。 */
function CreateTodoForm({ onSubmit }: CreateTodoFormProps) {
const [text, setText] = useState("");
return (
<div className="CreateTodoForm">
<input
placeholder="新しいTodo"
size={60}
value={text}
onChange={(ev) => setText(ev.currentTarget.value)}
/>
<button onClick={() => onSubmit(text)}>追加</button>
</div>
);
}
type ValueViewerProps = {
value: any;
};
/** `value`の内容を`JSON.stringify`して表示する、動作確認用コンポーネント。 */
function ValueViewer({ value }: ValueViewerProps) {
return (
<pre className="ValueViewer">{JSON.stringify(value, undefined, 2)}</pre>
);
}
/** ToDoリストの初期値。 */
const INITIAL_TODO: TodoItem[] = [
{ id: 1, text: "todo-item-1", done: false },
{ id: 2, text: "todo-item-2", done: true },
];
/**
* ID用途に重複しなさそうな数値を適当に生成する。
* 今回は適当にUnix Epoch(1970-01-01)からの経過ミリ秒を利用した。
*/
const generateId = () => Date.now();
/** ToDoのStateとそれに対する操作をまとめたカスタムHook。 */
const useTodoState = () => {
const [todoItems, setTodoItems] = useState(INITIAL_TODO);
const createItem = (text: string) => {
setTodoItems([...todoItems, { id: generateId(), text, done: false }]);
};
const updateItem = (newItem: TodoItem) => {
setTodoItems(
todoItems.map((item) => (item.id === newItem.id ? newItem : item)),
);
};
const deleteItem = (id: number) => {
setTodoItems(todoItems.filter((item) => item.id !== id));
};
return [todoItems, createItem, updateItem, deleteItem] as const;
};
/** アプリケーション本体となるReactコンポーネント。 */
export default function App() {
const [todoItems, createItem, updateItem, deleteItem] = useTodoState();
const [keyword, setKeyword] = useState("");
const [showingDone, setShowingDone] = useState(true);
const filteredTodoItems = todoItems.filter((item) => {
if (!showingDone && item.done) return false;
return item.text.includes(keyword);
});
return (
<div className="App">
<h1>ToDo</h1>
<div className="App_todo-list-control">
<input
placeholder="キーワードフィルタ"
value={keyword}
onChange={(ev) => setKeyword(ev.target.value)}
/>
<input
id="showing-done"
type="checkbox"
checked={showingDone}
onChange={(ev) => setShowingDone(ev.target.checked)}
/>
<label htmlFor="showing-done">完了したものも表示する</label>
</div>
{filteredTodoItems.length === 0 ? (
<div className="dimmed">該当するToDoはありません</div>
) : (
<div className="App_todo-list">
{filteredTodoItems.map((item) => (
<TodoListItem
key={item.id}
item={item}
onCheck={(checked) => {
updateItem({ ...item, done: checked });
}}
onDelete={() => deleteItem(item.id)}
/>
))}
</div>
)}
<CreateTodoForm onSubmit={(text) => createItem(text)} />
<ValueViewer
value={{ keyword, showingDone, todoItems, filteredTodoItems }}
/>
</div>
);
}