@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,225 @@
1
+ import { funState } from "./state";
2
+ import { prop } from "@fun-land/accessor";
3
+
4
+ describe("funState", () => {
5
+ it("should create state with initial value", () => {
6
+ const state = funState({ count: 0 });
7
+ expect(state.get()).toEqual({ count: 0 });
8
+ });
9
+
10
+ it("should update state with set", () => {
11
+ const state = funState({ count: 0 });
12
+ state.set({ count: 5 });
13
+ expect(state.get()).toEqual({ count: 5 });
14
+ });
15
+
16
+ it("should update state with mod", () => {
17
+ const state = funState({ count: 0 });
18
+ state.mod((s: { count: number }) => ({ count: s.count + 1 }));
19
+ expect(state.get()).toEqual({ count: 1 });
20
+ });
21
+
22
+ it("should focus on property", () => {
23
+ const state = funState({ count: 0, name: "test" });
24
+ const countState = state.prop("count");
25
+ expect(countState.get()).toBe(0);
26
+ });
27
+
28
+ it("should update focused state", () => {
29
+ const state = funState({ count: 0, name: "test" });
30
+ const countState = state.prop("count");
31
+ countState.set(5);
32
+ expect(state.get()).toEqual({ count: 5, name: "test" });
33
+ });
34
+
35
+ describe("subscribe subscriptions", () => {
36
+ it("should call subscriber when state changes", () => {
37
+ const state = funState({ count: 0 });
38
+ const controller = new AbortController();
39
+ const callback = jest.fn();
40
+
41
+ state.subscribe(controller.signal, callback);
42
+ state.set({ count: 1 });
43
+
44
+ expect(callback).toHaveBeenCalledWith({ count: 1 });
45
+ });
46
+
47
+ it("should call subscriber multiple times", () => {
48
+ const state = funState({ count: 0 });
49
+ const controller = new AbortController();
50
+ const callback = jest.fn();
51
+
52
+ state.subscribe(controller.signal, callback);
53
+ state.set({ count: 1 });
54
+ state.set({ count: 2 });
55
+
56
+ expect(callback).toHaveBeenCalledTimes(2);
57
+ expect(callback).toHaveBeenNthCalledWith(1, { count: 1 });
58
+ expect(callback).toHaveBeenNthCalledWith(2, { count: 2 });
59
+ });
60
+
61
+ it("should call focused state subscriber only when that property changes", () => {
62
+ const state = funState({ count: 0, name: "test" });
63
+ const countState = state.prop("count");
64
+ const controller = new AbortController();
65
+ const callback = jest.fn();
66
+
67
+ countState.subscribe(controller.signal, callback);
68
+
69
+ // Change count - should trigger
70
+ state.set({ count: 1, name: "test" });
71
+ expect(callback).toHaveBeenCalledWith(1);
72
+
73
+ // Change name only - should not trigger
74
+ callback.mockClear();
75
+ state.set({ count: 1, name: "changed" });
76
+ expect(callback).not.toHaveBeenCalled();
77
+
78
+ // Change count again - should trigger
79
+ state.set({ count: 2, name: "changed" });
80
+ expect(callback).toHaveBeenCalledWith(2);
81
+ });
82
+
83
+ it("should support multiple subscribers", () => {
84
+ const state = funState({ count: 0 });
85
+ const controller = new AbortController();
86
+ const callback1 = jest.fn();
87
+ const callback2 = jest.fn();
88
+
89
+ state.subscribe(controller.signal, callback1);
90
+ state.subscribe(controller.signal, callback2);
91
+
92
+ state.set({ count: 1 });
93
+
94
+ expect(callback1).toHaveBeenCalledWith({ count: 1 });
95
+ expect(callback2).toHaveBeenCalledWith({ count: 1 });
96
+ });
97
+
98
+ it("should work with accessor-based focus", () => {
99
+ interface User {
100
+ name: string;
101
+ age: number;
102
+ }
103
+ const state = funState<User>({ name: "Alice", age: 30 });
104
+ const nameState = state.focus(prop<User>()("name"));
105
+ const controller = new AbortController();
106
+ const callback = jest.fn();
107
+
108
+ nameState.subscribe(controller.signal, callback);
109
+
110
+ state.set({ name: "Bob", age: 30 });
111
+
112
+ expect(callback).toHaveBeenCalledWith("Bob");
113
+ });
114
+ });
115
+
116
+ describe("query with accessors", () => {
117
+ it("should query state using accessor", () => {
118
+ const state = funState({ count: 5 });
119
+ const result = state.query(prop<{ count: number }>()("count"));
120
+ expect(result).toEqual([5]);
121
+ });
122
+
123
+ it("should query focused state using accessor", () => {
124
+ interface User {
125
+ profile: {
126
+ name: string;
127
+ age: number;
128
+ };
129
+ }
130
+ const state = funState<User>({
131
+ profile: { name: "Alice", age: 30 },
132
+ });
133
+ const profileState = state.prop("profile");
134
+ const result = profileState.query(prop<User["profile"]>()("name"));
135
+ expect(result).toEqual(["Alice"]);
136
+ });
137
+ });
138
+
139
+ describe("nested focus", () => {
140
+ it("should support focusing a focused state", () => {
141
+ interface AppState {
142
+ user: {
143
+ profile: {
144
+ name: string;
145
+ };
146
+ };
147
+ }
148
+ const state = funState<AppState>({
149
+ user: { profile: { name: "Alice" } },
150
+ });
151
+
152
+ const userState = state.prop("user");
153
+ const profileState = userState.prop("profile");
154
+ const nameState = profileState.prop("name");
155
+
156
+ expect(nameState.get()).toBe("Alice");
157
+
158
+ nameState.set("Bob");
159
+ expect(state.get().user.profile.name).toBe("Bob");
160
+ });
161
+
162
+ it("should trigger subscriptions on nested focused states", () => {
163
+ interface AppState {
164
+ user: {
165
+ profile: {
166
+ name: string;
167
+ };
168
+ };
169
+ }
170
+ const state = funState<AppState>({
171
+ user: { profile: { name: "Alice" } },
172
+ });
173
+
174
+ const userState = state.prop("user");
175
+ const profileState = userState.prop("profile");
176
+ const nameState = profileState.prop("name");
177
+
178
+ const controller = new AbortController();
179
+ const callback = jest.fn();
180
+
181
+ nameState.subscribe(controller.signal, callback);
182
+
183
+ state.set({ user: { profile: { name: "Bob" } } });
184
+
185
+ expect(callback).toHaveBeenCalledWith("Bob");
186
+
187
+ controller.abort();
188
+ });
189
+
190
+ it("should not trigger deeply focused subscription when unrelated field changes", () => {
191
+ interface AppState {
192
+ user: {
193
+ profile: {
194
+ name: string;
195
+ age: number;
196
+ };
197
+ };
198
+ }
199
+ const state = funState<AppState>({
200
+ user: { profile: { name: "Alice", age: 30 } },
201
+ });
202
+
203
+ const userState = state.prop("user");
204
+ const profileState = userState.prop("profile");
205
+ const nameState = profileState.prop("name");
206
+
207
+ const controller = new AbortController();
208
+ const callback = jest.fn();
209
+
210
+ nameState.subscribe(controller.signal, callback);
211
+
212
+ // Change age, not name
213
+ state.set({ user: { profile: { name: "Alice", age: 31 } } });
214
+
215
+ expect(callback).not.toHaveBeenCalled();
216
+
217
+ // Change name
218
+ state.set({ user: { profile: { name: "Bob", age: 31 } } });
219
+
220
+ expect(callback).toHaveBeenCalledWith("Bob");
221
+
222
+ controller.abort();
223
+ });
224
+ });
225
+ });
package/src/state.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Re-export FunState and funState from fun-state with subscribe support */
2
+ export { funState, type FunState } from "@fun-land/fun-state";
package/src/types.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** Core types for fun-web */
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
4
+ export type Component<Props = {}> = (
5
+ signal: AbortSignal,
6
+ props: Props
7
+ ) => Element
8
+
9
+ export type ElementChild = string | number | Element | null | undefined
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "incremental": true,
4
+ "strict": true,
5
+ "outDir": "dist",
6
+ "sourceMap": true,
7
+ "declaration": true,
8
+ "skipLibCheck": true,
9
+ "target": "ES2015",
10
+ "moduleResolution": "node",
11
+ "module": "CommonJS",
12
+ "composite": true,
13
+ "lib": ["ES2015", "DOM"]
14
+ },
15
+ "include": ["src/**/*.ts"]
16
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": [
4
+ "src/**/*.test.ts"
5
+ ]
6
+ }