@fun-land/fun-web 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +9 -0
- package/README.md +515 -0
- package/coverage/clover.xml +136 -0
- package/coverage/coverage-final.json +4 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/dom.ts.html +961 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +146 -0
- package/coverage/lcov-report/mount.ts.html +202 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov-report/state.ts.html +91 -0
- package/coverage/lcov.info +260 -0
- package/dist/esm/src/dom.d.ts +85 -0
- package/dist/esm/src/dom.js +207 -0
- package/dist/esm/src/dom.js.map +1 -0
- package/dist/esm/src/index.d.ts +7 -0
- package/dist/esm/src/index.js +5 -0
- package/dist/esm/src/index.js.map +1 -0
- package/dist/esm/src/mount.d.ts +21 -0
- package/dist/esm/src/mount.js +27 -0
- package/dist/esm/src/mount.js.map +1 -0
- package/dist/esm/src/state.d.ts +2 -0
- package/dist/esm/src/state.js +3 -0
- package/dist/esm/src/state.js.map +1 -0
- package/dist/esm/src/types.d.ts +3 -0
- package/dist/esm/src/types.js +3 -0
- package/dist/esm/src/types.js.map +1 -0
- package/dist/esm/tsconfig.publish.tsbuildinfo +1 -0
- package/dist/src/dom.d.ts +85 -0
- package/dist/src/dom.js +224 -0
- package/dist/src/dom.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mount.d.ts +21 -0
- package/dist/src/mount.js +31 -0
- package/dist/src/mount.js.map +1 -0
- package/dist/src/state.d.ts +2 -0
- package/dist/src/state.js +7 -0
- package/dist/src/state.js.map +1 -0
- package/dist/src/types.d.ts +3 -0
- package/dist/src/types.js +4 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.publish.tsbuildinfo +1 -0
- package/eslint.config.js +54 -0
- package/examples/README.md +67 -0
- package/examples/counter/bundle.js +219 -0
- package/examples/counter/counter.ts +112 -0
- package/examples/counter/index.html +44 -0
- package/examples/todo-app/Todo.ts +79 -0
- package/examples/todo-app/index.html +142 -0
- package/examples/todo-app/todo-app.ts +120 -0
- package/examples/todo-app/todo-bundle.js +410 -0
- package/jest.config.js +5 -0
- package/package.json +49 -0
- package/src/dom.test.ts +768 -0
- package/src/dom.ts +296 -0
- package/src/index.ts +25 -0
- package/src/mount.test.ts +220 -0
- package/src/mount.ts +39 -0
- package/src/state.test.ts +225 -0
- package/src/state.ts +2 -0
- package/src/types.ts +9 -0
- package/tsconfig.json +16 -0
- package/tsconfig.publish.json +6 -0
- package/wip/hx-magic-properties-plan.md +575 -0
- package/wip/next.md +22 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Simple counter component demonstrating fun-web basics
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
h,
|
|
6
|
+
funState,
|
|
7
|
+
mount,
|
|
8
|
+
type Component,
|
|
9
|
+
type FunState,
|
|
10
|
+
} from "../../src/index";
|
|
11
|
+
import { prop } from "@fun-land/accessor";
|
|
12
|
+
|
|
13
|
+
// Component state (dynamic data)
|
|
14
|
+
interface CounterState {
|
|
15
|
+
count: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Component props (static configuration + state)
|
|
19
|
+
interface CounterProps {
|
|
20
|
+
label: string;
|
|
21
|
+
onReset: () => void;
|
|
22
|
+
state: FunState<CounterState>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Counter component - evaluates once, subscriptions handle updates
|
|
26
|
+
const Counter: Component<CounterProps> = (
|
|
27
|
+
signal,
|
|
28
|
+
{ state, onReset, label }
|
|
29
|
+
) => {
|
|
30
|
+
// Create DOM elements
|
|
31
|
+
const display = h(
|
|
32
|
+
"div",
|
|
33
|
+
{ className: "count-display" },
|
|
34
|
+
String(state.get().count)
|
|
35
|
+
);
|
|
36
|
+
const incrementBtn = h("button", { textContent: "+" });
|
|
37
|
+
const decrementBtn = h("button", { textContent: "-" });
|
|
38
|
+
const resetBtn = h("button", { textContent: "Reset" });
|
|
39
|
+
|
|
40
|
+
// Subscribe to state changes
|
|
41
|
+
state.prop("count").subscribe(signal, (count) => {
|
|
42
|
+
display.textContent = String(count);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Event listeners with cleanup
|
|
46
|
+
incrementBtn.addEventListener(
|
|
47
|
+
"click",
|
|
48
|
+
() => {
|
|
49
|
+
state.mod((s) => ({ count: s.count + 1 }));
|
|
50
|
+
},
|
|
51
|
+
{ signal }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
decrementBtn.addEventListener(
|
|
55
|
+
"click",
|
|
56
|
+
() => {
|
|
57
|
+
state.mod((s) => ({ count: s.count - 1 }));
|
|
58
|
+
},
|
|
59
|
+
{ signal }
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
resetBtn.addEventListener("click", onReset, { signal });
|
|
63
|
+
|
|
64
|
+
// Build and return DOM tree
|
|
65
|
+
return h("div", { className: "counter" }, [
|
|
66
|
+
h("h2", { textContent: label }),
|
|
67
|
+
display,
|
|
68
|
+
h("div", { className: "buttons" }, [incrementBtn, decrementBtn, resetBtn]),
|
|
69
|
+
]);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// App component demonstrating composition
|
|
73
|
+
interface AppState {
|
|
74
|
+
title: string;
|
|
75
|
+
counterValue: CounterState;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const App: Component = (signal) => {
|
|
79
|
+
const state = funState<AppState>({
|
|
80
|
+
title: "Fun-Web Counter Example",
|
|
81
|
+
counterValue: { count: 0 },
|
|
82
|
+
});
|
|
83
|
+
const heading = h("h1", { textContent: state.get().title });
|
|
84
|
+
|
|
85
|
+
// Subscribe to title changes
|
|
86
|
+
state.prop("title").subscribe(signal, (title) => {
|
|
87
|
+
heading.textContent = title;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Create child component with focused state
|
|
91
|
+
const counter = Counter(
|
|
92
|
+
signal, // pass same signal - cleanup cascades
|
|
93
|
+
{
|
|
94
|
+
label: "Click Counter",
|
|
95
|
+
onReset: () => state.prop("counterValue").set({ count: 0 }),
|
|
96
|
+
state: state.focus(prop<AppState>()("counterValue")),
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return h("div", { className: "app" }, [heading, counter]);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Usage
|
|
104
|
+
export const runExample = () => {
|
|
105
|
+
const container = document.getElementById("app");
|
|
106
|
+
if (!container) throw new Error("No #app element found");
|
|
107
|
+
|
|
108
|
+
const mounted = mount(App, {}, container);
|
|
109
|
+
|
|
110
|
+
// Return cleanup function
|
|
111
|
+
return () => mounted.unmount();
|
|
112
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>fun-web Counter Example</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
10
|
+
max-width: 600px;
|
|
11
|
+
margin: 50px auto;
|
|
12
|
+
padding: 20px;
|
|
13
|
+
background: #f5f5f5;
|
|
14
|
+
}
|
|
15
|
+
#app {
|
|
16
|
+
background: white;
|
|
17
|
+
padding: 40px;
|
|
18
|
+
border-radius: 8px;
|
|
19
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
20
|
+
text-align: center;
|
|
21
|
+
}
|
|
22
|
+
button {
|
|
23
|
+
font-size: 24px;
|
|
24
|
+
padding: 15px 30px;
|
|
25
|
+
background: #0066cc;
|
|
26
|
+
color: white;
|
|
27
|
+
border: none;
|
|
28
|
+
border-radius: 4px;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
transition: background 0.2s;
|
|
31
|
+
}
|
|
32
|
+
button:hover {
|
|
33
|
+
background: #0052a3;
|
|
34
|
+
}
|
|
35
|
+
button:active {
|
|
36
|
+
transform: translateY(1px);
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div id="app"></div>
|
|
42
|
+
<script src="bundle.js"></script>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
h,
|
|
3
|
+
bindProperty,
|
|
4
|
+
on,
|
|
5
|
+
type Component,
|
|
6
|
+
type FunState,
|
|
7
|
+
} from "../../src/index";
|
|
8
|
+
|
|
9
|
+
export interface TodoState {
|
|
10
|
+
key: string;
|
|
11
|
+
checked: boolean;
|
|
12
|
+
priority: number;
|
|
13
|
+
label: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TodoProps {
|
|
17
|
+
removeItem: () => void;
|
|
18
|
+
state: FunState<TodoState>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Todo: Component<TodoProps> = (signal, { state, removeItem }) => {
|
|
22
|
+
// h is a much more ergonomic way of creating html but it's just document.createElement under the hood
|
|
23
|
+
const prioritySelect = h("select", {}, [
|
|
24
|
+
h("option", { value: "0" }, "High"),
|
|
25
|
+
h("option", { value: "1" }, "Low"),
|
|
26
|
+
]);
|
|
27
|
+
// 😓 You can manually set initial state
|
|
28
|
+
prioritySelect.value = String(state.get().priority);
|
|
29
|
+
// 😓 and listen to when a state updates to update the element
|
|
30
|
+
state.prop("priority").subscribe(signal, (priority) => {
|
|
31
|
+
prioritySelect.value = String(priority);
|
|
32
|
+
});
|
|
33
|
+
// 😓 native event binding works but requires casting and dev must remember to pass signal so they don't leak memory
|
|
34
|
+
prioritySelect.addEventListener(
|
|
35
|
+
"change",
|
|
36
|
+
(e) => {
|
|
37
|
+
state.prop("priority").set(+(e.currentTarget as HTMLSelectElement).value);
|
|
38
|
+
},
|
|
39
|
+
{ signal }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const checkbox = h("input", { type: "checkbox" });
|
|
43
|
+
// 😎 For easier binding you can bind property to a reactive state
|
|
44
|
+
bindProperty(checkbox, "checked", state.prop("checked"), signal); // when state.checked updates the checkbox.checked updates
|
|
45
|
+
// 😎 For easier event binding use `on` helper for better type inferrence and you can't forget to cleanup
|
|
46
|
+
on(
|
|
47
|
+
checkbox,
|
|
48
|
+
"change",
|
|
49
|
+
(e) => {
|
|
50
|
+
state.prop("checked").set(e.currentTarget.checked);
|
|
51
|
+
},
|
|
52
|
+
signal
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// 😎 or do both at the same time since they both return the element
|
|
56
|
+
const labelInput = on(
|
|
57
|
+
bindProperty(
|
|
58
|
+
h("input", {
|
|
59
|
+
type: "text",
|
|
60
|
+
}),
|
|
61
|
+
"value",
|
|
62
|
+
state.prop("label"),
|
|
63
|
+
signal
|
|
64
|
+
),
|
|
65
|
+
"input",
|
|
66
|
+
(e) => {
|
|
67
|
+
state.prop("label").set(e.currentTarget.value);
|
|
68
|
+
},
|
|
69
|
+
signal
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return h("li", {}, [
|
|
73
|
+
checkbox,
|
|
74
|
+
prioritySelect,
|
|
75
|
+
labelInput,
|
|
76
|
+
// you can even inline to go tacit
|
|
77
|
+
on(h("button", { textContent: "X" }), "click", removeItem, signal),
|
|
78
|
+
]);
|
|
79
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>fun-web Todo App</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* CSS is all inlined here but I recommend using some CSS-in-JS library like typestyle or vanilla-extract so you can have your css colocated with components */
|
|
9
|
+
* {
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
}
|
|
12
|
+
body {
|
|
13
|
+
font-family:
|
|
14
|
+
system-ui,
|
|
15
|
+
-apple-system,
|
|
16
|
+
sans-serif;
|
|
17
|
+
max-width: 600px;
|
|
18
|
+
margin: 50px auto;
|
|
19
|
+
padding: 20px;
|
|
20
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
21
|
+
min-height: 100vh;
|
|
22
|
+
}
|
|
23
|
+
#app {
|
|
24
|
+
background: white;
|
|
25
|
+
padding: 30px;
|
|
26
|
+
border-radius: 12px;
|
|
27
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
|
28
|
+
}
|
|
29
|
+
.todo-app h1 {
|
|
30
|
+
margin: 0 0 20px 0;
|
|
31
|
+
color: #333;
|
|
32
|
+
font-size: 28px;
|
|
33
|
+
}
|
|
34
|
+
.todo-app form {
|
|
35
|
+
display: flex;
|
|
36
|
+
gap: 10px;
|
|
37
|
+
margin-bottom: 20px;
|
|
38
|
+
}
|
|
39
|
+
.todo-app form input[type="text"] {
|
|
40
|
+
flex: 1;
|
|
41
|
+
padding: 12px;
|
|
42
|
+
border: 2px solid #e0e0e0;
|
|
43
|
+
border-radius: 6px;
|
|
44
|
+
font-size: 14px;
|
|
45
|
+
transition: border-color 0.2s;
|
|
46
|
+
}
|
|
47
|
+
.todo-app form input[type="text"]:focus {
|
|
48
|
+
outline: none;
|
|
49
|
+
border-color: #667eea;
|
|
50
|
+
}
|
|
51
|
+
.todo-app form button {
|
|
52
|
+
padding: 12px 24px;
|
|
53
|
+
background: #667eea;
|
|
54
|
+
color: white;
|
|
55
|
+
border: none;
|
|
56
|
+
border-radius: 6px;
|
|
57
|
+
font-size: 14px;
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
transition: background 0.2s;
|
|
61
|
+
}
|
|
62
|
+
.todo-app form button:hover {
|
|
63
|
+
background: #5568d3;
|
|
64
|
+
}
|
|
65
|
+
.todo-app > div {
|
|
66
|
+
margin-bottom: 20px;
|
|
67
|
+
}
|
|
68
|
+
.todo-app > div button {
|
|
69
|
+
padding: 8px 16px;
|
|
70
|
+
background: #10b981;
|
|
71
|
+
color: white;
|
|
72
|
+
border: none;
|
|
73
|
+
border-radius: 6px;
|
|
74
|
+
font-size: 13px;
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
transition: background 0.2s;
|
|
77
|
+
}
|
|
78
|
+
.todo-app > div button:hover {
|
|
79
|
+
background: #059669;
|
|
80
|
+
}
|
|
81
|
+
.todo-app > div span {
|
|
82
|
+
margin-left: 10px;
|
|
83
|
+
color: #10b981;
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
}
|
|
86
|
+
.todo-app ul {
|
|
87
|
+
list-style: none;
|
|
88
|
+
padding: 0;
|
|
89
|
+
margin: 0;
|
|
90
|
+
}
|
|
91
|
+
.todo-app li {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: 10px;
|
|
95
|
+
padding: 12px;
|
|
96
|
+
background: #f9fafb;
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
margin-bottom: 8px;
|
|
99
|
+
transition: background 0.2s;
|
|
100
|
+
}
|
|
101
|
+
.todo-app li:hover {
|
|
102
|
+
background: #f3f4f6;
|
|
103
|
+
}
|
|
104
|
+
.todo-app li input[type="checkbox"] {
|
|
105
|
+
width: 18px;
|
|
106
|
+
height: 18px;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
}
|
|
109
|
+
.todo-app li select {
|
|
110
|
+
padding: 6px 8px;
|
|
111
|
+
border: 1px solid #e0e0e0;
|
|
112
|
+
border-radius: 4px;
|
|
113
|
+
font-size: 13px;
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
}
|
|
116
|
+
.todo-app li input[type="text"] {
|
|
117
|
+
flex: 1;
|
|
118
|
+
padding: 8px;
|
|
119
|
+
border: 1px solid #e0e0e0;
|
|
120
|
+
border-radius: 4px;
|
|
121
|
+
font-size: 14px;
|
|
122
|
+
}
|
|
123
|
+
.todo-app li button {
|
|
124
|
+
padding: 6px 12px;
|
|
125
|
+
background: #ef4444;
|
|
126
|
+
color: white;
|
|
127
|
+
border: none;
|
|
128
|
+
border-radius: 4px;
|
|
129
|
+
font-size: 13px;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
transition: background 0.2s;
|
|
132
|
+
}
|
|
133
|
+
.todo-app li button:hover {
|
|
134
|
+
background: #dc2626;
|
|
135
|
+
}
|
|
136
|
+
</style>
|
|
137
|
+
</head>
|
|
138
|
+
<body>
|
|
139
|
+
<div id="app"></div>
|
|
140
|
+
<script src="todo-bundle.js"></script>
|
|
141
|
+
</body>
|
|
142
|
+
</html>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
h,
|
|
3
|
+
funState,
|
|
4
|
+
mount,
|
|
5
|
+
bindProperty,
|
|
6
|
+
on,
|
|
7
|
+
keyedChildren,
|
|
8
|
+
type Component,
|
|
9
|
+
type FunState,
|
|
10
|
+
} from "../../src/index";
|
|
11
|
+
import { prepend, flow, Acc } from "@fun-land/accessor";
|
|
12
|
+
import { TodoState, Todo } from "./Todo";
|
|
13
|
+
|
|
14
|
+
// ===== Types =====
|
|
15
|
+
|
|
16
|
+
interface TodoAppState {
|
|
17
|
+
value: string;
|
|
18
|
+
items: TodoState[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ===== Accessors and helpers =====
|
|
22
|
+
|
|
23
|
+
const stateFoci = Acc<TodoAppState>();
|
|
24
|
+
|
|
25
|
+
const addItem = (state: TodoAppState): TodoAppState =>
|
|
26
|
+
stateFoci.prop("items").mod(
|
|
27
|
+
prepend<TodoState>({
|
|
28
|
+
checked: false,
|
|
29
|
+
label: state.value,
|
|
30
|
+
priority: 1,
|
|
31
|
+
key: crypto.randomUUID(),
|
|
32
|
+
})
|
|
33
|
+
)(state);
|
|
34
|
+
|
|
35
|
+
const clearValue = stateFoci.prop("value").set("");
|
|
36
|
+
|
|
37
|
+
const markAllDone = stateFoci.prop("items").all().prop("checked").set(true);
|
|
38
|
+
|
|
39
|
+
const removeByKey = (key: string) =>
|
|
40
|
+
stateFoci.prop("items").mod((xs) => xs.filter((t) => t.key !== key));
|
|
41
|
+
|
|
42
|
+
// ===== Todo App Component =====
|
|
43
|
+
|
|
44
|
+
const initialState: TodoAppState = {
|
|
45
|
+
value: "",
|
|
46
|
+
items: [
|
|
47
|
+
{ checked: false, label: "Learn fun-web", priority: 0, key: "asdf" },
|
|
48
|
+
{ checked: true, label: "Build something cool", priority: 1, key: "fdas" },
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const TodoApp: Component = (signal) => {
|
|
53
|
+
const state = funState(initialState);
|
|
54
|
+
const input = bindProperty(
|
|
55
|
+
h("input", {
|
|
56
|
+
type: "text",
|
|
57
|
+
value: state.get().value,
|
|
58
|
+
placeholder: "Add a todo...",
|
|
59
|
+
}),
|
|
60
|
+
"value",
|
|
61
|
+
state.prop("value"),
|
|
62
|
+
signal
|
|
63
|
+
);
|
|
64
|
+
on(
|
|
65
|
+
input,
|
|
66
|
+
"input",
|
|
67
|
+
(e) => {
|
|
68
|
+
state.prop("value").set(e.currentTarget.value);
|
|
69
|
+
},
|
|
70
|
+
signal
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const addBtn = h("button", { type: "submit", textContent: "Add" });
|
|
74
|
+
|
|
75
|
+
const form = on(
|
|
76
|
+
h("form", {}, [input, addBtn]),
|
|
77
|
+
"submit",
|
|
78
|
+
(e) => {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
if (state.get().value.trim()) {
|
|
81
|
+
state.mod(flow(addItem, clearValue));
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
signal
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Because `on` returns the element you can pipe through
|
|
88
|
+
const markAllBtn = on(
|
|
89
|
+
h("button", { textContent: "Mark All Done" }),
|
|
90
|
+
"click",
|
|
91
|
+
() => {
|
|
92
|
+
state.mod(markAllDone);
|
|
93
|
+
},
|
|
94
|
+
signal
|
|
95
|
+
);
|
|
96
|
+
const allDoneText = h("span", { textContent: "" });
|
|
97
|
+
|
|
98
|
+
const todoList = h("ul", {});
|
|
99
|
+
keyedChildren(todoList, signal, state.prop("items"), (row) =>
|
|
100
|
+
Todo(row.signal, {
|
|
101
|
+
removeItem: row.remove,
|
|
102
|
+
state: row.state,
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return h("div", { className: "todo-app" }, [
|
|
107
|
+
h("h1", { textContent: "Todo App" }),
|
|
108
|
+
form,
|
|
109
|
+
h("div", {}, [markAllBtn, allDoneText]),
|
|
110
|
+
todoList,
|
|
111
|
+
]);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// ===== Initialize =====
|
|
115
|
+
|
|
116
|
+
const app = document.getElementById("app");
|
|
117
|
+
|
|
118
|
+
if (app) {
|
|
119
|
+
mount(TodoApp, {}, app);
|
|
120
|
+
}
|