@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.
- package/LICENSE-eventTest.md +26 -0
- package/README.md +409 -42
- package/cssState.js +32 -1
- package/eventState.js +58 -49
- package/eventTest.js +196 -0
- package/examples/001-counter/README.md +44 -0
- package/examples/001-counter/eventState.js +86 -0
- package/examples/001-counter/index.html +30 -0
- package/examples/002-counter-improved/README.md +44 -0
- package/examples/002-counter-improved/eventState.js +86 -0
- package/examples/002-counter-improved/index.html +44 -0
- package/examples/003-input-reactive/README.md +44 -0
- package/examples/003-input-reactive/eventState.js +86 -0
- package/examples/003-input-reactive/index.html +30 -0
- package/examples/004-computed-state/README.md +45 -0
- package/examples/004-computed-state/eventState.js +86 -0
- package/examples/004-computed-state/index.html +62 -0
- package/examples/005-conditional-rendering/README.md +42 -0
- package/examples/005-conditional-rendering/eventState.js +86 -0
- package/examples/005-conditional-rendering/index.html +36 -0
- package/examples/006-list-rendering/README.md +49 -0
- package/examples/006-list-rendering/eventState.js +86 -0
- package/examples/006-list-rendering/index.html +60 -0
- package/examples/007-form-validation/README.md +52 -0
- package/examples/007-form-validation/eventState.js +86 -0
- package/examples/007-form-validation/index.html +99 -0
- package/examples/008-undo-redo/README.md +70 -0
- package/examples/008-undo-redo/eventState.js +86 -0
- package/examples/008-undo-redo/index.html +105 -0
- package/examples/009-localStorage-side-effects/README.md +72 -0
- package/examples/009-localStorage-side-effects/eventState.js +86 -0
- package/examples/009-localStorage-side-effects/index.html +54 -0
- package/examples/010-decoupled-components/README.md +74 -0
- package/examples/010-decoupled-components/eventState.js +86 -0
- package/examples/010-decoupled-components/index.html +90 -0
- package/examples/011-async-patterns/README.md +98 -0
- package/examples/011-async-patterns/eventState.js +86 -0
- package/examples/011-async-patterns/index.html +129 -0
- package/examples/028-counter-improved-eventTest/LICENSE +55 -0
- package/examples/028-counter-improved-eventTest/README.md +131 -0
- package/examples/028-counter-improved-eventTest/app/store.js +9 -0
- package/examples/028-counter-improved-eventTest/index.html +49 -0
- package/examples/028-counter-improved-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/examples/028-counter-improved-eventTest/runtime/core/eventState.js +100 -0
- package/examples/028-counter-improved-eventTest/runtime/core/helpers.js +212 -0
- package/examples/028-counter-improved-eventTest/runtime/core/router.js +271 -0
- package/examples/028-counter-improved-eventTest/store.d.ts +8 -0
- package/examples/028-counter-improved-eventTest/style.css +170 -0
- package/examples/028-counter-improved-eventTest/tests/README.md +208 -0
- package/examples/028-counter-improved-eventTest/tests/counter.test.js +116 -0
- package/examples/028-counter-improved-eventTest/tests/eventTest.js +176 -0
- package/examples/028-counter-improved-eventTest/tests/generateTypes.js +168 -0
- package/examples/028-counter-improved-eventTest/tests/run.js +20 -0
- package/examples/030-todo-app-with-eventTest/LICENSE +55 -0
- package/examples/030-todo-app-with-eventTest/README.md +121 -0
- package/examples/030-todo-app-with-eventTest/app/router.js +25 -0
- package/examples/030-todo-app-with-eventTest/app/store.js +16 -0
- package/examples/030-todo-app-with-eventTest/app/views/home.js +11 -0
- package/examples/030-todo-app-with-eventTest/app/views/todoDemo.js +88 -0
- package/examples/030-todo-app-with-eventTest/index.html +65 -0
- package/examples/030-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/examples/030-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/examples/030-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/examples/030-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/examples/030-todo-app-with-eventTest/store.d.ts +18 -0
- package/examples/030-todo-app-with-eventTest/style.css +170 -0
- package/examples/030-todo-app-with-eventTest/tests/README.md +208 -0
- package/examples/030-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/examples/030-todo-app-with-eventTest/tests/generateTypes.js +189 -0
- package/examples/030-todo-app-with-eventTest/tests/run.js +20 -0
- package/examples/030-todo-app-with-eventTest/tests/todos.test.js +167 -0
- package/examples/031-todo-app-with-eventTest/LICENSE +55 -0
- package/examples/031-todo-app-with-eventTest/README.md +54 -0
- package/examples/031-todo-app-with-eventTest/TUTORIAL.md +390 -0
- package/examples/031-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
- package/examples/031-todo-app-with-eventTest/app/bridges.js +113 -0
- package/examples/031-todo-app-with-eventTest/app/router.js +26 -0
- package/examples/031-todo-app-with-eventTest/app/store.js +15 -0
- package/examples/031-todo-app-with-eventTest/app/views/home.js +46 -0
- package/examples/031-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
- package/examples/031-todo-app-with-eventTest/devtools/dock.js +41 -0
- package/examples/031-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
- package/examples/031-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
- package/examples/031-todo-app-with-eventTest/devtools/telemetry.js +104 -0
- package/examples/031-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
- package/examples/031-todo-app-with-eventTest/index.html +103 -0
- package/examples/031-todo-app-with-eventTest/package-lock.json +2184 -0
- package/examples/031-todo-app-with-eventTest/package.json +24 -0
- package/examples/031-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/examples/031-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/examples/031-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/examples/031-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
- package/examples/031-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
- package/examples/031-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
- package/examples/031-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
- package/examples/031-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
- package/examples/031-todo-app-with-eventTest/store.d.ts +23 -0
- package/examples/031-todo-app-with-eventTest/style.css +170 -0
- package/examples/031-todo-app-with-eventTest/tests/README.md +208 -0
- package/examples/031-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/examples/031-todo-app-with-eventTest/tests/generateTypes.js +191 -0
- package/examples/031-todo-app-with-eventTest/tests/run.js +20 -0
- package/examples/031-todo-app-with-eventTest/tests/todos.test.js +192 -0
- package/examples/032-todo-app-with-eventTest/LICENSE +55 -0
- package/examples/032-todo-app-with-eventTest/README.md +54 -0
- package/examples/032-todo-app-with-eventTest/TUTORIAL.md +390 -0
- package/examples/032-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
- package/examples/032-todo-app-with-eventTest/app/actions/index.js +153 -0
- package/examples/032-todo-app-with-eventTest/app/bridges.js +113 -0
- package/examples/032-todo-app-with-eventTest/app/router.js +26 -0
- package/examples/032-todo-app-with-eventTest/app/store.js +15 -0
- package/examples/032-todo-app-with-eventTest/app/views/home.js +46 -0
- package/examples/032-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
- package/examples/032-todo-app-with-eventTest/devtools/dock.js +41 -0
- package/examples/032-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
- package/examples/032-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
- package/examples/032-todo-app-with-eventTest/devtools/telemetry.js +104 -0
- package/examples/032-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
- package/examples/032-todo-app-with-eventTest/index.html +87 -0
- package/examples/032-todo-app-with-eventTest/package-lock.json +2184 -0
- package/examples/032-todo-app-with-eventTest/package.json +24 -0
- package/examples/032-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/examples/032-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/examples/032-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/examples/032-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
- package/examples/032-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
- package/examples/032-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
- package/examples/032-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
- package/examples/032-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
- package/examples/032-todo-app-with-eventTest/store.d.ts +23 -0
- package/examples/032-todo-app-with-eventTest/style.css +170 -0
- package/examples/032-todo-app-with-eventTest/tests/README.md +208 -0
- package/examples/032-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/examples/032-todo-app-with-eventTest/tests/generateTypes.js +191 -0
- package/examples/032-todo-app-with-eventTest/tests/run.js +20 -0
- package/examples/032-todo-app-with-eventTest/tests/todos.test.js +192 -0
- package/index.js +14 -3
- package/package.json +16 -7
- package/stateSerializer.js +99 -4
- 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 =
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
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
|
-
|
|
40
|
+
if (!destroyed) {
|
|
41
|
+
// exact path
|
|
42
|
+
bus.dispatchEvent(new CustomEvent(path, { detail: value }));
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
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>
|