@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.
@@ -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 = 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 }
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 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
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 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
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
- // you can even inline to go tacit
77
- on(h("button", { textContent: "X" }), "click", removeItem, signal),
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
- bindProperty,
6
- on,
5
+ bindPropertyTo,
6
+ onTo,
7
7
  keyedChildren,
8
8
  type Component,
9
- type FunState,
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 = bindProperty(
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
- 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
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 = on(
69
+ const form = enhance(
76
70
  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
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 = on(
84
+ const markAllBtn = enhance(
89
85
  h("button", { textContent: "Mark All Done" }),
90
- "click",
91
- () => {
92
- state.mod(markAllDone);
93
- },
94
- signal
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, allDoneText]),
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.2.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": "^4.0.0"
27
+ "@fun-land/accessor": "workspace:*",
28
+ "@fun-land/fun-state": "workspace:*"
28
29
  },
29
30
  "devDependencies": {
30
- "@fun-land/accessor": "^4.0.1"
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": "yarn build-cjs && yarn build-esm",
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": "yarn build:counter && yarn build:todo",
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": "yarn build"
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": "3fc5d08b05d975dd25d77b7af91f9551be55f123"
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
- pipeEndo,
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 = pipeEndo(
355
+ const result = enhance(
356
+ el,
366
357
  text("Hello"),
367
358
  addClass("foo", "bar"),
368
359
  attr("data-test", "value")
369
- )(el);
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[]; subscribe: FunState<Item[]>["subscribe"] }
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
- subscribe: (_s: AbortSignal, _cb: any) => void 0,
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
- subscribe: (sig: AbortSignal, cb: (items: Item[]) => void) => {
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
- subscribe: (_s: AbortSignal, _cb: any) => void 0,
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
- subscribe: (sig: AbortSignal, cb: (items: Item[]) => void) => {
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.subscribe(signal, callback);
634
+ list.watch(signal, callback);
638
635
 
639
636
  list.set([{ key: "a", label: "A2" }]);
640
- expect(callback).toHaveBeenCalledTimes(1);
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(1); // Still 1, not called again
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 "./state";
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: Element): Element => {
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: Element): Element => {
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: Element): Element => {
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.subscribe(signal, (v: E[K]) => {
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: Element): Element => {
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: Element): Element => {
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: Element): Element => {
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
- * Functional composition - apply endomorphic functions (`<T>(x: T) => T`) left to right
198
+ * Apply enhancers to an HTMLElement.
177
199
  */
178
- export const pipeEndo =
179
- <T>(...fns: Array<(x: T) => T>) =>
180
- (x: T): T =>
181
- fns.reduce((acc, fn) => fn(acc), x);
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: () => list.mod((list) => list.filter((t) => t.key !== k)),
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.subscribe(signal, reconcile);
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
@@ -16,9 +16,11 @@ export {
16
16
  toggleClass,
17
17
  append,
18
18
  on,
19
+ onTo,
19
20
  bindProperty,
21
+ bindPropertyTo,
20
22
  keyedChildren,
21
- pipeEndo,
23
+ enhance,
22
24
  $,
23
25
  $$,
24
26
  } from "./dom";
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").subscribe(signal, (count: number) => {
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").subscribe(signal, callback);
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").subscribe(signal, (name: string) => {
193
+ userState.prop("name").watch(signal, (name: string) => {
194
194
  nameEl.textContent = name;
195
195
  });
196
196
 
197
- settingsState.prop("theme").subscribe(signal, (theme: string) => {
197
+ settingsState.prop("theme").watch(signal, (theme: string) => {
198
198
  themeEl.textContent = theme;
199
199
  });
200
200