@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.
Files changed (71) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +515 -0
  3. package/coverage/clover.xml +136 -0
  4. package/coverage/coverage-final.json +4 -0
  5. package/coverage/lcov-report/base.css +224 -0
  6. package/coverage/lcov-report/block-navigation.js +87 -0
  7. package/coverage/lcov-report/dom.ts.html +961 -0
  8. package/coverage/lcov-report/favicon.png +0 -0
  9. package/coverage/lcov-report/index.html +146 -0
  10. package/coverage/lcov-report/mount.ts.html +202 -0
  11. package/coverage/lcov-report/prettify.css +1 -0
  12. package/coverage/lcov-report/prettify.js +2 -0
  13. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  14. package/coverage/lcov-report/sorter.js +196 -0
  15. package/coverage/lcov-report/state.ts.html +91 -0
  16. package/coverage/lcov.info +260 -0
  17. package/dist/esm/src/dom.d.ts +85 -0
  18. package/dist/esm/src/dom.js +207 -0
  19. package/dist/esm/src/dom.js.map +1 -0
  20. package/dist/esm/src/index.d.ts +7 -0
  21. package/dist/esm/src/index.js +5 -0
  22. package/dist/esm/src/index.js.map +1 -0
  23. package/dist/esm/src/mount.d.ts +21 -0
  24. package/dist/esm/src/mount.js +27 -0
  25. package/dist/esm/src/mount.js.map +1 -0
  26. package/dist/esm/src/state.d.ts +2 -0
  27. package/dist/esm/src/state.js +3 -0
  28. package/dist/esm/src/state.js.map +1 -0
  29. package/dist/esm/src/types.d.ts +3 -0
  30. package/dist/esm/src/types.js +3 -0
  31. package/dist/esm/src/types.js.map +1 -0
  32. package/dist/esm/tsconfig.publish.tsbuildinfo +1 -0
  33. package/dist/src/dom.d.ts +85 -0
  34. package/dist/src/dom.js +224 -0
  35. package/dist/src/dom.js.map +1 -0
  36. package/dist/src/index.d.ts +7 -0
  37. package/dist/src/index.js +24 -0
  38. package/dist/src/index.js.map +1 -0
  39. package/dist/src/mount.d.ts +21 -0
  40. package/dist/src/mount.js +31 -0
  41. package/dist/src/mount.js.map +1 -0
  42. package/dist/src/state.d.ts +2 -0
  43. package/dist/src/state.js +7 -0
  44. package/dist/src/state.js.map +1 -0
  45. package/dist/src/types.d.ts +3 -0
  46. package/dist/src/types.js +4 -0
  47. package/dist/src/types.js.map +1 -0
  48. package/dist/tsconfig.publish.tsbuildinfo +1 -0
  49. package/eslint.config.js +54 -0
  50. package/examples/README.md +67 -0
  51. package/examples/counter/bundle.js +219 -0
  52. package/examples/counter/counter.ts +112 -0
  53. package/examples/counter/index.html +44 -0
  54. package/examples/todo-app/Todo.ts +79 -0
  55. package/examples/todo-app/index.html +142 -0
  56. package/examples/todo-app/todo-app.ts +120 -0
  57. package/examples/todo-app/todo-bundle.js +410 -0
  58. package/jest.config.js +5 -0
  59. package/package.json +49 -0
  60. package/src/dom.test.ts +768 -0
  61. package/src/dom.ts +296 -0
  62. package/src/index.ts +25 -0
  63. package/src/mount.test.ts +220 -0
  64. package/src/mount.ts +39 -0
  65. package/src/state.test.ts +225 -0
  66. package/src/state.ts +2 -0
  67. package/src/types.ts +9 -0
  68. package/tsconfig.json +16 -0
  69. package/tsconfig.publish.json +6 -0
  70. package/wip/hx-magic-properties-plan.md +575 -0
  71. package/wip/next.md +22 -0
@@ -0,0 +1,410 @@
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
+ })();
package/jest.config.js ADDED
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'jsdom',
4
+ collectCoverage: false
5
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@fun-land/fun-web",
3
+ "version": "0.2.0",
4
+ "description": "A web library for component-based development using fun-land",
5
+ "main": "dist/index.js",
6
+ "module": "dist/esm/index.js",
7
+ "author": "jethro larson",
8
+ "maintainers": [
9
+ "jethrolarson <jethrolarson@gmail.com>"
10
+ ],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/fun-land/fun-land.git"
14
+ },
15
+ "homepage": "https://github.com/fun-land/fun-land/tree/main/packages/fun-web",
16
+ "keywords": [
17
+ "web",
18
+ "components",
19
+ "functional",
20
+ "fun-land"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "sideEffects": false,
26
+ "peerDependencies": {
27
+ "@fun-land/accessor": "^4.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@fun-land/accessor": "^4.0.1"
31
+ },
32
+ "scripts": {
33
+ "build-cjs": "tsc -p ./tsconfig.publish.json",
34
+ "build-esm": "tsc -p ./tsconfig.publish.json --module esnext --outDir dist/esm",
35
+ "build": "yarn build-cjs && yarn build-esm",
36
+ "build:counter": "npx esbuild examples/counter/counter.ts --bundle --outfile=examples/counter/bundle.js --format=iife",
37
+ "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",
39
+ "lint": "eslint . --ext .ts",
40
+ "test": "jest",
41
+ "test-cover": "jest --coverage",
42
+ "prepublishOnly": "yarn build"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/fun-land/fun-land/issues"
46
+ },
47
+ "license": "MIT",
48
+ "gitHead": "3fc5d08b05d975dd25d77b7af91f9551be55f123"
49
+ }