@fun-land/fun-web 0.3.2 → 0.5.0-alpha.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.
@@ -28,18 +28,10 @@
28
28
  function comp(...accs) {
29
29
  return accs.reduce(_comp);
30
30
  }
31
- var viewed = (toView, fromView) => ({
32
- query: (s) => [toView(s)],
33
- mod: (f) => (s) => fromView(f(toView(s)))
34
- });
35
31
  var all = () => ({
36
32
  query: (xs) => xs,
37
33
  mod: (transform) => (xs) => xs.map(transform)
38
34
  });
39
- var filter = (pred) => ({
40
- query: (xs) => xs.filter(pred),
41
- mod: (transform) => (s) => s.map((x) => pred(x) ? transform(x) : x)
42
- });
43
35
  var set = (acc) => flow(K, acc.mod);
44
36
  var get = (acc) => (s) => {
45
37
  var _a;
@@ -187,7 +179,29 @@
187
179
  };
188
180
  var onTo = (type, handler, signal) => (el) => on(el, type, handler, signal);
189
181
  var enhance = (x, ...fns) => fns.reduce((acc, fn) => fn(acc), x);
190
- function keyedChildren(parent, signal, list, renderRow) {
182
+ var keyOf = (keyAcc, item) => {
183
+ const k = keyAcc.query(item)[0];
184
+ if (k == null)
185
+ throw new Error("bindListChildren: key accessor returned no value");
186
+ return k;
187
+ };
188
+ var byKey = (keyAcc, key) => ({
189
+ query: (xs) => {
190
+ const hit = xs.find((t) => keyOf(keyAcc, t) === key);
191
+ return hit ? [hit] : [];
192
+ },
193
+ mod: (f) => (xs) => {
194
+ let found = false;
195
+ return xs.map((t) => {
196
+ if (keyOf(keyAcc, t) !== key) return t;
197
+ if (found) throw new Error(`bindListChildren: duplicate key "${key}"`);
198
+ found = true;
199
+ return f(t);
200
+ });
201
+ }
202
+ });
203
+ var bindListChildren = (options) => (parent) => {
204
+ const { signal, state: list, key: keyAcc, row: renderRow } = options;
191
205
  const rows = /* @__PURE__ */ new Map();
192
206
  const dispose = () => {
193
207
  for (const row of rows.values()) {
@@ -201,8 +215,9 @@
201
215
  const nextKeys = [];
202
216
  const seen = /* @__PURE__ */ new Set();
203
217
  for (const it of items) {
204
- const k = it.key;
205
- if (seen.has(k)) throw new Error(`keyedChildren: duplicate key "${k}"`);
218
+ const k = keyOf(keyAcc, it);
219
+ if (seen.has(k))
220
+ throw new Error(`bindListChildren: duplicate key "${k}"`);
206
221
  seen.add(k);
207
222
  nextKeys.push(k);
208
223
  }
@@ -216,13 +231,13 @@
216
231
  for (const k of nextKeys) {
217
232
  if (!rows.has(k)) {
218
233
  const ctrl = new AbortController();
219
- const itemState = list.focus(filter((t) => t.key === k));
234
+ const itemState = list.focus(byKey(keyAcc, k));
220
235
  const el = renderRow({
221
236
  signal: ctrl.signal,
222
237
  state: itemState,
223
- remove: () => list.mod((list2) => list2.filter((t) => t.key !== k))
238
+ remove: () => list.mod((xs) => xs.filter((t) => keyOf(keyAcc, t) !== k))
224
239
  });
225
- rows.set(k, { key: k, el, ctrl });
240
+ rows.set(k, { el, ctrl });
226
241
  }
227
242
  }
228
243
  const children = parent.children;
@@ -230,17 +245,51 @@
230
245
  const k = nextKeys[i];
231
246
  const row = rows.get(k);
232
247
  const currentAtI = children[i];
233
- if (currentAtI !== row.el) {
248
+ if (currentAtI !== row.el)
234
249
  parent.insertBefore(row.el, currentAtI ?? null);
235
- }
236
250
  }
237
251
  };
238
252
  list.watch(signal, reconcile);
239
253
  signal.addEventListener("abort", dispose, { once: true });
240
254
  reconcile();
241
- return { reconcile, dispose };
255
+ return parent;
256
+ };
257
+ function renderWhen(options) {
258
+ const {
259
+ state,
260
+ predicate = (x) => x,
261
+ component,
262
+ props,
263
+ signal
264
+ } = options;
265
+ const container = document.createElement("span");
266
+ container.style.display = "contents";
267
+ let childCtrl = null;
268
+ let childEl = null;
269
+ const reconcile = () => {
270
+ const shouldRender = predicate(state.get());
271
+ if (shouldRender && !childEl) {
272
+ childCtrl = new AbortController();
273
+ childEl = component(childCtrl.signal, props);
274
+ container.appendChild(childEl);
275
+ } else if (!shouldRender && childEl) {
276
+ childCtrl?.abort();
277
+ childEl.remove();
278
+ childEl = null;
279
+ childCtrl = null;
280
+ }
281
+ };
282
+ state.watch(signal, reconcile);
283
+ signal.addEventListener(
284
+ "abort",
285
+ () => {
286
+ childCtrl?.abort();
287
+ },
288
+ { once: true }
289
+ );
290
+ reconcile();
291
+ return container;
242
292
  }
243
- var $ = (selector) => document.querySelector(selector) ?? void 0;
244
293
 
245
294
  // src/mount.ts
246
295
  var mount = (component, props, container) => {
@@ -260,8 +309,8 @@
260
309
  var init_TodoAppState = () => ({
261
310
  value: "",
262
311
  items: [
263
- { checked: false, label: "Learn fun-web", priority: 0, key: "asdf" },
264
- { checked: true, label: "Build something cool", priority: 1, key: "fdas" }
312
+ { checked: false, label: "Learn fun-web", key: "asdf" },
313
+ { checked: true, label: "Build something cool", key: "fdas" }
265
314
  ]
266
315
  });
267
316
  var stateAcc = Acc();
@@ -272,27 +321,13 @@
272
321
  prepend({
273
322
  checked: false,
274
323
  label: state.value,
275
- priority: 1,
276
324
  key: crypto.randomUUID()
277
325
  })
278
326
  )(state);
279
327
 
280
- // examples/todo-app/TodoState.ts
281
- var stateAcc2 = Acc();
282
- var priorityAsString = stateAcc2.prop("priority").focus(viewed(String, Number));
283
-
284
328
  // examples/todo-app/Todo.ts
285
329
  var Todo = (signal, { state, removeItem, onDragStart, onDragEnd, onDragOver }) => {
286
330
  const todoData = state.get();
287
- const priorityState = state.focus(priorityAsString);
288
- const prioritySelect = enhance(
289
- h("select", {}, [
290
- h("option", { value: "0" }, "High"),
291
- h("option", { value: "1" }, "Low")
292
- ]),
293
- bindPropertyTo("value", priorityState, signal),
294
- onTo("change", (e) => priorityState.set(e.currentTarget.value), signal)
295
- );
296
331
  const checkedState = state.prop("checked");
297
332
  const checkbox = enhance(
298
333
  h("input", { type: "checkbox" }),
@@ -319,7 +354,6 @@
319
354
  const li = h("li", { className: "todo-item", "data-key": todoData.key }, [
320
355
  dragHandle,
321
356
  checkbox,
322
- prioritySelect,
323
357
  labelInput,
324
358
  deleteBtn
325
359
  ]);
@@ -372,7 +406,7 @@
372
406
 
373
407
  // examples/todo-app/DraggableTodoList.ts
374
408
  var ANIMATION_DURATION = 300;
375
- var getElementByKey = (key) => $(`[data-key="${key}"]`);
409
+ var getElementByKey = (key) => document.querySelector(`[data-key="${key}"]`);
376
410
  var DraggableTodoList = (signal, { items }) => {
377
411
  let draggedKey = null;
378
412
  let lastTargetKey = null;
@@ -382,12 +416,12 @@
382
416
  if (currentCount > previousItemCount) {
383
417
  const positions = /* @__PURE__ */ new Map();
384
418
  currentItems.forEach((item) => {
385
- const el = $(`[data-key="${item.key}"]`);
419
+ const el = getElementByKey(item.key);
386
420
  if (el) positions.set(item.key, el.getBoundingClientRect());
387
421
  });
388
422
  requestAnimationFrame(() => {
389
423
  positions.forEach((first, key) => {
390
- const el = $(`[data-key="${key}"]`);
424
+ const el = getElementByKey(key);
391
425
  if (!el) return;
392
426
  const last = el.getBoundingClientRect();
393
427
  const deltaY = first.top - last.top;
@@ -459,19 +493,19 @@
459
493
  lastTargetKey = null;
460
494
  };
461
495
  const todoList = h("ul", { className: "todo-list" });
462
- keyedChildren(
463
- todoList,
496
+ bindListChildren({
497
+ key: prop()("key"),
464
498
  signal,
465
- items,
466
- (row) => Todo(row.signal, {
499
+ state: items,
500
+ row: (row) => Todo(row.signal, {
467
501
  removeItem: () => {
468
- const element = $(`[data-key="${row.state.get().key}"]`);
502
+ const element = getElementByKey(row.state.get().key);
469
503
  if (element) {
470
504
  element.classList.add("todo-item-exit");
471
505
  setTimeout(() => {
472
506
  const positions = /* @__PURE__ */ new Map();
473
507
  items.get().forEach((item) => {
474
- const el = $(`[data-key="${item.key}"]`);
508
+ const el = getElementByKey(item.key);
475
509
  if (el) positions.set(item.key, el.getBoundingClientRect());
476
510
  });
477
511
  row.remove();
@@ -505,7 +539,7 @@
505
539
  onDragEnd: handleDragEnd,
506
540
  onDragOver: handleDragOver
507
541
  })
508
- );
542
+ })(todoList);
509
543
  return todoList;
510
544
  };
511
545
 
@@ -564,17 +598,26 @@
564
598
  signal
565
599
  )
566
600
  );
567
- const allDoneText = h("span", {
568
- textContent: "",
569
- className: "all-done-text"
570
- });
601
+ const AllDoneComponent = () => {
602
+ return h("span", {
603
+ textContent: "\u{1F389} All Done!",
604
+ className: "all-done-text"
605
+ });
606
+ };
607
+ const allDoneState = funState(false);
571
608
  state.focus(allCheckedAcc).watchAll(signal, (checks) => {
572
- allDoneText.textContent = checks.length > 0 && checks.every(Boolean) ? "\u{1F389} All Done!" : "";
609
+ allDoneState.set(checks.length > 0 && checks.every(Boolean));
610
+ });
611
+ const allDoneEl = renderWhen({
612
+ state: allDoneState,
613
+ component: AllDoneComponent,
614
+ props: {},
615
+ signal
573
616
  });
574
617
  return h("div", { className: "todo-app" }, [
575
618
  h("h1", { textContent: "Todo Example" }),
576
619
  AddTodoForm(signal, { state }),
577
- h("div", { className: "controls" }, [markAllBtn, allDoneText]),
620
+ h("div", { className: "controls" }, [markAllBtn, allDoneEl]),
578
621
  DraggableTodoList(signal, { items: state.prop("items") })
579
622
  ]);
580
623
  };
@@ -2,17 +2,12 @@ import {
2
2
  h,
3
3
  funState,
4
4
  mount,
5
- onTo,
5
+ on,
6
6
  type Component,
7
7
  enhance,
8
8
  renderWhen,
9
9
  } from "../../src/index";
10
- import {
11
- type TodoAppState,
12
- markAllDone,
13
- allCheckedAcc,
14
- init_TodoAppState,
15
- } from "./TodoAppState";
10
+ import { markAllDone, allCheckedAcc, init_TodoAppState } from "./TodoAppState";
16
11
  import { DraggableTodoList } from "./DraggableTodoList";
17
12
  import { AddTodoForm } from "./AddTodoForm";
18
13
 
@@ -28,7 +23,7 @@ const TodoApp: Component = (signal) => {
28
23
  textContent: "Mark All Done",
29
24
  className: "mark-all-btn",
30
25
  }),
31
- onTo(
26
+ on(
32
27
  "click",
33
28
  () => {
34
29
  state.mod(markAllDone);
@@ -51,7 +46,12 @@ const TodoApp: Component = (signal) => {
51
46
  allDoneState.set(checks.length > 0 && checks.every(Boolean));
52
47
  });
53
48
 
54
- const allDoneEl = renderWhen(allDoneState, AllDoneComponent, {}, signal);
49
+ const allDoneEl = renderWhen({
50
+ state: allDoneState,
51
+ component: AllDoneComponent,
52
+ props: {},
53
+ signal,
54
+ });
55
55
 
56
56
  return h("div", { className: "todo-app" }, [
57
57
  h("h1", { textContent: "Todo Example" }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fun-land/fun-web",
3
- "version": "0.3.2",
3
+ "version": "0.5.0-alpha.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",
@@ -49,5 +49,5 @@
49
49
  "url": "https://github.com/fun-land/fun-land/issues"
50
50
  },
51
51
  "license": "MIT",
52
- "gitHead": "0019c93649f251557a679a0a6da36d181f7120e4"
52
+ "gitHead": "68dd8a583be1733e0eec9070e8f4dfd6c8742fc0"
53
53
  }
package/src/dom.test.ts CHANGED
@@ -779,12 +779,12 @@ describe("renderWhen", () => {
779
779
  const controller = new AbortController();
780
780
  const showState = funState(true);
781
781
 
782
- const container = renderWhen(
783
- showState,
784
- TestComponent,
785
- { text: "Hello" },
786
- controller.signal
787
- );
782
+ const container = renderWhen({
783
+ state: showState,
784
+ component: TestComponent,
785
+ props: { text: "Hello" },
786
+ signal: controller.signal
787
+ });
788
788
 
789
789
  expect(container.children.length).toBe(1);
790
790
  expect(container.children[0].textContent).toBe("Hello");
@@ -799,12 +799,12 @@ describe("renderWhen", () => {
799
799
  const controller = new AbortController();
800
800
  const showState = funState(false);
801
801
 
802
- const container = renderWhen(
803
- showState,
804
- TestComponent,
805
- { text: "Hello" },
806
- controller.signal
807
- );
802
+ const container = renderWhen({
803
+ state: showState,
804
+ component: TestComponent,
805
+ props: { text: "Hello" },
806
+ signal: controller.signal
807
+ });
808
808
 
809
809
  expect(container.children.length).toBe(0);
810
810
 
@@ -815,12 +815,12 @@ describe("renderWhen", () => {
815
815
  const controller = new AbortController();
816
816
  const showState = funState(false);
817
817
 
818
- const container = renderWhen(
819
- showState,
820
- TestComponent,
821
- { text: "Hello" },
822
- controller.signal
823
- );
818
+ const container = renderWhen({
819
+ state: showState,
820
+ component: TestComponent,
821
+ props: { text: "Hello" },
822
+ signal: controller.signal
823
+ });
824
824
 
825
825
  expect(container.children.length).toBe(0);
826
826
 
@@ -836,12 +836,12 @@ describe("renderWhen", () => {
836
836
  const controller = new AbortController();
837
837
  const showState = funState(true);
838
838
 
839
- const container = renderWhen(
840
- showState,
841
- TestComponent,
842
- { text: "Hello" },
843
- controller.signal
844
- );
839
+ const container = renderWhen({
840
+ state: showState,
841
+ component: TestComponent,
842
+ props: { text: "Hello" },
843
+ signal: controller.signal
844
+ });
845
845
 
846
846
  const child = container.children[0] as HTMLElement;
847
847
  expect(container.children.length).toBe(1);
@@ -867,12 +867,12 @@ describe("renderWhen", () => {
867
867
  return h("div", null, props.text);
868
868
  };
869
869
 
870
- renderWhen(
871
- showState,
872
- ComponentWithAbortListener,
873
- { text: "Hello" },
874
- controller.signal
875
- );
870
+ renderWhen({
871
+ state: showState,
872
+ component: ComponentWithAbortListener,
873
+ props: { text: "Hello" },
874
+ signal: controller.signal
875
+ });
876
876
 
877
877
  expect(abortCallback).not.toHaveBeenCalled();
878
878
 
@@ -896,12 +896,12 @@ describe("renderWhen", () => {
896
896
  return h("div", null, props.text);
897
897
  };
898
898
 
899
- renderWhen(
900
- showState,
901
- ComponentWithAbortListener,
902
- { text: "Hello" },
903
- controller.signal
904
- );
899
+ renderWhen({
900
+ state: showState,
901
+ component: ComponentWithAbortListener,
902
+ props: { text: "Hello" },
903
+ signal: controller.signal
904
+ });
905
905
 
906
906
  expect(abortCallback).not.toHaveBeenCalled();
907
907
 
@@ -914,12 +914,12 @@ describe("renderWhen", () => {
914
914
  const controller = new AbortController();
915
915
  const showState = funState(false);
916
916
 
917
- const container = renderWhen(
918
- showState,
919
- TestComponent,
920
- { text: "Hello" },
921
- controller.signal
922
- );
917
+ const container = renderWhen({
918
+ state: showState,
919
+ component: TestComponent,
920
+ props: { text: "Hello" },
921
+ signal: controller.signal
922
+ });
923
923
 
924
924
  expect(container.children.length).toBe(0);
925
925
 
@@ -942,12 +942,12 @@ describe("renderWhen", () => {
942
942
  const controller = new AbortController();
943
943
  const showState = funState(true);
944
944
 
945
- const container = renderWhen(
946
- showState,
947
- TestComponent,
948
- { text: "Hello" },
949
- controller.signal
950
- ) as HTMLElement;
945
+ const container = renderWhen({
946
+ state: showState,
947
+ component: TestComponent,
948
+ props: { text: "Hello" },
949
+ signal: controller.signal
950
+ }) as HTMLElement;
951
951
 
952
952
  expect(container.style.display).toBe("contents");
953
953
 
@@ -958,12 +958,12 @@ describe("renderWhen", () => {
958
958
  const controller = new AbortController();
959
959
  const showState = funState(true);
960
960
 
961
- const container = renderWhen(
962
- showState,
963
- TestComponent,
964
- { text: "Hello" },
965
- controller.signal
966
- );
961
+ const container = renderWhen({
962
+ state: showState,
963
+ component: TestComponent,
964
+ props: { text: "Hello" },
965
+ signal: controller.signal
966
+ });
967
967
 
968
968
  expect(container.children.length).toBe(1);
969
969
 
@@ -989,15 +989,47 @@ describe("renderWhen", () => {
989
989
  );
990
990
  };
991
991
 
992
- const container = renderWhen(
993
- showState,
994
- PropsComponent,
995
- { text: "Count", count: 42 },
996
- controller.signal
997
- );
992
+ const container = renderWhen({
993
+ state: showState,
994
+ component: PropsComponent,
995
+ props: { text: "Count", count: 42 },
996
+ signal: controller.signal
997
+ });
998
998
 
999
999
  expect(container.children[0].textContent).toBe("Count: 42");
1000
1000
 
1001
1001
  controller.abort();
1002
1002
  });
1003
+
1004
+ test("should work with predicate function", () => {
1005
+ const controller = new AbortController();
1006
+ enum Status { Loading, Success, Error }
1007
+ const statusState = funState(Status.Loading);
1008
+
1009
+ const container = renderWhen({
1010
+ state: statusState,
1011
+ predicate: (status) => status === Status.Success,
1012
+ component: TestComponent,
1013
+ props: { text: "Success!" },
1014
+ signal: controller.signal
1015
+ });
1016
+
1017
+ // Should not render initially
1018
+ expect(container.children.length).toBe(0);
1019
+
1020
+ // Should render when predicate matches
1021
+ statusState.set(Status.Success);
1022
+ expect(container.children.length).toBe(1);
1023
+ expect(container.children[0].textContent).toBe("Success!");
1024
+
1025
+ // Should unmount when predicate doesn't match
1026
+ statusState.set(Status.Error);
1027
+ expect(container.children.length).toBe(0);
1028
+
1029
+ // Should mount again when predicate matches
1030
+ statusState.set(Status.Success);
1031
+ expect(container.children.length).toBe(1);
1032
+
1033
+ controller.abort();
1034
+ });
1003
1035
  });