@fun-land/fun-web 0.3.0 → 0.3.2
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/README.md +65 -0
- package/dist/esm/src/dom.d.ts +10 -0
- package/dist/esm/src/dom.js +40 -0
- package/dist/esm/src/dom.js.map +1 -1
- package/dist/esm/src/index.d.ts +1 -1
- package/dist/esm/src/index.js +1 -1
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/tsconfig.publish.tsbuildinfo +1 -1
- package/dist/src/dom.d.ts +10 -0
- package/dist/src/dom.js +41 -0
- package/dist/src/dom.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.publish.tsbuildinfo +1 -1
- package/examples/counter/bundle.js +54 -62
- package/examples/todo-app/AddTodoForm.ts +46 -0
- package/examples/todo-app/DraggableTodoList.ts +173 -0
- package/examples/todo-app/README.md +172 -0
- package/examples/todo-app/Todo.ts +77 -30
- package/examples/todo-app/TodoApp.js +585 -0
- package/examples/todo-app/TodoApp.ts +70 -0
- package/examples/todo-app/TodoAppState.ts +32 -0
- package/examples/todo-app/TodoState.ts +5 -0
- package/examples/todo-app/index.html +2 -131
- package/examples/todo-app/todo-app.css +294 -0
- package/package.json +8 -6
- package/src/dom.test.ts +237 -0
- package/src/dom.ts +51 -0
- package/src/index.ts +1 -0
- package/wip/Screenshot 2026-01-13 at 11.56.08 AM.png +0 -0
- package/wip/Screenshot 2026-01-13 at 12.08.22 PM.png +0 -0
- package/wip/Screenshot 2026-01-13 at 12.11.14 PM.png +0 -0
- package/wip/Screenshot 2026-01-13 at 2.49.08 AM.png +0 -0
- package/examples/todo-app/todo-app.ts +0 -117
- package/examples/todo-app/todo-bundle.js +0 -410
package/src/dom.ts
CHANGED
|
@@ -312,6 +312,57 @@ export function keyedChildren<T extends Keyed>(
|
|
|
312
312
|
return { reconcile, dispose };
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Conditionally render a component based on a boolean FunState.
|
|
317
|
+
* Returns a container element that mounts/unmounts the component as the state changes.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* const showDetails = state(false);
|
|
321
|
+
* const detailsEl = renderWhen(showDetails, DetailsComponent, {id: 123}, signal);
|
|
322
|
+
* parent.appendChild(detailsEl);
|
|
323
|
+
*/
|
|
324
|
+
export function renderWhen<Props>(
|
|
325
|
+
state: FunState<boolean>,
|
|
326
|
+
comp: (signal: AbortSignal, props: Props) => Element,
|
|
327
|
+
props: Props,
|
|
328
|
+
signal: AbortSignal
|
|
329
|
+
): Element {
|
|
330
|
+
const container = document.createElement("span");
|
|
331
|
+
container.style.display = "contents";
|
|
332
|
+
let childCtrl: AbortController | null = null;
|
|
333
|
+
let childEl: Element | null = null;
|
|
334
|
+
|
|
335
|
+
const reconcile = () => {
|
|
336
|
+
const shouldRender = state.get();
|
|
337
|
+
|
|
338
|
+
if (shouldRender && !childEl) {
|
|
339
|
+
// Mount the component
|
|
340
|
+
childCtrl = new AbortController();
|
|
341
|
+
childEl = comp(childCtrl.signal, props);
|
|
342
|
+
container.appendChild(childEl);
|
|
343
|
+
} else if (!shouldRender && childEl) {
|
|
344
|
+
// Unmount the component
|
|
345
|
+
childCtrl?.abort();
|
|
346
|
+
childEl.remove();
|
|
347
|
+
childEl = null;
|
|
348
|
+
childCtrl = null;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// React to state changes
|
|
353
|
+
state.watch(signal, reconcile);
|
|
354
|
+
|
|
355
|
+
// Clean up when parent aborts
|
|
356
|
+
signal.addEventListener("abort", () => {
|
|
357
|
+
childCtrl?.abort();
|
|
358
|
+
}, { once: true });
|
|
359
|
+
|
|
360
|
+
// Initial render
|
|
361
|
+
reconcile();
|
|
362
|
+
|
|
363
|
+
return container;
|
|
364
|
+
}
|
|
365
|
+
|
|
315
366
|
export const $ = <T extends Element>(selector: string): T | undefined =>
|
|
316
367
|
document.querySelector<T>(selector) ?? undefined;
|
|
317
368
|
|
package/src/index.ts
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
h,
|
|
3
|
-
funState,
|
|
4
|
-
mount,
|
|
5
|
-
bindPropertyTo,
|
|
6
|
-
onTo,
|
|
7
|
-
keyedChildren,
|
|
8
|
-
type Component,
|
|
9
|
-
enhance,
|
|
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
|
-
// ===== Todo App Component =====
|
|
40
|
-
|
|
41
|
-
const initialState: TodoAppState = {
|
|
42
|
-
value: "",
|
|
43
|
-
items: [
|
|
44
|
-
{ checked: false, label: "Learn fun-web", priority: 0, key: "asdf" },
|
|
45
|
-
{ checked: true, label: "Build something cool", priority: 1, key: "fdas" },
|
|
46
|
-
],
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const TodoApp: Component = (signal) => {
|
|
50
|
-
const state = funState(initialState);
|
|
51
|
-
const input = enhance(
|
|
52
|
-
h("input", {
|
|
53
|
-
type: "text",
|
|
54
|
-
value: state.get().value,
|
|
55
|
-
placeholder: "Add a todo...",
|
|
56
|
-
}),
|
|
57
|
-
bindPropertyTo("value", state.prop("value"), signal),
|
|
58
|
-
onTo(
|
|
59
|
-
"input",
|
|
60
|
-
(e) => {
|
|
61
|
-
state.prop("value").set(e.currentTarget.value);
|
|
62
|
-
},
|
|
63
|
-
signal
|
|
64
|
-
)
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
const addBtn = h("button", { type: "submit", textContent: "Add" });
|
|
68
|
-
|
|
69
|
-
const form = enhance(
|
|
70
|
-
h("form", {}, [input, addBtn]),
|
|
71
|
-
onTo(
|
|
72
|
-
"submit",
|
|
73
|
-
(e) => {
|
|
74
|
-
e.preventDefault();
|
|
75
|
-
if (state.get().value.trim()) {
|
|
76
|
-
state.mod(flow(addItem, clearValue));
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
signal
|
|
80
|
-
)
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
// Because `on` returns the element you can pipe through
|
|
84
|
-
const markAllBtn = enhance(
|
|
85
|
-
h("button", { textContent: "Mark All Done" }),
|
|
86
|
-
onTo(
|
|
87
|
-
"click",
|
|
88
|
-
() => {
|
|
89
|
-
state.mod(markAllDone);
|
|
90
|
-
},
|
|
91
|
-
signal
|
|
92
|
-
)
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const todoList = h("ul", {});
|
|
96
|
-
keyedChildren(todoList, signal, state.prop("items"), (row) =>
|
|
97
|
-
Todo(row.signal, {
|
|
98
|
-
removeItem: row.remove,
|
|
99
|
-
state: row.state,
|
|
100
|
-
})
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
return h("div", { className: "todo-app" }, [
|
|
104
|
-
h("h1", { textContent: "Todo App" }),
|
|
105
|
-
form,
|
|
106
|
-
h("div", {}, [markAllBtn, h("span", { textContent: "" })]),
|
|
107
|
-
todoList,
|
|
108
|
-
]);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
// ===== Initialize =====
|
|
112
|
-
|
|
113
|
-
const app = document.getElementById("app");
|
|
114
|
-
|
|
115
|
-
if (app) {
|
|
116
|
-
mount(TodoApp, {}, app);
|
|
117
|
-
}
|
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
(() => {
|
|
3
|
-
// ../fun-state/node_modules/@fun-land/accessor/dist/esm/util.js
|
|
4
|
-
var flow = (f, g) => (x) => g(f(x));
|
|
5
|
-
var K = (a) => (_b) => a;
|
|
6
|
-
var flatmap = (f) => (xs) => {
|
|
7
|
-
let out = [];
|
|
8
|
-
for (const x of xs) {
|
|
9
|
-
out = out.concat(f(x));
|
|
10
|
-
}
|
|
11
|
-
return out;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
// ../fun-state/node_modules/@fun-land/accessor/dist/esm/accessor.js
|
|
15
|
-
var prop = () => (k) => ({
|
|
16
|
-
query: (obj) => [obj[k]],
|
|
17
|
-
mod: (transform) => (obj) => Object.assign(Object.assign({}, obj), { [k]: transform(obj[k]) })
|
|
18
|
-
});
|
|
19
|
-
var _comp = (acc1, acc2) => ({
|
|
20
|
-
query: flow(acc1.query, flatmap(acc2.query)),
|
|
21
|
-
mod: flow(acc2.mod, acc1.mod)
|
|
22
|
-
});
|
|
23
|
-
function comp(...accs) {
|
|
24
|
-
return accs.reduce(_comp);
|
|
25
|
-
}
|
|
26
|
-
var set = (acc) => flow(K, acc.mod);
|
|
27
|
-
|
|
28
|
-
// ../fun-state/dist/esm/src/FunState.js
|
|
29
|
-
var pureState = ({ getState, modState, subscribe }) => {
|
|
30
|
-
const setState = (v) => {
|
|
31
|
-
modState(() => v);
|
|
32
|
-
};
|
|
33
|
-
const focus = (acc) => subState({ getState, modState, subscribe }, acc);
|
|
34
|
-
const subscribeToState = (signal, callback) => {
|
|
35
|
-
const unsubscribe = subscribe(callback);
|
|
36
|
-
signal.addEventListener("abort", unsubscribe, { once: true });
|
|
37
|
-
};
|
|
38
|
-
const fs = {
|
|
39
|
-
get: getState,
|
|
40
|
-
query: (acc) => acc.query(getState()),
|
|
41
|
-
mod: modState,
|
|
42
|
-
set: setState,
|
|
43
|
-
focus,
|
|
44
|
-
prop: flow(prop(), focus),
|
|
45
|
-
subscribe: subscribeToState
|
|
46
|
-
};
|
|
47
|
-
return fs;
|
|
48
|
-
};
|
|
49
|
-
var subState = ({ getState, modState, subscribe }, accessor) => {
|
|
50
|
-
const props = prop();
|
|
51
|
-
const _get = () => accessor.query(getState())[0];
|
|
52
|
-
const _mod = flow(accessor.mod, modState);
|
|
53
|
-
function createFocusedSubscribe() {
|
|
54
|
-
return (listener) => {
|
|
55
|
-
let lastValue = _get();
|
|
56
|
-
return subscribe((parentState) => {
|
|
57
|
-
const newValue = accessor.query(parentState)[0];
|
|
58
|
-
if (newValue !== lastValue) {
|
|
59
|
-
lastValue = newValue;
|
|
60
|
-
listener(newValue);
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
const focus = (acc) => subState({ getState: _get, modState: _mod, subscribe: createFocusedSubscribe() }, acc);
|
|
66
|
-
const _prop = flow(props, focus);
|
|
67
|
-
const subscribeToState = (signal, callback) => {
|
|
68
|
-
let lastValue = _get();
|
|
69
|
-
const unsubscribe = subscribe((parentState) => {
|
|
70
|
-
const newValue = accessor.query(parentState)[0];
|
|
71
|
-
if (newValue !== lastValue) {
|
|
72
|
-
lastValue = newValue;
|
|
73
|
-
callback(newValue);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
signal.addEventListener("abort", unsubscribe, { once: true });
|
|
77
|
-
};
|
|
78
|
-
return {
|
|
79
|
-
get: _get,
|
|
80
|
-
query: (acc) => comp(accessor, acc).query(getState()),
|
|
81
|
-
mod: _mod,
|
|
82
|
-
set: flow(set(accessor), modState),
|
|
83
|
-
focus,
|
|
84
|
-
prop: _prop,
|
|
85
|
-
subscribe: subscribeToState
|
|
86
|
-
};
|
|
87
|
-
};
|
|
88
|
-
var standaloneEngine = (initialState2) => {
|
|
89
|
-
let state = initialState2;
|
|
90
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
91
|
-
const getState = () => state;
|
|
92
|
-
const modState = (f) => {
|
|
93
|
-
state = f(getState());
|
|
94
|
-
listeners.forEach((listener) => listener(state));
|
|
95
|
-
};
|
|
96
|
-
const subscribe = (listener) => {
|
|
97
|
-
listeners.add(listener);
|
|
98
|
-
return () => listeners.delete(listener);
|
|
99
|
-
};
|
|
100
|
-
return { getState, modState, subscribe };
|
|
101
|
-
};
|
|
102
|
-
var funState = (initialState2) => pureState(standaloneEngine(initialState2));
|
|
103
|
-
|
|
104
|
-
// ../accessor/dist/esm/util.js
|
|
105
|
-
var flow2 = (f, g) => (x) => g(f(x));
|
|
106
|
-
var K2 = (a) => (_b) => a;
|
|
107
|
-
var flatmap2 = (f) => (xs) => {
|
|
108
|
-
let out = [];
|
|
109
|
-
for (const x of xs) {
|
|
110
|
-
out = out.concat(f(x));
|
|
111
|
-
}
|
|
112
|
-
return out;
|
|
113
|
-
};
|
|
114
|
-
var prepend = (x) => (xs) => [x, ...xs];
|
|
115
|
-
|
|
116
|
-
// ../accessor/dist/esm/accessor.js
|
|
117
|
-
var prop2 = () => (k) => ({
|
|
118
|
-
query: (obj) => [obj[k]],
|
|
119
|
-
mod: (transform) => (obj) => Object.assign(Object.assign({}, obj), { [k]: transform(obj[k]) })
|
|
120
|
-
});
|
|
121
|
-
var index2 = (i) => ({
|
|
122
|
-
query: (s) => [s[i]],
|
|
123
|
-
mod: (f) => (xs) => xs.map((x, j) => i === j ? f(x) : x)
|
|
124
|
-
});
|
|
125
|
-
var _comp2 = (acc1, acc2) => ({
|
|
126
|
-
query: flow2(acc1.query, flatmap2(acc2.query)),
|
|
127
|
-
mod: flow2(acc2.mod, acc1.mod)
|
|
128
|
-
});
|
|
129
|
-
function comp2(...accs) {
|
|
130
|
-
return accs.reduce(_comp2);
|
|
131
|
-
}
|
|
132
|
-
var all2 = () => ({
|
|
133
|
-
query: (xs) => xs,
|
|
134
|
-
mod: (transform) => (xs) => xs.map(transform)
|
|
135
|
-
});
|
|
136
|
-
var filter = (pred) => ({
|
|
137
|
-
query: (xs) => xs.filter(pred),
|
|
138
|
-
mod: (transform) => (s) => s.map((x) => pred(x) ? transform(x) : x)
|
|
139
|
-
});
|
|
140
|
-
var unit = () => ({
|
|
141
|
-
query: (x) => [x],
|
|
142
|
-
mod: (transform) => (x) => transform(x)
|
|
143
|
-
});
|
|
144
|
-
var optional = () => ({
|
|
145
|
-
mod: (f) => (s) => s !== void 0 ? f(s) : s,
|
|
146
|
-
query: (s) => s !== void 0 ? [s] : []
|
|
147
|
-
});
|
|
148
|
-
function Acc(acc = unit()) {
|
|
149
|
-
return focusedAcc(acc);
|
|
150
|
-
}
|
|
151
|
-
var focusedAcc = (acc) => ({
|
|
152
|
-
query: (struct) => acc.query(struct),
|
|
153
|
-
get: (struct) => {
|
|
154
|
-
var _a;
|
|
155
|
-
return (_a = acc.query(struct)[0]) !== null && _a !== void 0 ? _a : void 0;
|
|
156
|
-
},
|
|
157
|
-
mod: acc.mod,
|
|
158
|
-
set: flow2(K2, acc.mod),
|
|
159
|
-
focus: (bcc) => focusedAcc(comp2(acc, bcc)),
|
|
160
|
-
prop(k) {
|
|
161
|
-
return this.focus(prop2()(k));
|
|
162
|
-
},
|
|
163
|
-
at: (idx) => focusedAcc(comp2(acc, index2(idx))),
|
|
164
|
-
all: () => focusedAcc(comp2(acc, all2())),
|
|
165
|
-
optional: () => focusedAcc(comp2(acc, optional()))
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// src/dom.ts
|
|
169
|
-
var h = (tag, attrs2, children) => {
|
|
170
|
-
const element = document.createElement(tag);
|
|
171
|
-
if (attrs2) {
|
|
172
|
-
for (const [key, value] of Object.entries(attrs2)) {
|
|
173
|
-
if (value == null)
|
|
174
|
-
continue;
|
|
175
|
-
if (key.startsWith("on") && typeof value === "function") {
|
|
176
|
-
const eventName = key.slice(2).toLowerCase();
|
|
177
|
-
element.addEventListener(eventName, value);
|
|
178
|
-
} else if (key.includes("-") || key === "role") {
|
|
179
|
-
element.setAttribute(key, String(value));
|
|
180
|
-
} else {
|
|
181
|
-
element[key] = value;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (children != null) {
|
|
186
|
-
appendChildren(element, children);
|
|
187
|
-
}
|
|
188
|
-
return element;
|
|
189
|
-
};
|
|
190
|
-
var appendChildren = (parent, children) => {
|
|
191
|
-
if (Array.isArray(children)) {
|
|
192
|
-
children.forEach((child) => appendChildren(parent, child));
|
|
193
|
-
} else if (children != null) {
|
|
194
|
-
if (typeof children === "string" || typeof children === "number") {
|
|
195
|
-
parent.appendChild(document.createTextNode(String(children)));
|
|
196
|
-
} else {
|
|
197
|
-
parent.appendChild(children);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
function bindProperty(el, key, fs, signal) {
|
|
202
|
-
el[key] = fs.get();
|
|
203
|
-
fs.subscribe(signal, (v) => {
|
|
204
|
-
el[key] = v;
|
|
205
|
-
});
|
|
206
|
-
return el;
|
|
207
|
-
}
|
|
208
|
-
var on = (el, type, handler, signal) => {
|
|
209
|
-
el.addEventListener(type, handler, { signal });
|
|
210
|
-
return el;
|
|
211
|
-
};
|
|
212
|
-
function keyedChildren(parent, signal, list, renderRow) {
|
|
213
|
-
const rows = /* @__PURE__ */ new Map();
|
|
214
|
-
const dispose = () => {
|
|
215
|
-
for (const row of rows.values()) {
|
|
216
|
-
row.ctrl.abort();
|
|
217
|
-
row.el.remove();
|
|
218
|
-
}
|
|
219
|
-
rows.clear();
|
|
220
|
-
};
|
|
221
|
-
const reconcile = () => {
|
|
222
|
-
const items = list.get();
|
|
223
|
-
const nextKeys = [];
|
|
224
|
-
const seen = /* @__PURE__ */ new Set();
|
|
225
|
-
for (const it of items) {
|
|
226
|
-
const k = it.key;
|
|
227
|
-
if (seen.has(k))
|
|
228
|
-
throw new Error(`keyedChildren: duplicate key "${k}"`);
|
|
229
|
-
seen.add(k);
|
|
230
|
-
nextKeys.push(k);
|
|
231
|
-
}
|
|
232
|
-
for (const [k, row] of rows) {
|
|
233
|
-
if (!seen.has(k)) {
|
|
234
|
-
row.ctrl.abort();
|
|
235
|
-
row.el.remove();
|
|
236
|
-
rows.delete(k);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
for (const k of nextKeys) {
|
|
240
|
-
if (!rows.has(k)) {
|
|
241
|
-
const ctrl = new AbortController();
|
|
242
|
-
const itemState = list.focus(filter((t) => t.key === k));
|
|
243
|
-
const el = renderRow(ctrl.signal, itemState);
|
|
244
|
-
rows.set(k, { key: k, el, ctrl });
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
const children = parent.children;
|
|
248
|
-
for (let i = 0; i < nextKeys.length; i++) {
|
|
249
|
-
const k = nextKeys[i];
|
|
250
|
-
const row = rows.get(k);
|
|
251
|
-
const currentAtI = children[i];
|
|
252
|
-
if (currentAtI !== row.el) {
|
|
253
|
-
parent.insertBefore(row.el, currentAtI ?? null);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
list.subscribe(signal, reconcile);
|
|
258
|
-
signal.addEventListener("abort", dispose, { once: true });
|
|
259
|
-
reconcile();
|
|
260
|
-
return { reconcile, dispose };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// src/mount.ts
|
|
264
|
-
var mount = (component, props, container) => {
|
|
265
|
-
const controller = new AbortController();
|
|
266
|
-
const element = component(controller.signal, props);
|
|
267
|
-
container.appendChild(element);
|
|
268
|
-
return {
|
|
269
|
-
element,
|
|
270
|
-
unmount: () => {
|
|
271
|
-
controller.abort();
|
|
272
|
-
element.remove();
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
// examples/todo-app/Todo.ts
|
|
278
|
-
var Todo = (signal, { state, removeItem }) => {
|
|
279
|
-
const prioritySelect = h("select", {}, [
|
|
280
|
-
h("option", { value: "0" }, "High"),
|
|
281
|
-
h("option", { value: "1" }, "Low")
|
|
282
|
-
]);
|
|
283
|
-
prioritySelect.value = String(state.get().priority);
|
|
284
|
-
state.prop("priority").subscribe(signal, (priority) => {
|
|
285
|
-
prioritySelect.value = String(priority);
|
|
286
|
-
});
|
|
287
|
-
prioritySelect.addEventListener(
|
|
288
|
-
"change",
|
|
289
|
-
(e) => {
|
|
290
|
-
state.prop("priority").set(+e.currentTarget.value);
|
|
291
|
-
},
|
|
292
|
-
{ signal }
|
|
293
|
-
);
|
|
294
|
-
const checkbox = h("input", { type: "checkbox" });
|
|
295
|
-
bindProperty(checkbox, "checked", state.prop("checked"), signal);
|
|
296
|
-
on(
|
|
297
|
-
checkbox,
|
|
298
|
-
"change",
|
|
299
|
-
(e) => {
|
|
300
|
-
state.prop("checked").set(e.currentTarget.checked);
|
|
301
|
-
},
|
|
302
|
-
signal
|
|
303
|
-
);
|
|
304
|
-
const labelInput = on(
|
|
305
|
-
bindProperty(
|
|
306
|
-
h("input", {
|
|
307
|
-
type: "text"
|
|
308
|
-
}),
|
|
309
|
-
"value",
|
|
310
|
-
state.prop("label"),
|
|
311
|
-
signal
|
|
312
|
-
),
|
|
313
|
-
"input",
|
|
314
|
-
(e) => {
|
|
315
|
-
state.prop("label").set(e.currentTarget.value);
|
|
316
|
-
},
|
|
317
|
-
signal
|
|
318
|
-
);
|
|
319
|
-
return h("li", {}, [
|
|
320
|
-
checkbox,
|
|
321
|
-
prioritySelect,
|
|
322
|
-
labelInput,
|
|
323
|
-
// you can even inline to go tacit
|
|
324
|
-
on(h("button", { textContent: "X" }), "click", removeItem, signal)
|
|
325
|
-
]);
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
// examples/todo-app/todo-app.ts
|
|
329
|
-
var stateFoci = Acc();
|
|
330
|
-
var addItem = (state) => stateFoci.prop("items").mod(
|
|
331
|
-
prepend({
|
|
332
|
-
checked: false,
|
|
333
|
-
label: state.value,
|
|
334
|
-
priority: 1,
|
|
335
|
-
key: crypto.randomUUID()
|
|
336
|
-
})
|
|
337
|
-
)(state);
|
|
338
|
-
var clearValue = stateFoci.prop("value").set("");
|
|
339
|
-
var markAllDone = stateFoci.prop("items").all().prop("checked").set(true);
|
|
340
|
-
var removeByKey = (key) => stateFoci.prop("items").mod((xs) => xs.filter((t) => t.key !== key));
|
|
341
|
-
var initialState = {
|
|
342
|
-
value: "",
|
|
343
|
-
items: [
|
|
344
|
-
{ checked: false, label: "Learn fun-web", priority: 0, key: "asdf" },
|
|
345
|
-
{ checked: true, label: "Build something cool", priority: 1, key: "fdas" }
|
|
346
|
-
]
|
|
347
|
-
};
|
|
348
|
-
var TodoApp = (signal) => {
|
|
349
|
-
const state = funState(initialState);
|
|
350
|
-
const input = bindProperty(
|
|
351
|
-
h("input", {
|
|
352
|
-
type: "text",
|
|
353
|
-
value: state.get().value,
|
|
354
|
-
placeholder: "Add a todo..."
|
|
355
|
-
}),
|
|
356
|
-
"value",
|
|
357
|
-
state.prop("value"),
|
|
358
|
-
signal
|
|
359
|
-
);
|
|
360
|
-
on(
|
|
361
|
-
input,
|
|
362
|
-
"input",
|
|
363
|
-
(e) => {
|
|
364
|
-
state.prop("value").set(e.currentTarget.value);
|
|
365
|
-
},
|
|
366
|
-
signal
|
|
367
|
-
);
|
|
368
|
-
const addBtn = h("button", { type: "submit", textContent: "Add" });
|
|
369
|
-
const form = on(
|
|
370
|
-
h("form", {}, [input, addBtn]),
|
|
371
|
-
"submit",
|
|
372
|
-
(e) => {
|
|
373
|
-
e.preventDefault();
|
|
374
|
-
if (state.get().value.trim()) {
|
|
375
|
-
state.mod(flow2(addItem, clearValue));
|
|
376
|
-
}
|
|
377
|
-
},
|
|
378
|
-
signal
|
|
379
|
-
);
|
|
380
|
-
const markAllBtn = on(
|
|
381
|
-
h("button", { textContent: "Mark All Done" }),
|
|
382
|
-
"click",
|
|
383
|
-
() => {
|
|
384
|
-
state.mod(markAllDone);
|
|
385
|
-
},
|
|
386
|
-
signal
|
|
387
|
-
);
|
|
388
|
-
const allDoneText = h("span", { textContent: "" });
|
|
389
|
-
const todoList = h("ul", {});
|
|
390
|
-
keyedChildren(
|
|
391
|
-
todoList,
|
|
392
|
-
signal,
|
|
393
|
-
state.prop("items"),
|
|
394
|
-
(rowSignal, todoState) => Todo(rowSignal, {
|
|
395
|
-
removeItem: () => state.mod(removeByKey(todoState.prop("key").get())),
|
|
396
|
-
state: todoState
|
|
397
|
-
})
|
|
398
|
-
);
|
|
399
|
-
return h("div", { className: "todo-app" }, [
|
|
400
|
-
h("h1", { textContent: "Todo App" }),
|
|
401
|
-
form,
|
|
402
|
-
h("div", {}, [markAllBtn, allDoneText]),
|
|
403
|
-
todoList
|
|
404
|
-
]);
|
|
405
|
-
};
|
|
406
|
-
var app = document.getElementById("app");
|
|
407
|
-
if (app) {
|
|
408
|
-
mount(TodoApp, {}, app);
|
|
409
|
-
}
|
|
410
|
-
})();
|