@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
@@ -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,54 @@
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>009 localStorage Side Effects - EventState</title>
7
+ </head>
8
+ <body>
9
+ <h1>Persistent Counter</h1>
10
+
11
+ <h2>Count: <span id="count">0</span></h2>
12
+
13
+ <button id="increment">+1</button>
14
+ <button id="decrement">-1</button>
15
+ <button id="reset">Reset</button>
16
+
17
+ <p><small>Reload the page - your count persists!</small></p>
18
+
19
+ <script type="module">
20
+ import { createEventState } from './eventState.js';
21
+
22
+ const STORAGE_KEY = 'eventstate-counter';
23
+
24
+ // Load initial state from localStorage
25
+ const savedCount = localStorage.getItem(STORAGE_KEY);
26
+ const initialCount = savedCount !== null ? parseInt(savedCount, 10) : 0;
27
+
28
+ // Create store with saved state
29
+ const store = createEventState({ count: initialCount });
30
+
31
+ // Side effect: save to localStorage on every change
32
+ store.subscribe('count', (value) => {
33
+ localStorage.setItem(STORAGE_KEY, value);
34
+ document.getElementById('count').textContent = value;
35
+ });
36
+
37
+ // Operations
38
+ document.getElementById('increment').onclick = () => {
39
+ store.set('count', store.get('count') + 1);
40
+ };
41
+
42
+ document.getElementById('decrement').onclick = () => {
43
+ store.set('count', store.get('count') - 1);
44
+ };
45
+
46
+ document.getElementById('reset').onclick = () => {
47
+ store.set('count', 0);
48
+ };
49
+
50
+ // Initial render
51
+ document.getElementById('count').textContent = initialCount;
52
+ </script>
53
+ </body>
54
+ </html>
@@ -0,0 +1,74 @@
1
+ # 010 Decoupled Components - Shared State
2
+
3
+ Demonstrates how components can communicate through shared state without direct coupling.
4
+
5
+ ## What's Here
6
+
7
+ - **Component A (Writer):** Sends messages
8
+ - **Component B (Reader):** Displays current message
9
+ - **Component C (Logger):** Logs all messages with timestamps
10
+ - **Zero coupling:** Components don't know about each other
11
+
12
+ ## How It Works
13
+
14
+ ```javascript
15
+ // Shared store - the ONLY connection
16
+ const store = createEventState({ message: '' });
17
+
18
+ // Component A: Writes to state
19
+ document.getElementById('sendBtn').onclick = () => {
20
+ store.set('message', inputValue);
21
+ };
22
+
23
+ // Component B: Reads from state
24
+ store.subscribe('message', (value) => {
25
+ document.getElementById('display').textContent = value;
26
+ });
27
+
28
+ // Component C: Also reads from state
29
+ store.subscribe('message', (value) => {
30
+ logMessage(value);
31
+ });
32
+ ```
33
+
34
+ ## Key Insight
35
+
36
+ **State is the interface between components.**
37
+
38
+ Components communicate by:
39
+ 1. Writing to shared state
40
+ 2. Subscribing to shared state
41
+ 3. **Never** calling each other directly
42
+
43
+ This is the **pub/sub pattern** in action:
44
+ - ✅ Components are decoupled
45
+ - ✅ Easy to add/remove components
46
+ - ✅ No prop drilling
47
+ - ✅ No callbacks passed down
48
+
49
+ Other frameworks need:
50
+ - React: Context API, prop drilling, or state management libraries
51
+ - Vue: Provide/inject or Vuex
52
+ - Svelte: Context API or stores
53
+
54
+ **EventState:** Just share the store. That's it.
55
+
56
+ ## Architecture Pattern
57
+
58
+ This demonstrates the **mediator pattern**:
59
+ - Components don't talk to each other
60
+ - They talk to the store
61
+ - The store mediates all communication
62
+
63
+ This is how your full UIstate framework works:
64
+ - Intents → State changes
65
+ - State changes → UI updates
66
+ - Everything is decoupled through events
67
+
68
+ ## Run It
69
+
70
+ Open `index.html`, type messages, and watch all three components react independently.
71
+
72
+ ## Try This
73
+
74
+ Add a fourth component that counts vowels in messages. It only needs to subscribe to `'message'` - no other code changes needed.
@@ -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,90 @@
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>010 Decoupled Components - EventState</title>
7
+ <style>
8
+ .component {
9
+ border: 2px solid #333;
10
+ padding: 20px;
11
+ margin: 10px 0;
12
+ }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <h1>Decoupled Components</h1>
17
+ <p>Two independent "components" sharing state without knowing about each other.</p>
18
+
19
+ <!-- Component A: Writer -->
20
+ <div class="component">
21
+ <h2>Component A: Writer</h2>
22
+ <input type="text" id="messageInput" placeholder="Type a message...">
23
+ <button id="sendBtn">Send</button>
24
+ </div>
25
+
26
+ <!-- Component B: Reader -->
27
+ <div class="component">
28
+ <h2>Component B: Reader</h2>
29
+ <p><strong>Received:</strong> <span id="messageDisplay">(nothing yet)</span></p>
30
+ <p><small>Message count: <span id="messageCount">0</span></small></p>
31
+ </div>
32
+
33
+ <!-- Component C: Logger -->
34
+ <div class="component">
35
+ <h2>Component C: Logger</h2>
36
+ <ul id="messageLog"></ul>
37
+ </div>
38
+
39
+ <script type="module">
40
+ import { createEventState } from './eventState.js';
41
+
42
+ // Shared store - the ONLY connection between components
43
+ const store = createEventState({
44
+ message: '',
45
+ messageCount: 0
46
+ });
47
+
48
+ // ===== COMPONENT A: Writer =====
49
+ // Knows nothing about B or C, just writes to state
50
+ document.getElementById('sendBtn').onclick = () => {
51
+ const input = document.getElementById('messageInput');
52
+ const value = input.value.trim();
53
+
54
+ if (value) {
55
+ store.set('message', value);
56
+ store.set('messageCount', store.get('messageCount') + 1);
57
+ input.value = '';
58
+ }
59
+ };
60
+
61
+ document.getElementById('messageInput').onkeypress = (e) => {
62
+ if (e.key === 'Enter') {
63
+ document.getElementById('sendBtn').click();
64
+ }
65
+ };
66
+
67
+ // ===== COMPONENT B: Reader =====
68
+ // Knows nothing about A or C, just reads from state
69
+ store.subscribe('message', (value) => {
70
+ document.getElementById('messageDisplay').textContent = value || '(nothing yet)';
71
+ });
72
+
73
+ store.subscribe('messageCount', (value) => {
74
+ document.getElementById('messageCount').textContent = value;
75
+ });
76
+
77
+ // ===== COMPONENT C: Logger =====
78
+ // Knows nothing about A or B, just logs all messages
79
+ store.subscribe('message', (value) => {
80
+ if (value) {
81
+ const log = document.getElementById('messageLog');
82
+ const timestamp = new Date().toLocaleTimeString();
83
+ const li = document.createElement('li');
84
+ li.textContent = `[${timestamp}] ${value}`;
85
+ log.appendChild(li);
86
+ }
87
+ });
88
+ </script>
89
+ </body>
90
+ </html>
@@ -0,0 +1,98 @@
1
+ # 011 Async Patterns - Debouncing & Loading States
2
+
3
+ Demonstrates how to handle async operations with EventState.
4
+
5
+ ## What's Here
6
+
7
+ - **Debounced search:** Wait for user to stop typing before searching
8
+ - **Loading states:** Show loading indicator during async operations
9
+ - **Error handling:** Graceful failure with try/catch
10
+ - **No async library needed** - just Promises and state
11
+
12
+ ## How It Works
13
+
14
+ ### Debounced Search
15
+
16
+ ```javascript
17
+ let searchTimeout = null;
18
+
19
+ store.subscribe('searchQuery', async (value) => {
20
+ clearTimeout(searchTimeout);
21
+
22
+ store.set('isSearching', true);
23
+
24
+ // Wait 300ms before searching
25
+ searchTimeout = setTimeout(async () => {
26
+ const result = await performSearch(value);
27
+ store.set('searchResult', result);
28
+ store.set('isSearching', false);
29
+ }, 300);
30
+ });
31
+ ```
32
+
33
+ ### Loading States
34
+
35
+ ```javascript
36
+ document.getElementById('fetchBtn').onclick = async () => {
37
+ store.set('isLoading', true);
38
+
39
+ try {
40
+ const data = await fetchData();
41
+ store.set('apiData', data);
42
+ } finally {
43
+ store.set('isLoading', false);
44
+ }
45
+ };
46
+ ```
47
+
48
+ ## Key Insight
49
+
50
+ **Async is just state changes over time.**
51
+
52
+ Pattern for any async operation:
53
+ 1. Set loading state to `true`
54
+ 2. Perform async operation
55
+ 3. Set result state
56
+ 4. Set loading state to `false`
57
+
58
+ Other frameworks need:
59
+ - React: `useState` + `useEffect` + cleanup functions
60
+ - Vue: `ref` + `watch` + async watchers
61
+ - Svelte: Reactive statements + `#await` blocks
62
+
63
+ **EventState:** Just use async/await in subscribers. That's it.
64
+
65
+ ## Patterns Demonstrated
66
+
67
+ ### 1. Debouncing
68
+ Wait for user to stop typing before triggering expensive operations.
69
+
70
+ ### 2. Loading Indicators
71
+ Show feedback during async operations to improve UX.
72
+
73
+ ### 3. Disabling Actions
74
+ Prevent duplicate requests by disabling buttons during loading.
75
+
76
+ ### 4. Cleanup
77
+ Clear timeouts to prevent race conditions.
78
+
79
+ ## Run It
80
+
81
+ Open `index.html`:
82
+ - Type in the search box (notice the 300ms delay)
83
+ - Click "Fetch Data" (notice the loading state)
84
+
85
+ ## Try This
86
+
87
+ Open DevTools Network tab and throttle to "Slow 3G" to see loading states more clearly.
88
+
89
+ ## Real-World Usage
90
+
91
+ This pattern works for:
92
+ - ✅ API calls
93
+ - ✅ Autocomplete
94
+ - ✅ Form submission
95
+ - ✅ File uploads
96
+ - ✅ Any async operation
97
+
98
+ Just manage loading/error/success states in the store.
@@ -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 };