@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
|
@@ -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 };
|