@uistate/core 4.1.2 → 5.0.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 (150) hide show
  1. package/LICENSE-eventTest.md +26 -0
  2. package/README.md +409 -42
  3. package/cssState.js +32 -1
  4. package/eventState.js +58 -49
  5. package/eventTest.js +196 -0
  6. package/examples/001-counter/README.md +44 -0
  7. package/examples/001-counter/eventState.js +86 -0
  8. package/examples/001-counter/index.html +30 -0
  9. package/examples/002-counter-improved/README.md +44 -0
  10. package/examples/002-counter-improved/eventState.js +86 -0
  11. package/examples/002-counter-improved/index.html +44 -0
  12. package/examples/003-input-reactive/README.md +44 -0
  13. package/examples/003-input-reactive/eventState.js +86 -0
  14. package/examples/003-input-reactive/index.html +30 -0
  15. package/examples/004-computed-state/README.md +45 -0
  16. package/examples/004-computed-state/eventState.js +86 -0
  17. package/examples/004-computed-state/index.html +62 -0
  18. package/examples/005-conditional-rendering/README.md +42 -0
  19. package/examples/005-conditional-rendering/eventState.js +86 -0
  20. package/examples/005-conditional-rendering/index.html +36 -0
  21. package/examples/006-list-rendering/README.md +49 -0
  22. package/examples/006-list-rendering/eventState.js +86 -0
  23. package/examples/006-list-rendering/index.html +60 -0
  24. package/examples/007-form-validation/README.md +52 -0
  25. package/examples/007-form-validation/eventState.js +86 -0
  26. package/examples/007-form-validation/index.html +99 -0
  27. package/examples/008-undo-redo/README.md +70 -0
  28. package/examples/008-undo-redo/eventState.js +86 -0
  29. package/examples/008-undo-redo/index.html +105 -0
  30. package/examples/009-localStorage-side-effects/README.md +72 -0
  31. package/examples/009-localStorage-side-effects/eventState.js +86 -0
  32. package/examples/009-localStorage-side-effects/index.html +54 -0
  33. package/examples/010-decoupled-components/README.md +74 -0
  34. package/examples/010-decoupled-components/eventState.js +86 -0
  35. package/examples/010-decoupled-components/index.html +90 -0
  36. package/examples/011-async-patterns/README.md +98 -0
  37. package/examples/011-async-patterns/eventState.js +86 -0
  38. package/examples/011-async-patterns/index.html +129 -0
  39. package/examples/028-counter-improved-eventTest/LICENSE +55 -0
  40. package/examples/028-counter-improved-eventTest/README.md +131 -0
  41. package/examples/028-counter-improved-eventTest/app/store.js +9 -0
  42. package/examples/028-counter-improved-eventTest/index.html +49 -0
  43. package/examples/028-counter-improved-eventTest/runtime/core/behaviors.runtime.js +282 -0
  44. package/examples/028-counter-improved-eventTest/runtime/core/eventState.js +100 -0
  45. package/examples/028-counter-improved-eventTest/runtime/core/helpers.js +212 -0
  46. package/examples/028-counter-improved-eventTest/runtime/core/router.js +271 -0
  47. package/examples/028-counter-improved-eventTest/store.d.ts +8 -0
  48. package/examples/028-counter-improved-eventTest/style.css +170 -0
  49. package/examples/028-counter-improved-eventTest/tests/README.md +208 -0
  50. package/examples/028-counter-improved-eventTest/tests/counter.test.js +116 -0
  51. package/examples/028-counter-improved-eventTest/tests/eventTest.js +176 -0
  52. package/examples/028-counter-improved-eventTest/tests/generateTypes.js +168 -0
  53. package/examples/028-counter-improved-eventTest/tests/run.js +20 -0
  54. package/examples/030-todo-app-with-eventTest/LICENSE +55 -0
  55. package/examples/030-todo-app-with-eventTest/README.md +121 -0
  56. package/examples/030-todo-app-with-eventTest/app/router.js +25 -0
  57. package/examples/030-todo-app-with-eventTest/app/store.js +16 -0
  58. package/examples/030-todo-app-with-eventTest/app/views/home.js +11 -0
  59. package/examples/030-todo-app-with-eventTest/app/views/todoDemo.js +88 -0
  60. package/examples/030-todo-app-with-eventTest/index.html +65 -0
  61. package/examples/030-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  62. package/examples/030-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  63. package/examples/030-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  64. package/examples/030-todo-app-with-eventTest/runtime/core/router.js +271 -0
  65. package/examples/030-todo-app-with-eventTest/store.d.ts +18 -0
  66. package/examples/030-todo-app-with-eventTest/style.css +170 -0
  67. package/examples/030-todo-app-with-eventTest/tests/README.md +208 -0
  68. package/examples/030-todo-app-with-eventTest/tests/eventTest.js +176 -0
  69. package/examples/030-todo-app-with-eventTest/tests/generateTypes.js +189 -0
  70. package/examples/030-todo-app-with-eventTest/tests/run.js +20 -0
  71. package/examples/030-todo-app-with-eventTest/tests/todos.test.js +167 -0
  72. package/examples/031-todo-app-with-eventTest/LICENSE +55 -0
  73. package/examples/031-todo-app-with-eventTest/README.md +54 -0
  74. package/examples/031-todo-app-with-eventTest/TUTORIAL.md +390 -0
  75. package/examples/031-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
  76. package/examples/031-todo-app-with-eventTest/app/bridges.js +113 -0
  77. package/examples/031-todo-app-with-eventTest/app/router.js +26 -0
  78. package/examples/031-todo-app-with-eventTest/app/store.js +15 -0
  79. package/examples/031-todo-app-with-eventTest/app/views/home.js +46 -0
  80. package/examples/031-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
  81. package/examples/031-todo-app-with-eventTest/devtools/dock.js +41 -0
  82. package/examples/031-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
  83. package/examples/031-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
  84. package/examples/031-todo-app-with-eventTest/devtools/telemetry.js +104 -0
  85. package/examples/031-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
  86. package/examples/031-todo-app-with-eventTest/index.html +103 -0
  87. package/examples/031-todo-app-with-eventTest/package-lock.json +2184 -0
  88. package/examples/031-todo-app-with-eventTest/package.json +24 -0
  89. package/examples/031-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  90. package/examples/031-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  91. package/examples/031-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  92. package/examples/031-todo-app-with-eventTest/runtime/core/router.js +271 -0
  93. package/examples/031-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
  94. package/examples/031-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
  95. package/examples/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
  96. package/examples/031-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
  97. package/examples/031-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
  98. package/examples/031-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
  99. package/examples/031-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
  100. package/examples/031-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
  101. package/examples/031-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
  102. package/examples/031-todo-app-with-eventTest/store.d.ts +23 -0
  103. package/examples/031-todo-app-with-eventTest/style.css +170 -0
  104. package/examples/031-todo-app-with-eventTest/tests/README.md +208 -0
  105. package/examples/031-todo-app-with-eventTest/tests/eventTest.js +176 -0
  106. package/examples/031-todo-app-with-eventTest/tests/generateTypes.js +191 -0
  107. package/examples/031-todo-app-with-eventTest/tests/run.js +20 -0
  108. package/examples/031-todo-app-with-eventTest/tests/todos.test.js +192 -0
  109. package/examples/032-todo-app-with-eventTest/LICENSE +55 -0
  110. package/examples/032-todo-app-with-eventTest/README.md +54 -0
  111. package/examples/032-todo-app-with-eventTest/TUTORIAL.md +390 -0
  112. package/examples/032-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
  113. package/examples/032-todo-app-with-eventTest/app/actions/index.js +153 -0
  114. package/examples/032-todo-app-with-eventTest/app/bridges.js +113 -0
  115. package/examples/032-todo-app-with-eventTest/app/router.js +26 -0
  116. package/examples/032-todo-app-with-eventTest/app/store.js +15 -0
  117. package/examples/032-todo-app-with-eventTest/app/views/home.js +46 -0
  118. package/examples/032-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
  119. package/examples/032-todo-app-with-eventTest/devtools/dock.js +41 -0
  120. package/examples/032-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
  121. package/examples/032-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
  122. package/examples/032-todo-app-with-eventTest/devtools/telemetry.js +104 -0
  123. package/examples/032-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
  124. package/examples/032-todo-app-with-eventTest/index.html +87 -0
  125. package/examples/032-todo-app-with-eventTest/package-lock.json +2184 -0
  126. package/examples/032-todo-app-with-eventTest/package.json +24 -0
  127. package/examples/032-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  128. package/examples/032-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  129. package/examples/032-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  130. package/examples/032-todo-app-with-eventTest/runtime/core/router.js +271 -0
  131. package/examples/032-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
  132. package/examples/032-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
  133. package/examples/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
  134. package/examples/032-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
  135. package/examples/032-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
  136. package/examples/032-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
  137. package/examples/032-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
  138. package/examples/032-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
  139. package/examples/032-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
  140. package/examples/032-todo-app-with-eventTest/store.d.ts +23 -0
  141. package/examples/032-todo-app-with-eventTest/style.css +170 -0
  142. package/examples/032-todo-app-with-eventTest/tests/README.md +208 -0
  143. package/examples/032-todo-app-with-eventTest/tests/eventTest.js +176 -0
  144. package/examples/032-todo-app-with-eventTest/tests/generateTypes.js +191 -0
  145. package/examples/032-todo-app-with-eventTest/tests/run.js +20 -0
  146. package/examples/032-todo-app-with-eventTest/tests/todos.test.js +192 -0
  147. package/index.js +14 -3
  148. package/package.json +16 -7
  149. package/stateSerializer.js +99 -4
  150. package/templateManager.js +50 -2
package/eventState.js CHANGED
@@ -1,78 +1,87 @@
1
1
  const createEventState = (initial = {}) => {
2
2
  const store = JSON.parse(JSON.stringify(initial));
3
3
 
4
- const bus = document.createElement("x-store");
5
-
6
- bus.style.display = "none";
7
- document.documentElement.appendChild(bus);
4
+ const bus = new EventTarget();
5
+ let destroyed = false;
8
6
 
9
7
  return {
10
8
  get: (path) => {
9
+ if (destroyed) throw new Error('Cannot get from destroyed store');
11
10
  if (!path) return store;
12
- return path
13
- .split(".")
14
- .reduce(
15
- (obj, prop) =>
16
- obj && obj[prop] !== undefined ? obj[prop] : undefined,
17
- store
18
- );
19
- },
20
11
 
21
- set: (path, value) => {
22
- if(!path) return;
12
+ // Parse dot-paths: 'items.0' ['items', '0']
13
+ const parts = path.split('.').flatMap(part => {
14
+ const match = part.match(/([^\[]+)\[(\d+)\]/);
15
+ return match ? [match[1], match[2]] : part;
16
+ });
23
17
 
24
- let target = store;
25
- const parts = path.split(".");
18
+ return parts.reduce(
19
+ (obj, prop) => (obj && obj[prop] !== undefined ? obj[prop] : undefined),
20
+ store
21
+ );
22
+ },
23
+ set: (path, value) => {
24
+ if (destroyed) throw new Error('Cannot set on destroyed store');
25
+ if (!path) return;
26
+ const parts = path.split('.');
26
27
  const last = parts.pop();
28
+ let target = store;
27
29
 
28
- parts.forEach((part) => {
29
- if (!target[part] || typeof target[part] !== "object") {
30
- target[part] = {};
31
- }
32
- target = target[part];
33
- });
30
+ for (const key of parts) {
31
+ // create intermediate objects as needed
32
+ if (typeof target[key] !== 'object' || target[key] === null) {
33
+ target[key] = {};
34
+ }
35
+ target = target[key];
36
+ }
34
37
 
35
38
  target[last] = value;
36
39
 
37
- bus.dispatchEvent(new CustomEvent(path, { detail: value }));
40
+ if (!destroyed) {
41
+ // exact path
42
+ bus.dispatchEvent(new CustomEvent(path, { detail: value }));
38
43
 
39
- if (parts.length > 0) {
40
- let parentPath = "";
41
- for (const part of parts) {
42
- parentPath = parentPath ? `${parentPath}.${part}` : part;
43
- bus.dispatchEvent(
44
- new CustomEvent(`${parentPath}.*`, {
45
- detail: { path, value },
46
- })
47
- );
44
+ // parent wildcards: a, a.b -> 'a.*', 'a.b.*'
45
+ for (let i = 1; i <= parts.length; i++) {
46
+ const parent = parts.slice(0, i).join('.');
47
+ bus.dispatchEvent(new CustomEvent(`${parent}.*`, { detail: { path, value } }));
48
48
  }
49
49
 
50
- bus.dispatchEvent(
51
- new CustomEvent("*", {
52
- detail: { path, value},
53
- })
54
- );
50
+ // root wildcard
51
+ bus.dispatchEvent(new CustomEvent('*', { detail: { path, value } }));
55
52
  }
56
53
 
57
54
  return value;
58
55
  },
56
+ subscribe(path, handler) {
57
+ if (destroyed) throw new Error('store destroyed');
58
+ if (!path || typeof handler !== 'function') {
59
+ throw new TypeError('subscribe(path, handler) requires a string path and function handler');
60
+ }
61
+ const onEvent = (evt) => handler(evt.detail, evt);
62
+ bus.addEventListener(path, onEvent);
59
63
 
60
- subscribe: (path, callback) => {
61
- if (!path || typeof callback !== "function") return () => {};
62
-
63
- const handler = (e) => callback(e.detail, path);
64
- bus.addEventListener(path, handler);
65
-
66
- return () => bus.removeEventListener(path, handler);
64
+ return function unsubscribe() {
65
+ bus.removeEventListener(path, onEvent);
66
+ };
67
67
  },
68
-
69
- destroy: () => {
70
- if (bus.parentNode) {
71
- bus.parentNode.removeChild(bus);
68
+ addStateListener: subscribe,
69
+ off(unsubscribe) {
70
+ if (typeof unsubscribe !== 'function') {
71
+ throw new TypeError('off(unsubscribe) requires a function returned by subscribe');
72
72
  }
73
+ return unsubscribe();
73
74
  },
75
+
76
+ destroy() {
77
+ if (!destroyed) {
78
+ destroyed = true;
79
+ // EventTarget has no parentNode - just mark as destroyed
80
+ // Future sets/subscribes will be blocked by destroyed flag
81
+ }
82
+ }
74
83
  };
75
- };
84
+ }
76
85
 
77
86
  export default createEventState;
78
87
  export { createEventState };
package/eventTest.js ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * eventTest.js - Event-Sequence Testing for UIstate
3
+ *
4
+ * Copyright (c) 2025 Ajdin Imsirovic
5
+ *
6
+ * This file is licensed under a PROPRIETARY LICENSE.
7
+ *
8
+ * Permission is hereby granted to USE this software for:
9
+ * - Personal projects
10
+ * - Open-source projects
11
+ * - Educational purposes
12
+ *
13
+ * RESTRICTIONS:
14
+ * - Commercial use requires a separate license (contact: your@email.com)
15
+ * - Modification and redistribution of this file are NOT permitted
16
+ * - This file may not be included in derivative works without permission
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
19
+ *
20
+ * For commercial licensing inquiries: your@email.com
21
+ *
22
+ * eventTest.js - Event-Sequence Testing for EventState
23
+ *
24
+ * Provides TDD-style testing with type extraction capabilities
25
+ */
26
+
27
+ import { createEventState } from './eventState.js';
28
+
29
+ export function createEventTest(initialState = {}) {
30
+ const store = createEventState(initialState);
31
+ const eventLog = [];
32
+ const typeAssertions = [];
33
+
34
+ // Spy on all events
35
+ store.subscribe('*', (detail) => {
36
+ const { path, value } = detail;
37
+ eventLog.push({ timestamp: Date.now(), path, value });
38
+ });
39
+
40
+ const api = {
41
+ store,
42
+
43
+ // Trigger a state change
44
+ trigger(path, value) {
45
+ store.set(path, value);
46
+ return this;
47
+ },
48
+
49
+ // Assert exact value
50
+ assertPath(path, expected) {
51
+ const actual = store.get(path);
52
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
53
+ throw new Error(`Expected ${path} to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
54
+ }
55
+ return this;
56
+ },
57
+
58
+ // Assert type (for type generation)
59
+ assertType(path, expectedType) {
60
+ const actual = store.get(path);
61
+ const actualType = typeof actual;
62
+
63
+ if (actualType !== expectedType) {
64
+ throw new Error(`Expected ${path} to be type ${expectedType}, got ${actualType}`);
65
+ }
66
+
67
+ // Store for type generation
68
+ typeAssertions.push({ path, type: expectedType });
69
+ return this;
70
+ },
71
+
72
+ // Assert array with element shape (for type generation)
73
+ assertArrayOf(path, elementShape) {
74
+ const actual = store.get(path);
75
+
76
+ if (!Array.isArray(actual)) {
77
+ throw new Error(`Expected ${path} to be an array, got ${typeof actual}`);
78
+ }
79
+
80
+ // Validate first element matches shape (if array not empty)
81
+ if (actual.length > 0) {
82
+ validateShape(actual[0], elementShape, path);
83
+ }
84
+
85
+ // Store for type generation
86
+ typeAssertions.push({ path, type: 'array', elementShape });
87
+ return this;
88
+ },
89
+
90
+ // Assert object shape (for type generation)
91
+ assertShape(path, objectShape) {
92
+ const actual = store.get(path);
93
+
94
+ if (typeof actual !== 'object' || actual === null || Array.isArray(actual)) {
95
+ throw new Error(`Expected ${path} to be an object, got ${typeof actual}`);
96
+ }
97
+
98
+ validateShape(actual, objectShape, path);
99
+
100
+ // Store for type generation
101
+ typeAssertions.push({ path, type: 'object', shape: objectShape });
102
+ return this;
103
+ },
104
+
105
+ // Assert array length
106
+ assertArrayLength(path, expectedLength) {
107
+ const actual = store.get(path);
108
+
109
+ if (!Array.isArray(actual)) {
110
+ throw new Error(`Expected ${path} to be an array`);
111
+ }
112
+
113
+ if (actual.length !== expectedLength) {
114
+ throw new Error(`Expected ${path} to have length ${expectedLength}, got ${actual.length}`);
115
+ }
116
+
117
+ return this;
118
+ },
119
+
120
+ // Assert event fired N times
121
+ assertEventFired(path, times) {
122
+ const count = eventLog.filter(e => e.path === path).length;
123
+ if (times !== undefined && count !== times) {
124
+ throw new Error(`Expected ${path} to fire ${times} times, fired ${count}`);
125
+ }
126
+ return this;
127
+ },
128
+
129
+ // Get event log
130
+ getEventLog() {
131
+ return [...eventLog];
132
+ },
133
+
134
+ // Get type assertions (for type generation)
135
+ getTypeAssertions() {
136
+ return [...typeAssertions];
137
+ }
138
+ };
139
+
140
+ return api;
141
+ }
142
+
143
+ // Helper to validate object shape
144
+ function validateShape(actual, shape, path) {
145
+ for (const [key, expectedType] of Object.entries(shape)) {
146
+ if (!(key in actual)) {
147
+ throw new Error(`Expected ${path} to have property ${key}`);
148
+ }
149
+
150
+ const actualValue = actual[key];
151
+
152
+ // Handle nested objects
153
+ if (typeof expectedType === 'object' && !Array.isArray(expectedType)) {
154
+ validateShape(actualValue, expectedType, `${path}.${key}`);
155
+ } else {
156
+ // Primitive type check
157
+ const actualType = typeof actualValue;
158
+ if (actualType !== expectedType) {
159
+ throw new Error(`Expected ${path}.${key} to be type ${expectedType}, got ${actualType}`);
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ // Simple test runner
166
+ export function test(name, fn) {
167
+ try {
168
+ fn();
169
+ console.log(`✓ ${name}`);
170
+ return true;
171
+ } catch (error) {
172
+ console.error(`✗ ${name}`);
173
+ console.error(` ${error.message}`);
174
+ return false;
175
+ }
176
+ }
177
+
178
+ // Run multiple tests
179
+ export function runTests(tests) {
180
+ console.log('\n🧪 Running tests...\n');
181
+
182
+ let passed = 0;
183
+ let failed = 0;
184
+
185
+ for (const [name, fn] of Object.entries(tests)) {
186
+ if (test(name, fn)) {
187
+ passed++;
188
+ } else {
189
+ failed++;
190
+ }
191
+ }
192
+
193
+ console.log(`\n📊 Results: ${passed} passed, ${failed} failed\n`);
194
+
195
+ return { passed, failed };
196
+ }
@@ -0,0 +1,44 @@
1
+ # 001 Counter - Minimal EventState Example
2
+
3
+ The simplest possible EventState example: a counter with one button.
4
+
5
+ ## What's Here
6
+
7
+ - `eventState.js` - ~60 lines of reactive state management
8
+ - `index.html` - Counter UI with subscription
9
+
10
+ ## How It Works
11
+
12
+ ```javascript
13
+ // 1. Create store
14
+ const store = createEventState({ count: 0 });
15
+
16
+ // 2. Subscribe to changes
17
+ store.subscribe('count', ({ value }) => {
18
+ document.getElementById('count').textContent = value;
19
+ });
20
+
21
+ // 3. Update state
22
+ store.set('count', store.get('count') + 1);
23
+ ```
24
+
25
+ That's it. No framework, no build step, just reactive data.
26
+
27
+ ## Run It
28
+
29
+ Open `index.html` in a browser. Click the `+` button to increment.
30
+
31
+ ## Key Concepts
32
+
33
+ - **`createEventState(initial)`** - Creates a reactive store
34
+ - **`store.get(path)`** - Read state at dot-path
35
+ - **`store.set(path, value)`** - Write state and notify subscribers
36
+ - **`store.subscribe(path, handler)`** - React to changes
37
+
38
+ ## Next Steps
39
+
40
+ See `examples/009-todo-app-with-eventTest/` for a full application with:
41
+ - Router
42
+ - Bridges (intent → domain pattern)
43
+ - Tests with type generation
44
+ - Dev tools
@@ -0,0 +1,86 @@
1
+ const createEventState = (initial = {}) => {
2
+ const store = JSON.parse(JSON.stringify(initial));
3
+
4
+ const bus = new EventTarget();
5
+ let destroyed = false;
6
+
7
+ return {
8
+ get: (path) => {
9
+ if (destroyed) throw new Error('Cannot get from destroyed store');
10
+ if (!path) return store;
11
+
12
+ // Parse dot-paths: 'items.0' → ['items', '0']
13
+ const parts = path.split('.').flatMap(part => {
14
+ const match = part.match(/([^\[]+)\[(\d+)\]/);
15
+ return match ? [match[1], match[2]] : part;
16
+ });
17
+
18
+ return parts.reduce(
19
+ (obj, prop) => (obj && obj[prop] !== undefined ? obj[prop] : undefined),
20
+ store
21
+ );
22
+ },
23
+ set: (path, value) => {
24
+ if (destroyed) throw new Error('Cannot set on destroyed store');
25
+ if (!path) return;
26
+ const parts = path.split('.');
27
+ const last = parts.pop();
28
+ let target = store;
29
+
30
+ for (const key of parts) {
31
+ // create intermediate objects as needed
32
+ if (typeof target[key] !== 'object' || target[key] === null) {
33
+ target[key] = {};
34
+ }
35
+ target = target[key];
36
+ }
37
+
38
+ target[last] = value;
39
+
40
+ if (!destroyed) {
41
+ // exact path
42
+ bus.dispatchEvent(new CustomEvent(path, { detail: value }));
43
+
44
+ // parent wildcards: a, a.b -> 'a.*', 'a.b.*'
45
+ for (let i = 1; i <= parts.length; i++) {
46
+ const parent = parts.slice(0, i).join('.');
47
+ bus.dispatchEvent(new CustomEvent(`${parent}.*`, { detail: { path, value } }));
48
+ }
49
+
50
+ // root wildcard
51
+ bus.dispatchEvent(new CustomEvent('*', { detail: { path, value } }));
52
+ }
53
+
54
+ return value;
55
+ },
56
+ subscribe(path, handler) {
57
+ if (destroyed) throw new Error('store destroyed');
58
+ if (!path || typeof handler !== 'function') {
59
+ throw new TypeError('subscribe(path, handler) requires a string path and function handler');
60
+ }
61
+ const onEvent = (evt) => handler(evt.detail, evt);
62
+ bus.addEventListener(path, onEvent);
63
+
64
+ return function unsubscribe() {
65
+ bus.removeEventListener(path, onEvent);
66
+ };
67
+ },
68
+ off(unsubscribe) {
69
+ if (typeof unsubscribe !== 'function') {
70
+ throw new TypeError('off(unsubscribe) requires a function returned by subscribe');
71
+ }
72
+ return unsubscribe();
73
+ },
74
+
75
+ destroy() {
76
+ if (!destroyed) {
77
+ destroyed = true;
78
+ // EventTarget has no parentNode - just mark as destroyed
79
+ // Future sets/subscribes will be blocked by destroyed flag
80
+ }
81
+ }
82
+ };
83
+ }
84
+
85
+ export default createEventState;
86
+ export { createEventState };
@@ -0,0 +1,30 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>001 Counter - EventState</title>
7
+ </head>
8
+ <body>
9
+ <h1>Counter: <span id="count">0</span></h1>
10
+ <button id="increment">+</button>
11
+
12
+ <script type="module">
13
+ import { createEventState } from './eventState.js';
14
+
15
+ // Create store
16
+ const store = createEventState({ count: 0 });
17
+
18
+ // Subscribe to count changes
19
+ store.subscribe('count', ( value ) => {
20
+ document.getElementById('count').textContent = value;
21
+ });
22
+
23
+ // Increment on button click
24
+ document.getElementById('increment').onclick = () => {
25
+ const current = store.get('count');
26
+ store.set('count', current + 1);
27
+ };
28
+ </script>
29
+ </body>
30
+ </html>
@@ -0,0 +1,44 @@
1
+ # 001 Counter - Minimal EventState Example
2
+
3
+ The simplest possible EventState example: a counter with one button.
4
+
5
+ ## What's Here
6
+
7
+ - `eventState.js` - ~60 lines of reactive state management
8
+ - `index.html` - Counter UI with subscription
9
+
10
+ ## How It Works
11
+
12
+ ```javascript
13
+ // 1. Create store
14
+ const store = createEventState({ count: 0 });
15
+
16
+ // 2. Subscribe to changes
17
+ store.subscribe('count', ({ value }) => {
18
+ document.getElementById('count').textContent = value;
19
+ });
20
+
21
+ // 3. Update state
22
+ store.set('count', store.get('count') + 1);
23
+ ```
24
+
25
+ That's it. No framework, no build step, just reactive data.
26
+
27
+ ## Run It
28
+
29
+ Open `index.html` in a browser. Click the `+` button to increment.
30
+
31
+ ## Key Concepts
32
+
33
+ - **`createEventState(initial)`** - Creates a reactive store
34
+ - **`store.get(path)`** - Read state at dot-path
35
+ - **`store.set(path, value)`** - Write state and notify subscribers
36
+ - **`store.subscribe(path, handler)`** - React to changes
37
+
38
+ ## Next Steps
39
+
40
+ See `examples/009-todo-app-with-eventTest/` for a full application with:
41
+ - Router
42
+ - Bridges (intent → domain pattern)
43
+ - Tests with type generation
44
+ - Dev tools
@@ -0,0 +1,86 @@
1
+ const createEventState = (initial = {}) => {
2
+ const store = JSON.parse(JSON.stringify(initial));
3
+
4
+ const bus = new EventTarget();
5
+ let destroyed = false;
6
+
7
+ return {
8
+ get: (path) => {
9
+ if (destroyed) throw new Error('Cannot get from destroyed store');
10
+ if (!path) return store;
11
+
12
+ // Parse dot-paths: 'items.0' → ['items', '0']
13
+ const parts = path.split('.').flatMap(part => {
14
+ const match = part.match(/([^\[]+)\[(\d+)\]/);
15
+ return match ? [match[1], match[2]] : part;
16
+ });
17
+
18
+ return parts.reduce(
19
+ (obj, prop) => (obj && obj[prop] !== undefined ? obj[prop] : undefined),
20
+ store
21
+ );
22
+ },
23
+ set: (path, value) => {
24
+ if (destroyed) throw new Error('Cannot set on destroyed store');
25
+ if (!path) return;
26
+ const parts = path.split('.');
27
+ const last = parts.pop();
28
+ let target = store;
29
+
30
+ for (const key of parts) {
31
+ // create intermediate objects as needed
32
+ if (typeof target[key] !== 'object' || target[key] === null) {
33
+ target[key] = {};
34
+ }
35
+ target = target[key];
36
+ }
37
+
38
+ target[last] = value;
39
+
40
+ if (!destroyed) {
41
+ // exact path
42
+ bus.dispatchEvent(new CustomEvent(path, { detail: value }));
43
+
44
+ // parent wildcards: a, a.b -> 'a.*', 'a.b.*'
45
+ for (let i = 1; i <= parts.length; i++) {
46
+ const parent = parts.slice(0, i).join('.');
47
+ bus.dispatchEvent(new CustomEvent(`${parent}.*`, { detail: { path, value } }));
48
+ }
49
+
50
+ // root wildcard
51
+ bus.dispatchEvent(new CustomEvent('*', { detail: { path, value } }));
52
+ }
53
+
54
+ return value;
55
+ },
56
+ subscribe(path, handler) {
57
+ if (destroyed) throw new Error('store destroyed');
58
+ if (!path || typeof handler !== 'function') {
59
+ throw new TypeError('subscribe(path, handler) requires a string path and function handler');
60
+ }
61
+ const onEvent = (evt) => handler(evt.detail, evt);
62
+ bus.addEventListener(path, onEvent);
63
+
64
+ return function unsubscribe() {
65
+ bus.removeEventListener(path, onEvent);
66
+ };
67
+ },
68
+ off(unsubscribe) {
69
+ if (typeof unsubscribe !== 'function') {
70
+ throw new TypeError('off(unsubscribe) requires a function returned by subscribe');
71
+ }
72
+ return unsubscribe();
73
+ },
74
+
75
+ destroy() {
76
+ if (!destroyed) {
77
+ destroyed = true;
78
+ // EventTarget has no parentNode - just mark as destroyed
79
+ // Future sets/subscribes will be blocked by destroyed flag
80
+ }
81
+ }
82
+ };
83
+ }
84
+
85
+ export default createEventState;
86
+ export { createEventState };
@@ -0,0 +1,44 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>002 Counter Improved - EventState</title>
7
+ </head>
8
+ <body>
9
+ <h1>Counter: <span id="count">0</span></h1>
10
+ <button id="increment">+</button>
11
+ <button id="decrement">-</button>
12
+ <button id="double">x2</button>
13
+
14
+ <script type="module">
15
+ import { createEventState } from './eventState.js';
16
+
17
+ // Create store
18
+ const store = createEventState({ count: 0 });
19
+
20
+ // Subscribe to count changes
21
+ store.subscribe('count', ( value ) => {
22
+ document.getElementById('count').textContent = value;
23
+ });
24
+
25
+ // Increment on button click
26
+ document.getElementById('increment').onclick = () => {
27
+ const current = store.get('count');
28
+ store.set('count', current + 1);
29
+ };
30
+
31
+ // Decrement on button click
32
+ document.getElementById('decrement').onclick = () => {
33
+ const current = store.get('count');
34
+ store.set('count', current - 1);
35
+ };
36
+
37
+ // Decrement on button click
38
+ document.getElementById('double').onclick = () => {
39
+ const current = store.get('count');
40
+ store.set('count', current * 2);
41
+ };
42
+ </script>
43
+ </body>
44
+ </html>