-
-
Notifications
You must be signed in to change notification settings - Fork 153
/
Copy pathindex.ts
109 lines (97 loc) · 2.83 KB
/
index.ts
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
// SPDX-License-Identifier: Apache-2.0
import type { IObjectOf } from "@thi.ng/api";
import { defAtom, defCursor, defHistory, defView } from "@thi.ng/atom";
import { start } from "@thi.ng/hdom";
import { map, pairs } from "@thi.ng/transducers";
interface Task {
done: boolean;
body: string;
}
type Tasks = IObjectOf<Task>;
interface State {
tasks: Tasks;
nextID: number;
}
// central app state (immutable)
const db = defAtom<State>({ tasks: {}, nextID: 0 });
// attach undo/redo history for `tasks` branch (arbitrary undo limit of 100 steps)
const tasks = defHistory(defCursor<State, "tasks">(db, ["tasks"]), 100);
// cursor for direct access to `nextID`
const nextID = defCursor(db, ["nextID"]);
// create derived view of tasks transformed into components
const items = defView(db, ["tasks"], (tasks: Tasks) => [
...map(([id, t]) => taskItem(id, t), pairs(tasks)),
]);
// state updaters
// each applies its updates via the history atom wrapper
// the `atom.setter` calls produce an immutable update function for given paths
const addNewTask = () =>
tasks.resetIn([nextID.swap((id) => id + 1)], { body: "", done: false });
const toggleTask = (id: string) =>
tasks.swapIn(<const>[id, "done"], (done) => !done);
const updateTask = (id: string, body: string) =>
tasks.resetIn(<const>[id, "body"], body);
// single task component
const taskItem = (id: string, task: Task): any[] => {
const checkAttribs = {
type: "checkbox",
checked: task.done,
onclick: () => toggleTask(id),
};
const textAttribs = {
type: "text",
placeholder: "todo...",
value: task.body,
onkeydown: (e: any) => e.key === "Enter" && e.target.blur(),
onblur: (e: any) => updateTask(id, (<HTMLInputElement>e.target).value),
};
return [
"div",
{ class: "task" + (task.done ? " done" : "") },
["input", checkAttribs],
["input", textAttribs],
];
};
// complete task list
// uses transducer to transform all tasks using above component function
const taskList = () => {
const _items = items.deref()!;
return _items.length
? ["div#tasks", _items]
: ["div", "nothing todo, get busy..."];
};
const button =
(onclick: EventListener, body: string) => (_: any, disabled: boolean) =>
["button", { onclick, disabled }, body];
const toolbar = () => {
const btAdd = button(() => addNewTask(), "+ Add");
const btUndo = button(() => tasks.undo(), "Undo");
const btRedo = button(() => tasks.redo(), "Redo");
return () => [
"div#toolbar",
[btAdd],
[btUndo, !tasks.canUndo()],
[btRedo, !tasks.canRedo()],
];
};
// static header component (simple array)
const header = [
"h1",
"My tasks",
[
"small",
"made with \u2764 ",
[
"a",
{
href: "https://github.com/thi-ng/umbrella/tree/develop/packages/hdom",
},
"@thi.ng/hdom",
],
],
];
const app = () => {
return ["div", header, toolbar(), taskList];
};
// kick off UI w/ root component function
start(app());