@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,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>003 Input Reactive - EventState</title>
7
+ </head>
8
+ <body>
9
+ <h1>Type something:</h1>
10
+ <input type="text" id="textInput" placeholder="Start typing...">
11
+ <p id="output"></p>
12
+
13
+ <script type="module">
14
+ import { createEventState } from './eventState.js';
15
+
16
+ // Create store
17
+ const store = createEventState({ text: '' });
18
+
19
+ // Subscribe to text changes
20
+ store.subscribe('text', (value) => {
21
+ document.getElementById('output').textContent = value;
22
+ });
23
+
24
+ // Update state on input
25
+ document.getElementById('textInput').oninput = (e) => {
26
+ store.set('text', e.target.value);
27
+ };
28
+ </script>
29
+ </body>
30
+ </html>
@@ -0,0 +1,45 @@
1
+ # 004 Computed State - Derived Values
2
+
3
+ Shows how trivial it is to create computed/derived state in EventState.
4
+
5
+ ## What's Here
6
+
7
+ - Two inputs: `firstName` and `lastName`
8
+ - Two computed values: `fullName` and `charCount`
9
+ - **Zero framework magic** - just functions
10
+
11
+ ## How It Works
12
+
13
+ ```javascript
14
+ // 1. Define a function that computes derived state
15
+ const updateFullName = () => {
16
+ const first = store.get('firstName');
17
+ const last = store.get('lastName');
18
+ const fullName = `${first} ${last}`.trim();
19
+ document.getElementById('fullName').textContent = fullName;
20
+ };
21
+
22
+ // 2. Subscribe to wildcard to catch all changes
23
+ store.subscribe('*', () => {
24
+ updateFullName();
25
+ updateCharCount();
26
+ });
27
+
28
+ // 3. That's it. No special API needed.
29
+ ```
30
+
31
+ ## Key Insight
32
+
33
+ **Computed state is just a function.**
34
+
35
+ Other frameworks need:
36
+ - Vue: `computed()`
37
+ - React: `useMemo()`
38
+ - Svelte: `$:` reactive declarations
39
+ - MobX: `@computed` decorators
40
+
41
+ **EventState:** Just write a function and call it when state changes.
42
+
43
+ ## Run It
44
+
45
+ Open `index.html` in a browser. Type in the inputs and watch the computed values update.
@@ -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,62 @@
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>004 Computed State - EventState</title>
7
+ </head>
8
+ <body>
9
+ <h1>Computed State</h1>
10
+
11
+ <label>First Name: <input type="text" id="firstName" placeholder="Abraham"></label><br>
12
+ <label>Last Name: <input type="text" id="lastName" placeholder="Lincoln"></label><br>
13
+
14
+ <p><strong>Full Name:</strong> <span id="fullName"></span></p>
15
+ <p><strong>Character Count:</strong> <span id="charCount">0</span></p>
16
+
17
+ <script type="module">
18
+ import { createEventState } from './eventState.js';
19
+
20
+ // Create store
21
+ const store = createEventState({
22
+ firstName: '',
23
+ lastName: ''
24
+ });
25
+
26
+ // Computed: full name (derived from firstName + lastName)
27
+ const updateFullName = () => {
28
+ const first = store.get('firstName');
29
+ const last = store.get('lastName');
30
+ const fullName = `${first} ${last}`.trim();
31
+ document.getElementById('fullName').textContent = fullName || '(empty)';
32
+ };
33
+
34
+ // Computed: character count
35
+ const updateCharCount = () => {
36
+ const first = store.get('firstName');
37
+ const last = store.get('lastName');
38
+ const total = (first + last).length;
39
+ document.getElementById('charCount').textContent = total;
40
+ };
41
+
42
+ // Subscribe to any state change and recompute
43
+ store.subscribe('*', () => {
44
+ updateFullName();
45
+ updateCharCount();
46
+ });
47
+
48
+ // Wire up inputs
49
+ document.getElementById('firstName').oninput = (e) => {
50
+ store.set('firstName', e.target.value);
51
+ };
52
+
53
+ document.getElementById('lastName').oninput = (e) => {
54
+ store.set('lastName', e.target.value);
55
+ };
56
+
57
+ // Initial render
58
+ updateFullName();
59
+ updateCharCount();
60
+ </script>
61
+ </body>
62
+ </html>
@@ -0,0 +1,42 @@
1
+ # 005 Conditional Rendering - Show/Hide Elements
2
+
3
+ Demonstrates how to conditionally render elements based on state.
4
+
5
+ ## What's Here
6
+
7
+ - Checkbox to toggle visibility
8
+ - Message that shows/hides based on state
9
+ - **No `v-if` or `{#if}` needed** - just DOM manipulation
10
+
11
+ ## How It Works
12
+
13
+ ```javascript
14
+ // 1. Store boolean state
15
+ const store = createEventState({ isVisible: false });
16
+
17
+ // 2. Subscribe and toggle display
18
+ store.subscribe('isVisible', (value) => {
19
+ const message = document.getElementById('message');
20
+ message.style.display = value ? 'block' : 'none';
21
+ });
22
+
23
+ // 3. Update state on checkbox change
24
+ document.getElementById('toggle').onchange = (e) => {
25
+ store.set('isVisible', e.target.checked);
26
+ };
27
+ ```
28
+
29
+ ## Key Insight
30
+
31
+ **Conditional rendering is just DOM manipulation.**
32
+
33
+ Other frameworks need:
34
+ - React: `{isVisible && <p>Message</p>}`
35
+ - Vue: `v-if="isVisible"`
36
+ - Svelte: `{#if isVisible}`
37
+
38
+ **EventState:** Just set `style.display` in a subscriber. That's it.
39
+
40
+ ## Run It
41
+
42
+ Open `index.html` in a browser. Check the box to reveal the message.
@@ -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,36 @@
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>005 Conditional Rendering - EventState</title>
7
+ </head>
8
+ <body>
9
+ <h1>Conditional Rendering</h1>
10
+
11
+ <label>
12
+ <input type="checkbox" id="toggle">
13
+ Show secret message
14
+ </label>
15
+
16
+ <p id="message" style="display: none;">🎉 You found the secret message!</p>
17
+
18
+ <script type="module">
19
+ import { createEventState } from './eventState.js';
20
+
21
+ // Create store
22
+ const store = createEventState({ isVisible: false });
23
+
24
+ // Subscribe to visibility changes
25
+ store.subscribe('isVisible', (value) => {
26
+ const message = document.getElementById('message');
27
+ message.style.display = value ? 'block' : 'none';
28
+ });
29
+
30
+ // Toggle on checkbox change
31
+ document.getElementById('toggle').onchange = (e) => {
32
+ store.set('isVisible', e.target.checked);
33
+ };
34
+ </script>
35
+ </body>
36
+ </html>
@@ -0,0 +1,49 @@
1
+ # 006 List Rendering - Dynamic Arrays
2
+
3
+ Demonstrates how to render lists from array state.
4
+
5
+ ## What's Here
6
+
7
+ - Input to add items
8
+ - Dynamic list that updates on state change
9
+ - **No `v-for` or `.map()` in JSX** - just `innerHTML`
10
+
11
+ ## How It Works
12
+
13
+ ```javascript
14
+ // 1. Store array in state
15
+ const store = createEventState({ items: [] });
16
+
17
+ // 2. Render function
18
+ const renderList = () => {
19
+ const items = store.get('items');
20
+ const list = document.getElementById('itemList');
21
+ list.innerHTML = items.map(item => `<li>${item}</li>`).join('');
22
+ };
23
+
24
+ // 3. Subscribe to array changes
25
+ store.subscribe('items', renderList);
26
+
27
+ // 4. Add items by creating new array
28
+ const items = store.get('items');
29
+ store.set('items', [...items, newItem]);
30
+ ```
31
+
32
+ ## Key Insight
33
+
34
+ **List rendering is just string concatenation.**
35
+
36
+ Other frameworks need:
37
+ - React: `{items.map(item => <li>{item}</li>)}`
38
+ - Vue: `v-for="item in items"`
39
+ - Svelte: `{#each items as item}`
40
+
41
+ **EventState:** Just use `.map()` and `innerHTML`. That's it.
42
+
43
+ ## Important
44
+
45
+ We create a **new array** with `[...items, newItem]` instead of mutating. This ensures the subscription fires (new reference = change detected).
46
+
47
+ ## Run It
48
+
49
+ Open `index.html` in a browser. Type items and click Add (or press Enter).
@@ -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 };