@fun-land/fun-web 0.2.0 → 0.3.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/README.md +45 -17
- package/dist/esm/src/dom.d.ts +16 -9
- package/dist/esm/src/dom.js +14 -4
- 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 +16 -9
- package/dist/src/dom.js +18 -6
- package/dist/src/dom.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +4 -2
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.publish.tsbuildinfo +1 -1
- package/examples/README.md +4 -4
- package/examples/counter/counter.ts +2 -2
- package/examples/todo-app/Todo.ts +33 -46
- package/examples/todo-app/todo-app.ts +32 -35
- package/package.json +9 -7
- package/src/dom.test.ts +23 -25
- package/src/dom.ts +44 -21
- package/src/index.ts +3 -1
- package/src/mount.test.ts +4 -4
- package/src/state.test.ts +44 -18
- package/tsconfig.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { viewed } from "accessor";
|
|
1
2
|
import {
|
|
2
3
|
h,
|
|
3
|
-
bindProperty,
|
|
4
|
-
on,
|
|
5
4
|
type Component,
|
|
6
5
|
type FunState,
|
|
6
|
+
enhance,
|
|
7
|
+
bindPropertyTo,
|
|
8
|
+
onTo,
|
|
7
9
|
} from "../../src/index";
|
|
8
10
|
|
|
9
11
|
export interface TodoState {
|
|
@@ -18,62 +20,47 @@ export interface TodoProps {
|
|
|
18
20
|
state: FunState<TodoState>;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
// a special Accessor that reads as string but writes as number
|
|
24
|
+
const stringNumberView = viewed(String, Number);
|
|
25
|
+
|
|
21
26
|
export const Todo: Component<TodoProps> = (signal, { state, removeItem }) => {
|
|
27
|
+
const priorityState = state.prop("priority").focus(stringNumberView);
|
|
22
28
|
// h is a much more ergonomic way of creating html but it's just document.createElement under the hood
|
|
23
|
-
const prioritySelect =
|
|
24
|
-
h("
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 }
|
|
29
|
+
const prioritySelect = enhance(
|
|
30
|
+
h("select", {}, [
|
|
31
|
+
h("option", { value: "0" }, "High"),
|
|
32
|
+
h("option", { value: "1" }, "Low"),
|
|
33
|
+
]),
|
|
34
|
+
bindPropertyTo("value", priorityState, signal),
|
|
35
|
+
// native event binding works but for easier event binding use `on` helper for better type inferrence and you can't forget to cleanup
|
|
36
|
+
onTo("change", (e) => priorityState.set(e.currentTarget.value), signal)
|
|
40
37
|
);
|
|
41
38
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"change",
|
|
49
|
-
(e) => {
|
|
50
|
-
state.prop("checked").set(e.currentTarget.checked);
|
|
51
|
-
},
|
|
52
|
-
signal
|
|
39
|
+
const checkedState = state.prop("checked");
|
|
40
|
+
const checkbox = enhance(
|
|
41
|
+
h("input", { type: "checkbox" }),
|
|
42
|
+
// use bindPropertyTo to automatically update a property when the focused state changes
|
|
43
|
+
bindPropertyTo("checked", checkedState, signal), // when state.checked updates the checkbox.checked updates
|
|
44
|
+
onTo("change", (e) => checkedState.set(e.currentTarget.checked), signal)
|
|
53
45
|
);
|
|
54
46
|
|
|
55
47
|
// 😎 or do both at the same time since they both return the element
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
signal
|
|
64
|
-
),
|
|
65
|
-
"input",
|
|
66
|
-
(e) => {
|
|
67
|
-
state.prop("label").set(e.currentTarget.value);
|
|
68
|
-
},
|
|
69
|
-
signal
|
|
48
|
+
const labelState = state.prop("label");
|
|
49
|
+
const labelInput = enhance(
|
|
50
|
+
h("input", {
|
|
51
|
+
type: "text",
|
|
52
|
+
}),
|
|
53
|
+
bindPropertyTo("value", labelState, signal),
|
|
54
|
+
onTo("input", (e) => labelState.set(e.currentTarget.value), signal)
|
|
70
55
|
);
|
|
71
56
|
|
|
72
57
|
return h("li", {}, [
|
|
73
58
|
checkbox,
|
|
74
59
|
prioritySelect,
|
|
75
60
|
labelInput,
|
|
76
|
-
|
|
77
|
-
|
|
61
|
+
enhance(
|
|
62
|
+
h("button", { textContent: "X" }),
|
|
63
|
+
onTo("click", removeItem, signal)
|
|
64
|
+
),
|
|
78
65
|
]);
|
|
79
66
|
};
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
h,
|
|
3
3
|
funState,
|
|
4
4
|
mount,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
bindPropertyTo,
|
|
6
|
+
onTo,
|
|
7
7
|
keyedChildren,
|
|
8
8
|
type Component,
|
|
9
|
-
|
|
9
|
+
enhance,
|
|
10
10
|
} from "../../src/index";
|
|
11
11
|
import { prepend, flow, Acc } from "@fun-land/accessor";
|
|
12
12
|
import { TodoState, Todo } from "./Todo";
|
|
@@ -36,9 +36,6 @@ const clearValue = stateFoci.prop("value").set("");
|
|
|
36
36
|
|
|
37
37
|
const markAllDone = stateFoci.prop("items").all().prop("checked").set(true);
|
|
38
38
|
|
|
39
|
-
const removeByKey = (key: string) =>
|
|
40
|
-
stateFoci.prop("items").mod((xs) => xs.filter((t) => t.key !== key));
|
|
41
|
-
|
|
42
39
|
// ===== Todo App Component =====
|
|
43
40
|
|
|
44
41
|
const initialState: TodoAppState = {
|
|
@@ -51,49 +48,49 @@ const initialState: TodoAppState = {
|
|
|
51
48
|
|
|
52
49
|
const TodoApp: Component = (signal) => {
|
|
53
50
|
const state = funState(initialState);
|
|
54
|
-
const input =
|
|
51
|
+
const input = enhance(
|
|
55
52
|
h("input", {
|
|
56
53
|
type: "text",
|
|
57
54
|
value: state.get().value,
|
|
58
55
|
placeholder: "Add a todo...",
|
|
59
56
|
}),
|
|
60
|
-
"value",
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
state.prop("value").set(e.currentTarget.value);
|
|
69
|
-
},
|
|
70
|
-
signal
|
|
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
|
+
)
|
|
71
65
|
);
|
|
72
66
|
|
|
73
67
|
const addBtn = h("button", { type: "submit", textContent: "Add" });
|
|
74
68
|
|
|
75
|
-
const form =
|
|
69
|
+
const form = enhance(
|
|
76
70
|
h("form", {}, [input, addBtn]),
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
e
|
|
80
|
-
|
|
81
|
-
state.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
)
|
|
85
81
|
);
|
|
86
82
|
|
|
87
83
|
// Because `on` returns the element you can pipe through
|
|
88
|
-
const markAllBtn =
|
|
84
|
+
const markAllBtn = enhance(
|
|
89
85
|
h("button", { textContent: "Mark All Done" }),
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
onTo(
|
|
87
|
+
"click",
|
|
88
|
+
() => {
|
|
89
|
+
state.mod(markAllDone);
|
|
90
|
+
},
|
|
91
|
+
signal
|
|
92
|
+
)
|
|
95
93
|
);
|
|
96
|
-
const allDoneText = h("span", { textContent: "" });
|
|
97
94
|
|
|
98
95
|
const todoList = h("ul", {});
|
|
99
96
|
keyedChildren(todoList, signal, state.prop("items"), (row) =>
|
|
@@ -106,7 +103,7 @@ const TodoApp: Component = (signal) => {
|
|
|
106
103
|
return h("div", { className: "todo-app" }, [
|
|
107
104
|
h("h1", { textContent: "Todo App" }),
|
|
108
105
|
form,
|
|
109
|
-
h("div", {}, [markAllBtn,
|
|
106
|
+
h("div", {}, [markAllBtn, h("span", { textContent: "" })]),
|
|
110
107
|
todoList,
|
|
111
108
|
]);
|
|
112
109
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fun-land/fun-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A web library for component-based development using fun-land",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -24,26 +24,28 @@
|
|
|
24
24
|
},
|
|
25
25
|
"sideEffects": false,
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@fun-land/accessor": "
|
|
27
|
+
"@fun-land/accessor": "workspace:*",
|
|
28
|
+
"@fun-land/fun-state": "workspace:*"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"@fun-land/accessor": "
|
|
31
|
+
"@fun-land/accessor": "4.0.2",
|
|
32
|
+
"@fun-land/fun-state": "9.0.0"
|
|
31
33
|
},
|
|
32
34
|
"scripts": {
|
|
33
35
|
"build-cjs": "tsc -p ./tsconfig.publish.json",
|
|
34
36
|
"build-esm": "tsc -p ./tsconfig.publish.json --module esnext --outDir dist/esm",
|
|
35
|
-
"build": "
|
|
37
|
+
"build": "pnpm run build-cjs && pnpm run build-esm",
|
|
36
38
|
"build:counter": "npx esbuild examples/counter/counter.ts --bundle --outfile=examples/counter/bundle.js --format=iife",
|
|
37
39
|
"build:todo": "npx esbuild examples/todo-app/todo-app.ts --bundle --outfile=examples/todo-app/todo-bundle.js --format=iife",
|
|
38
|
-
"build:examples": "
|
|
40
|
+
"build:examples": "pnpm run build:counter && pnpm run build:todo",
|
|
39
41
|
"lint": "eslint . --ext .ts",
|
|
40
42
|
"test": "jest",
|
|
41
43
|
"test-cover": "jest --coverage",
|
|
42
|
-
"prepublishOnly": "
|
|
44
|
+
"prepublishOnly": "pnpm run build"
|
|
43
45
|
},
|
|
44
46
|
"bugs": {
|
|
45
47
|
"url": "https://github.com/fun-land/fun-land/issues"
|
|
46
48
|
},
|
|
47
49
|
"license": "MIT",
|
|
48
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "fda8ca2be51c862ceaf3657b040ba7f60d354098"
|
|
49
51
|
}
|
package/src/dom.test.ts
CHANGED
|
@@ -2,10 +2,14 @@ import {
|
|
|
2
2
|
h,
|
|
3
3
|
text,
|
|
4
4
|
attr,
|
|
5
|
+
attrs,
|
|
6
|
+
append,
|
|
7
|
+
bindProperty,
|
|
5
8
|
addClass,
|
|
9
|
+
toggleClass,
|
|
6
10
|
removeClass,
|
|
7
11
|
on,
|
|
8
|
-
|
|
12
|
+
enhance,
|
|
9
13
|
keyedChildren,
|
|
10
14
|
$,
|
|
11
15
|
$$,
|
|
@@ -149,7 +153,6 @@ describe("attr()", () => {
|
|
|
149
153
|
describe("attrs()", () => {
|
|
150
154
|
it("should set multiple attributes", () => {
|
|
151
155
|
const el = document.createElement("div");
|
|
152
|
-
const { attrs } = require("./dom");
|
|
153
156
|
attrs({ "data-test": "value", "aria-label": "Test" })(el);
|
|
154
157
|
expect(el.getAttribute("data-test")).toBe("value");
|
|
155
158
|
expect(el.getAttribute("aria-label")).toBe("Test");
|
|
@@ -157,7 +160,6 @@ describe("attrs()", () => {
|
|
|
157
160
|
|
|
158
161
|
it("should return element for chaining", () => {
|
|
159
162
|
const el = document.createElement("div");
|
|
160
|
-
const { attrs } = require("./dom");
|
|
161
163
|
const result = attrs({ "data-test": "value" })(el);
|
|
162
164
|
expect(result).toBe(el);
|
|
163
165
|
});
|
|
@@ -203,14 +205,12 @@ describe("removeClass()", () => {
|
|
|
203
205
|
describe("toggleClass()", () => {
|
|
204
206
|
it("should toggle a class on", () => {
|
|
205
207
|
const el = document.createElement("div");
|
|
206
|
-
const { toggleClass } = require("./dom");
|
|
207
208
|
toggleClass("foo")(el);
|
|
208
209
|
expect(el.classList.contains("foo")).toBe(true);
|
|
209
210
|
});
|
|
210
211
|
|
|
211
212
|
it("should toggle a class off", () => {
|
|
212
213
|
const el = document.createElement("div");
|
|
213
|
-
const { toggleClass } = require("./dom");
|
|
214
214
|
el.classList.add("foo");
|
|
215
215
|
toggleClass("foo")(el);
|
|
216
216
|
expect(el.classList.contains("foo")).toBe(false);
|
|
@@ -218,7 +218,6 @@ describe("toggleClass()", () => {
|
|
|
218
218
|
|
|
219
219
|
it("should force add with true", () => {
|
|
220
220
|
const el = document.createElement("div");
|
|
221
|
-
const { toggleClass } = require("./dom");
|
|
222
221
|
toggleClass("foo", true)(el);
|
|
223
222
|
expect(el.classList.contains("foo")).toBe(true);
|
|
224
223
|
toggleClass("foo", true)(el);
|
|
@@ -227,7 +226,6 @@ describe("toggleClass()", () => {
|
|
|
227
226
|
|
|
228
227
|
it("should force remove with false", () => {
|
|
229
228
|
const el = document.createElement("div");
|
|
230
|
-
const { toggleClass } = require("./dom");
|
|
231
229
|
el.classList.add("foo");
|
|
232
230
|
toggleClass("foo", false)(el);
|
|
233
231
|
expect(el.classList.contains("foo")).toBe(false);
|
|
@@ -235,7 +233,6 @@ describe("toggleClass()", () => {
|
|
|
235
233
|
|
|
236
234
|
it("should return element for chaining", () => {
|
|
237
235
|
const el = document.createElement("div");
|
|
238
|
-
const { toggleClass } = require("./dom");
|
|
239
236
|
const result = toggleClass("foo")(el);
|
|
240
237
|
expect(result).toBe(el);
|
|
241
238
|
});
|
|
@@ -245,7 +242,6 @@ describe("append()", () => {
|
|
|
245
242
|
it("should append a single child", () => {
|
|
246
243
|
const parent = document.createElement("div");
|
|
247
244
|
const child = document.createElement("span");
|
|
248
|
-
const { append } = require("./dom");
|
|
249
245
|
append(child)(parent);
|
|
250
246
|
expect(parent.children.length).toBe(1);
|
|
251
247
|
expect(parent.children[0]).toBe(child);
|
|
@@ -255,7 +251,6 @@ describe("append()", () => {
|
|
|
255
251
|
const parent = document.createElement("div");
|
|
256
252
|
const child1 = document.createElement("span");
|
|
257
253
|
const child2 = document.createElement("p");
|
|
258
|
-
const { append } = require("./dom");
|
|
259
254
|
append(child1, child2)(parent);
|
|
260
255
|
expect(parent.children.length).toBe(2);
|
|
261
256
|
expect(parent.children[0]).toBe(child1);
|
|
@@ -265,7 +260,6 @@ describe("append()", () => {
|
|
|
265
260
|
it("should return element for chaining", () => {
|
|
266
261
|
const parent = document.createElement("div");
|
|
267
262
|
const child = document.createElement("span");
|
|
268
|
-
const { append } = require("./dom");
|
|
269
263
|
const result = append(child)(parent);
|
|
270
264
|
expect(result).toBe(parent);
|
|
271
265
|
});
|
|
@@ -275,7 +269,6 @@ describe("bindProperty()", () => {
|
|
|
275
269
|
it("should set initial property value from state", () => {
|
|
276
270
|
const el = document.createElement("input") as HTMLInputElement;
|
|
277
271
|
const controller = new AbortController();
|
|
278
|
-
const { bindProperty } = require("./dom");
|
|
279
272
|
|
|
280
273
|
const state = funState("hello");
|
|
281
274
|
bindProperty(el, "value", state, controller.signal);
|
|
@@ -287,7 +280,6 @@ describe("bindProperty()", () => {
|
|
|
287
280
|
it("should update property when state changes", () => {
|
|
288
281
|
const el = document.createElement("input") as HTMLInputElement;
|
|
289
282
|
const controller = new AbortController();
|
|
290
|
-
const { bindProperty } = require("./dom");
|
|
291
283
|
|
|
292
284
|
const state = funState("hello");
|
|
293
285
|
bindProperty(el, "value", state, controller.signal);
|
|
@@ -303,7 +295,6 @@ describe("bindProperty()", () => {
|
|
|
303
295
|
it("should stop updating after signal aborts", () => {
|
|
304
296
|
const el = document.createElement("input") as HTMLInputElement;
|
|
305
297
|
const controller = new AbortController();
|
|
306
|
-
const { bindProperty } = require("./dom");
|
|
307
298
|
|
|
308
299
|
const state = funState("hello");
|
|
309
300
|
bindProperty(el, "value", state, controller.signal);
|
|
@@ -317,7 +308,6 @@ describe("bindProperty()", () => {
|
|
|
317
308
|
it("should return element for chaining", () => {
|
|
318
309
|
const el = document.createElement("input") as HTMLInputElement;
|
|
319
310
|
const controller = new AbortController();
|
|
320
|
-
const { bindProperty } = require("./dom");
|
|
321
311
|
|
|
322
312
|
const state = funState("hello");
|
|
323
313
|
const result = bindProperty(el, "value", state, controller.signal);
|
|
@@ -362,11 +352,12 @@ describe("on()", () => {
|
|
|
362
352
|
describe("pipeEndo()", () => {
|
|
363
353
|
it("should apply functions in order", () => {
|
|
364
354
|
const el = document.createElement("div");
|
|
365
|
-
const result =
|
|
355
|
+
const result = enhance(
|
|
356
|
+
el,
|
|
366
357
|
text("Hello"),
|
|
367
358
|
addClass("foo", "bar"),
|
|
368
359
|
attr("data-test", "value")
|
|
369
|
-
)
|
|
360
|
+
);
|
|
370
361
|
|
|
371
362
|
expect(result).toBe(el);
|
|
372
363
|
expect(el.textContent).toBe("Hello");
|
|
@@ -396,7 +387,7 @@ function makeAbortSignal(): {
|
|
|
396
387
|
function setupKeyedChildrenWithKeyAwareRenderer(
|
|
397
388
|
parent: Element,
|
|
398
389
|
signal: AbortSignal,
|
|
399
|
-
listState: { get: () => Item[];
|
|
390
|
+
listState: { get: () => Item[]; watch: FunState<Item[]>["watch"] }
|
|
400
391
|
) {
|
|
401
392
|
const mountCountByKey = new Map<string, number>();
|
|
402
393
|
const abortCountByKey = new Map<string, number>();
|
|
@@ -512,12 +503,12 @@ describe("keyedChildren", () => {
|
|
|
512
503
|
focus: (acc: any) =>
|
|
513
504
|
({
|
|
514
505
|
get: () => acc.query(items)[0],
|
|
515
|
-
|
|
506
|
+
watch: (_s: AbortSignal, _cb: any) => void 0,
|
|
516
507
|
}) as any,
|
|
517
508
|
prop: () => {
|
|
518
509
|
throw new Error("not used");
|
|
519
510
|
},
|
|
520
|
-
|
|
511
|
+
watch: (sig: AbortSignal, cb: (items: Item[]) => void) => {
|
|
521
512
|
listeners.add(cb);
|
|
522
513
|
sig.addEventListener(
|
|
523
514
|
"abort",
|
|
@@ -527,6 +518,9 @@ describe("keyedChildren", () => {
|
|
|
527
518
|
{ once: true }
|
|
528
519
|
);
|
|
529
520
|
},
|
|
521
|
+
watchAll: (_sig: AbortSignal, _cb: (values: Item[][]) => void) => {
|
|
522
|
+
throw new Error("watchAll not used in these tests");
|
|
523
|
+
},
|
|
530
524
|
};
|
|
531
525
|
|
|
532
526
|
const { mountCountByKey } = setupKeyedChildrenWithKeyAwareRenderer(
|
|
@@ -589,12 +583,12 @@ describe("keyedChildren", () => {
|
|
|
589
583
|
focus: (acc: any) =>
|
|
590
584
|
({
|
|
591
585
|
get: () => acc.query(items)[0],
|
|
592
|
-
|
|
586
|
+
watch: (_s: AbortSignal, _cb: any) => void 0,
|
|
593
587
|
}) as any,
|
|
594
588
|
prop: () => {
|
|
595
589
|
throw new Error("not used");
|
|
596
590
|
},
|
|
597
|
-
|
|
591
|
+
watch: (sig: AbortSignal, cb: (items: Item[]) => void) => {
|
|
598
592
|
listeners.add(cb);
|
|
599
593
|
sig.addEventListener(
|
|
600
594
|
"abort",
|
|
@@ -604,6 +598,9 @@ describe("keyedChildren", () => {
|
|
|
604
598
|
{ once: true }
|
|
605
599
|
);
|
|
606
600
|
},
|
|
601
|
+
watchAll: (_sig: AbortSignal, _cb: (values: Item[][]) => void) => {
|
|
602
|
+
throw new Error("watchAll not used in these tests");
|
|
603
|
+
},
|
|
607
604
|
};
|
|
608
605
|
|
|
609
606
|
const { abortCountByKey } = setupKeyedChildrenWithKeyAwareRenderer(
|
|
@@ -634,16 +631,17 @@ describe("keyedChildren", () => {
|
|
|
634
631
|
|
|
635
632
|
// Subscribe and verify callback is called before abort
|
|
636
633
|
const callback = jest.fn();
|
|
637
|
-
list.
|
|
634
|
+
list.watch(signal, callback);
|
|
638
635
|
|
|
639
636
|
list.set([{ key: "a", label: "A2" }]);
|
|
640
|
-
|
|
637
|
+
// Called with initial value, then with updated value
|
|
638
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
641
639
|
|
|
642
640
|
// After abort, callback should not be called
|
|
643
641
|
controller.abort();
|
|
644
642
|
|
|
645
643
|
list.set([{ key: "a", label: "A3" }]);
|
|
646
|
-
expect(callback).toHaveBeenCalledTimes(
|
|
644
|
+
expect(callback).toHaveBeenCalledTimes(2); // Still 2, not called again
|
|
647
645
|
|
|
648
646
|
void container;
|
|
649
647
|
});
|
package/src/dom.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** DOM utilities for functional element creation and manipulation */
|
|
2
|
-
import { FunState } from "
|
|
2
|
+
import { type FunState } from "@fun-land/fun-state";
|
|
3
3
|
import type { ElementChild } from "./types";
|
|
4
4
|
import { filter } from "@fun-land/accessor";
|
|
5
5
|
|
|
6
|
+
export type Enhancer<El extends Element> = (element: El) => El;
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Create an HTML element with attributes and children
|
|
8
10
|
*
|
|
@@ -75,32 +77,32 @@ const appendChildren = (
|
|
|
75
77
|
*/
|
|
76
78
|
export const text =
|
|
77
79
|
(content: string | number) =>
|
|
78
|
-
(el:
|
|
80
|
+
<El extends Element>(el: El): El => {
|
|
79
81
|
el.textContent = String(content);
|
|
80
82
|
return el;
|
|
81
|
-
}
|
|
83
|
+
};;
|
|
82
84
|
|
|
83
85
|
/**
|
|
84
86
|
* Set an attribute on an element (returns element for chaining)
|
|
85
87
|
*/
|
|
86
88
|
export const attr =
|
|
87
89
|
(name: string, value: string) =>
|
|
88
|
-
(el:
|
|
90
|
+
<El extends Element>(el: El): El => {
|
|
89
91
|
el.setAttribute(name, value);
|
|
90
92
|
return el;
|
|
91
|
-
}
|
|
93
|
+
};;
|
|
92
94
|
|
|
93
95
|
/**
|
|
94
96
|
* Set multiple attributes on an element (returns element for chaining)
|
|
95
97
|
*/
|
|
96
98
|
export const attrs =
|
|
97
99
|
(obj: Record<string, string>) =>
|
|
98
|
-
(el:
|
|
100
|
+
<El extends Element>(el: El): El => {
|
|
99
101
|
Object.entries(obj).forEach(([name, value]) => {
|
|
100
102
|
el.setAttribute(name, value);
|
|
101
103
|
});
|
|
102
104
|
return el;
|
|
103
|
-
}
|
|
105
|
+
};;
|
|
104
106
|
|
|
105
107
|
export function bindProperty<E extends Element, K extends keyof E>(
|
|
106
108
|
el: E,
|
|
@@ -112,31 +114,40 @@ export function bindProperty<E extends Element, K extends keyof E>(
|
|
|
112
114
|
el[key] = fs.get();
|
|
113
115
|
|
|
114
116
|
// reactive sync
|
|
115
|
-
fs.
|
|
117
|
+
fs.watch(signal, (v: E[K]) => {
|
|
116
118
|
el[key] = v;
|
|
117
119
|
});
|
|
118
120
|
return el;
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
export const bindPropertyTo =
|
|
124
|
+
<E extends Element, K extends keyof E & string>(
|
|
125
|
+
key: K,
|
|
126
|
+
state: FunState<E[K]>,
|
|
127
|
+
signal: AbortSignal
|
|
128
|
+
) =>
|
|
129
|
+
(el: E): E =>
|
|
130
|
+
bindProperty(el, key, state, signal);
|
|
131
|
+
|
|
121
132
|
/**
|
|
122
133
|
* Add CSS classes to an element (returns element for chaining)
|
|
123
134
|
*/
|
|
124
135
|
export const addClass =
|
|
125
136
|
(...classes: string[]) =>
|
|
126
|
-
(el:
|
|
137
|
+
<El extends Element>(el: El): El => {
|
|
127
138
|
el.classList.add(...classes);
|
|
128
139
|
return el;
|
|
129
|
-
}
|
|
140
|
+
};;
|
|
130
141
|
|
|
131
142
|
/**
|
|
132
143
|
* Remove CSS classes from an element (returns element for chaining)
|
|
133
144
|
*/
|
|
134
145
|
export const removeClass =
|
|
135
146
|
(...classes: string[]) =>
|
|
136
|
-
(el:
|
|
147
|
+
<El extends Element>(el: El): El => {
|
|
137
148
|
el.classList.remove(...classes);
|
|
138
149
|
return el;
|
|
139
|
-
}
|
|
150
|
+
};;
|
|
140
151
|
|
|
141
152
|
/**
|
|
142
153
|
* Toggle a CSS class on an element (returns element for chaining)
|
|
@@ -150,13 +161,14 @@ export const toggleClass =
|
|
|
150
161
|
|
|
151
162
|
/**
|
|
152
163
|
* Append children to an element (returns parent for chaining)
|
|
164
|
+
* @returns {Enhancer}
|
|
153
165
|
*/
|
|
154
166
|
export const append =
|
|
155
167
|
(...children: Element[]) =>
|
|
156
|
-
(el:
|
|
168
|
+
<El extends Element>(el: El): El => {
|
|
157
169
|
children.forEach((child) => el.appendChild(child));
|
|
158
170
|
return el;
|
|
159
|
-
}
|
|
171
|
+
};;
|
|
160
172
|
|
|
161
173
|
/**
|
|
162
174
|
* Add event listener with required AbortSignal (returns element for chaining)
|
|
@@ -172,13 +184,23 @@ export const on = <E extends Element, K extends keyof HTMLElementEventMap>(
|
|
|
172
184
|
return el;
|
|
173
185
|
};
|
|
174
186
|
|
|
187
|
+
/** Enhancer version of `on()` */
|
|
188
|
+
export const onTo =
|
|
189
|
+
<E extends Element, K extends keyof HTMLElementEventMap>(
|
|
190
|
+
type: K,
|
|
191
|
+
handler: (ev: HTMLElementEventMap[K] & { currentTarget: E }) => void,
|
|
192
|
+
signal: AbortSignal
|
|
193
|
+
) =>
|
|
194
|
+
(el: E): E =>
|
|
195
|
+
on(el, type, handler, signal);
|
|
196
|
+
|
|
175
197
|
/**
|
|
176
|
-
*
|
|
198
|
+
* Apply enhancers to an HTMLElement.
|
|
177
199
|
*/
|
|
178
|
-
export const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
200
|
+
export const enhance = <El extends Element>(
|
|
201
|
+
x: El,
|
|
202
|
+
...fns: Array<Enhancer<El>>
|
|
203
|
+
) => fns.reduce((acc, fn) => fn(acc), x);
|
|
182
204
|
|
|
183
205
|
/**
|
|
184
206
|
*
|
|
@@ -259,7 +281,8 @@ export function keyedChildren<T extends Keyed>(
|
|
|
259
281
|
const el = renderRow({
|
|
260
282
|
signal: ctrl.signal,
|
|
261
283
|
state: itemState,
|
|
262
|
-
remove: () =>
|
|
284
|
+
remove: () =>
|
|
285
|
+
list.mod((list: T[]) => list.filter((t) => t.key !== k)),
|
|
263
286
|
});
|
|
264
287
|
rows.set(k, { key: k, el, ctrl });
|
|
265
288
|
}
|
|
@@ -278,7 +301,7 @@ export function keyedChildren<T extends Keyed>(
|
|
|
278
301
|
};
|
|
279
302
|
|
|
280
303
|
// Reconcile whenever the list changes; `subscribe` will unsubscribe on abort (per your fix).
|
|
281
|
-
list.
|
|
304
|
+
list.watch(signal, reconcile);
|
|
282
305
|
|
|
283
306
|
// Ensure all children clean up when parent aborts
|
|
284
307
|
signal.addEventListener("abort", dispose, { once: true });
|
package/src/index.ts
CHANGED
package/src/mount.test.ts
CHANGED
|
@@ -115,7 +115,7 @@ describe("mount()", () => {
|
|
|
115
115
|
const { state } = props;
|
|
116
116
|
const div = h("div", { textContent: String(state.get().count) });
|
|
117
117
|
|
|
118
|
-
state.prop("count").
|
|
118
|
+
state.prop("count").watch(signal, (count: number) => {
|
|
119
119
|
div.textContent = String(count);
|
|
120
120
|
});
|
|
121
121
|
|
|
@@ -149,7 +149,7 @@ describe("mount()", () => {
|
|
|
149
149
|
const Component: Component<Props> = (signal, props) => {
|
|
150
150
|
const { state } = props;
|
|
151
151
|
const div = h("div");
|
|
152
|
-
state.prop("count").
|
|
152
|
+
state.prop("count").watch(signal, callback);
|
|
153
153
|
return div;
|
|
154
154
|
};
|
|
155
155
|
|
|
@@ -190,11 +190,11 @@ describe("mount()", () => {
|
|
|
190
190
|
settingsState.get().theme
|
|
191
191
|
);
|
|
192
192
|
|
|
193
|
-
userState.prop("name").
|
|
193
|
+
userState.prop("name").watch(signal, (name: string) => {
|
|
194
194
|
nameEl.textContent = name;
|
|
195
195
|
});
|
|
196
196
|
|
|
197
|
-
settingsState.prop("theme").
|
|
197
|
+
settingsState.prop("theme").watch(signal, (theme: string) => {
|
|
198
198
|
themeEl.textContent = theme;
|
|
199
199
|
});
|
|
200
200
|
|