@uistate/core 5.0.2 → 5.1.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/eventStateNew.js +149 -0
- package/eventTest.js +1 -1
- package/examples/002-counter-improved/index.html +4 -1
- package/examples/003-input-reactive/index.html +4 -1
- package/examples/004-computed-state/index.html +9 -6
- package/examples/005-conditional-rendering/index.html +7 -4
- package/examples/006-list-rendering/index.html +8 -5
- package/examples/007-form-validation/index.html +11 -8
- package/examples/008-undo-redo/index.html +15 -12
- package/examples/009-localStorage-side-effects/index.html +7 -4
- package/examples/010-decoupled-components/index.html +6 -3
- package/examples/011-async-patterns/index.html +19 -16
- package/examples/028-counter-improved-eventTest/app/store.js +1 -1
- package/examples/028-counter-improved-eventTest/runtime/core/eventStateNew.js +149 -0
- package/examples/028-counter-improved-eventTest/tests/eventTest.js +1 -1
- package/examples/030-todo-app-with-eventTest/app/store.js +1 -1
- package/examples/030-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/examples/030-todo-app-with-eventTest/tests/eventTest.js +1 -1
- package/examples/031-todo-app-with-eventTest/app/store.js +1 -1
- package/examples/031-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +1 -1
- package/examples/031-todo-app-with-eventTest/tests/eventTest.js +1 -1
- package/examples/032-todo-app-with-eventTest/app/store.js +1 -1
- package/examples/032-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +1 -1
- package/examples/032-todo-app-with-eventTest/tests/eventTest.js +1 -1
- package/index.js +2 -2
- package/package.json +3 -1
- package/examples/002-counter-improved/eventState.js +0 -86
- package/examples/003-input-reactive/eventState.js +0 -86
- package/examples/004-computed-state/eventState.js +0 -86
- package/examples/005-conditional-rendering/eventState.js +0 -86
- package/examples/006-list-rendering/eventState.js +0 -86
- package/examples/007-form-validation/eventState.js +0 -86
- package/examples/008-undo-redo/eventState.js +0 -86
- package/examples/009-localStorage-side-effects/eventState.js +0 -86
- package/examples/010-decoupled-components/eventState.js +0 -86
- package/examples/011-async-patterns/eventState.js +0 -86
package/eventStateNew.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventState v2 - Optimized Path-Based State Management
|
|
3
|
+
*
|
|
4
|
+
* A lightweight, performant state management library using path-based subscriptions.
|
|
5
|
+
* Optimized for selective notifications and granular updates.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Path-based get/set operations (e.g., 'user.profile.name')
|
|
9
|
+
* - Selective subscriptions (only relevant subscribers fire)
|
|
10
|
+
* - Wildcard subscriptions (e.g., 'user.*' catches all user changes)
|
|
11
|
+
* - Global subscriptions (e.g., '*' catches all changes)
|
|
12
|
+
* - Zero dependencies
|
|
13
|
+
* - ~2KB minified
|
|
14
|
+
*
|
|
15
|
+
* Performance characteristics:
|
|
16
|
+
* - 2-9x faster than Zustand for selective subscriptions
|
|
17
|
+
* - Competitive overall performance
|
|
18
|
+
* - Minimal rendering overhead (1.27x faster paint times)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const store = createEventState({ count: 0, user: { name: 'Alice' } });
|
|
22
|
+
*
|
|
23
|
+
* // Subscribe to specific path (receives value directly)
|
|
24
|
+
* const unsub = store.subscribe('count', (value) => {
|
|
25
|
+
* console.log('Count changed:', value);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Update state
|
|
29
|
+
* store.set('count', 1);
|
|
30
|
+
*
|
|
31
|
+
* // Get state
|
|
32
|
+
* const count = store.get('count');
|
|
33
|
+
*
|
|
34
|
+
* // Wildcard subscription
|
|
35
|
+
* store.subscribe('user.*', ({ path, value }) => {
|
|
36
|
+
* console.log(`User field ${path} changed to:`, value);
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* // Global subscription
|
|
40
|
+
* store.subscribe('*', ({ path, value }) => {
|
|
41
|
+
* console.log(`State changed at ${path}:`, value);
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Cleanup
|
|
45
|
+
* unsub();
|
|
46
|
+
* store.destroy();
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
export function createEventState(initial = {}) {
|
|
50
|
+
const state = JSON.parse(JSON.stringify(initial));
|
|
51
|
+
const listeners = new Map();
|
|
52
|
+
let destroyed = false;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
/**
|
|
56
|
+
* Get value at path
|
|
57
|
+
* @param {string} path - Dot-separated path (e.g., 'user.profile.name')
|
|
58
|
+
* @returns {*} Value at path, or entire state if no path provided
|
|
59
|
+
*/
|
|
60
|
+
get(path) {
|
|
61
|
+
if (destroyed) throw new Error('Cannot get from destroyed store');
|
|
62
|
+
if (!path) return state;
|
|
63
|
+
return path.split(".").reduce((obj, key) => obj?.[key], state);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set value at path and notify subscribers
|
|
68
|
+
* @param {string} path - Dot-separated path (e.g., 'user.profile.name')
|
|
69
|
+
* @param {*} value - New value
|
|
70
|
+
* @returns {*} The value that was set
|
|
71
|
+
*/
|
|
72
|
+
set(path, value) {
|
|
73
|
+
if (destroyed) throw new Error('Cannot set on destroyed store');
|
|
74
|
+
if (!path) return value;
|
|
75
|
+
|
|
76
|
+
const parts = path.split(".");
|
|
77
|
+
const key = parts.pop();
|
|
78
|
+
let cur = state;
|
|
79
|
+
|
|
80
|
+
// Navigate to parent object, creating nested objects as needed
|
|
81
|
+
for (const p of parts) {
|
|
82
|
+
if (!cur[p]) cur[p] = {};
|
|
83
|
+
cur = cur[p];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const oldValue = cur[key];
|
|
87
|
+
cur[key] = value;
|
|
88
|
+
|
|
89
|
+
if (!destroyed) {
|
|
90
|
+
const detail = { path, value, oldValue };
|
|
91
|
+
|
|
92
|
+
// Notify exact path subscribers (pass value directly for backwards compatibility)
|
|
93
|
+
const exactListeners = listeners.get(path);
|
|
94
|
+
if (exactListeners) {
|
|
95
|
+
exactListeners.forEach(cb => cb(value));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Notify wildcard subscribers for all parent paths (pass detail object)
|
|
99
|
+
for (let i = 0; i < parts.length; i++) {
|
|
100
|
+
const parentPath = parts.slice(0, i + 1).join('.');
|
|
101
|
+
const wildcardListeners = listeners.get(`${parentPath}.*`);
|
|
102
|
+
if (wildcardListeners) {
|
|
103
|
+
wildcardListeners.forEach(cb => cb(detail));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Notify global subscribers (pass detail object)
|
|
108
|
+
const globalListeners = listeners.get('*');
|
|
109
|
+
if (globalListeners) {
|
|
110
|
+
globalListeners.forEach(cb => cb(detail));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return value;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Subscribe to changes at path
|
|
119
|
+
* @param {string} path - Path to subscribe to (supports wildcards: 'user.*', '*')
|
|
120
|
+
* @param {Function} handler - Callback function receiving { path, value, oldValue }
|
|
121
|
+
* @returns {Function} Unsubscribe function
|
|
122
|
+
*/
|
|
123
|
+
subscribe(path, handler) {
|
|
124
|
+
if (destroyed) throw new Error('Cannot subscribe to destroyed store');
|
|
125
|
+
if (!path || typeof handler !== 'function') {
|
|
126
|
+
throw new TypeError('subscribe requires path and handler');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!listeners.has(path)) {
|
|
130
|
+
listeners.set(path, new Set());
|
|
131
|
+
}
|
|
132
|
+
listeners.get(path).add(handler);
|
|
133
|
+
|
|
134
|
+
return () => listeners.get(path)?.delete(handler);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Destroy store and clear all subscriptions
|
|
139
|
+
*/
|
|
140
|
+
destroy() {
|
|
141
|
+
if (!destroyed) {
|
|
142
|
+
destroyed = true;
|
|
143
|
+
listeners.clear();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default createEventState;
|
package/eventTest.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* Provides TDD-style testing with type extraction capabilities
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { createEventState } from './
|
|
27
|
+
import { createEventState } from './eventStateNew.js';
|
|
28
28
|
|
|
29
29
|
export function createEventTest(initialState = {}) {
|
|
30
30
|
const store = createEventState(initialState);
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>002 Counter Improved - EventState</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
9
|
+
</script>
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
9
12
|
<h1>Counter: <span id="count">0</span></h1>
|
|
@@ -12,7 +15,7 @@
|
|
|
12
15
|
<button id="double">x2</button>
|
|
13
16
|
|
|
14
17
|
<script type="module">
|
|
15
|
-
import { createEventState } from '
|
|
18
|
+
import { createEventState } from '@uistate/core';
|
|
16
19
|
|
|
17
20
|
// Create store
|
|
18
21
|
const store = createEventState({ count: 0 });
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>003 Input Reactive - EventState</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
9
|
+
</script>
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
9
12
|
<h1>Type something:</h1>
|
|
@@ -11,7 +14,7 @@
|
|
|
11
14
|
<p id="output"></p>
|
|
12
15
|
|
|
13
16
|
<script type="module">
|
|
14
|
-
import { createEventState } from '
|
|
17
|
+
import { createEventState } from '@uistate/core';
|
|
15
18
|
|
|
16
19
|
// Create store
|
|
17
20
|
const store = createEventState({ text: '' });
|
|
@@ -4,23 +4,26 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>004 Computed State - EventState</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
9
|
+
</script>
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
9
12
|
<h1>Computed State</h1>
|
|
10
|
-
|
|
13
|
+
|
|
11
14
|
<label>First Name: <input type="text" id="firstName" placeholder="Abraham"></label><br>
|
|
12
15
|
<label>Last Name: <input type="text" id="lastName" placeholder="Lincoln"></label><br>
|
|
13
|
-
|
|
16
|
+
|
|
14
17
|
<p><strong>Full Name:</strong> <span id="fullName"></span></p>
|
|
15
18
|
<p><strong>Character Count:</strong> <span id="charCount">0</span></p>
|
|
16
19
|
|
|
17
20
|
<script type="module">
|
|
18
|
-
import { createEventState } from '
|
|
21
|
+
import { createEventState } from '@uistate/core';
|
|
19
22
|
|
|
20
23
|
// Create store
|
|
21
|
-
const store = createEventState({
|
|
22
|
-
firstName: '',
|
|
23
|
-
lastName: ''
|
|
24
|
+
const store = createEventState({
|
|
25
|
+
firstName: '',
|
|
26
|
+
lastName: ''
|
|
24
27
|
});
|
|
25
28
|
|
|
26
29
|
// Computed: full name (derived from firstName + lastName)
|
|
@@ -4,19 +4,22 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>005 Conditional Rendering - EventState</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
9
|
+
</script>
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
9
12
|
<h1>Conditional Rendering</h1>
|
|
10
|
-
|
|
13
|
+
|
|
11
14
|
<label>
|
|
12
15
|
<input type="checkbox" id="toggle">
|
|
13
16
|
Show secret message
|
|
14
17
|
</label>
|
|
15
|
-
|
|
16
|
-
<p id="message" style="display: none;"
|
|
18
|
+
|
|
19
|
+
<p id="message" style="display: none;">You found the secret message!</p>
|
|
17
20
|
|
|
18
21
|
<script type="module">
|
|
19
|
-
import { createEventState } from '
|
|
22
|
+
import { createEventState } from '@uistate/core';
|
|
20
23
|
|
|
21
24
|
// Create store
|
|
22
25
|
const store = createEventState({ isVisible: false });
|
|
@@ -4,17 +4,20 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>006 List Rendering - EventState</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
9
|
+
</script>
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
9
12
|
<h1>Shopping List</h1>
|
|
10
|
-
|
|
13
|
+
|
|
11
14
|
<input type="text" id="itemInput" placeholder="Add item...">
|
|
12
15
|
<button id="addBtn">Add</button>
|
|
13
|
-
|
|
16
|
+
|
|
14
17
|
<ul id="itemList"></ul>
|
|
15
18
|
|
|
16
19
|
<script type="module">
|
|
17
|
-
import { createEventState } from '
|
|
20
|
+
import { createEventState } from '@uistate/core';
|
|
18
21
|
|
|
19
22
|
// Create store
|
|
20
23
|
const store = createEventState({ items: [] });
|
|
@@ -23,7 +26,7 @@
|
|
|
23
26
|
const renderList = () => {
|
|
24
27
|
const items = store.get('items');
|
|
25
28
|
const list = document.getElementById('itemList');
|
|
26
|
-
|
|
29
|
+
|
|
27
30
|
if (items.length === 0) {
|
|
28
31
|
list.innerHTML = '<li><em>No items yet</em></li>';
|
|
29
32
|
} else {
|
|
@@ -38,7 +41,7 @@
|
|
|
38
41
|
document.getElementById('addBtn').onclick = () => {
|
|
39
42
|
const input = document.getElementById('itemInput');
|
|
40
43
|
const value = input.value.trim();
|
|
41
|
-
|
|
44
|
+
|
|
42
45
|
if (value) {
|
|
43
46
|
const items = store.get('items');
|
|
44
47
|
store.set('items', [...items, value]);
|
|
@@ -4,30 +4,33 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>007 Form Validation - EventState</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
9
|
+
</script>
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
9
12
|
<h1>Sign Up Form</h1>
|
|
10
|
-
|
|
13
|
+
|
|
11
14
|
<label>
|
|
12
15
|
Email:
|
|
13
16
|
<input type="email" id="email" placeholder="you@example.com">
|
|
14
17
|
</label>
|
|
15
18
|
<p id="emailError" style="color: red;"></p>
|
|
16
|
-
|
|
19
|
+
|
|
17
20
|
<label>
|
|
18
21
|
Password:
|
|
19
22
|
<input type="password" id="password" placeholder="Min 8 characters">
|
|
20
23
|
</label>
|
|
21
24
|
<p id="passwordError" style="color: red;"></p>
|
|
22
|
-
|
|
25
|
+
|
|
23
26
|
<button id="submitBtn" disabled>Submit</button>
|
|
24
27
|
<p id="submitMessage" style="color: green;"></p>
|
|
25
28
|
|
|
26
29
|
<script type="module">
|
|
27
|
-
import { createEventState } from '
|
|
30
|
+
import { createEventState } from '@uistate/core';
|
|
28
31
|
|
|
29
32
|
// Create store
|
|
30
|
-
const store = createEventState({
|
|
33
|
+
const store = createEventState({
|
|
31
34
|
email: '',
|
|
32
35
|
password: '',
|
|
33
36
|
emailValid: false,
|
|
@@ -55,7 +58,7 @@
|
|
|
55
58
|
store.subscribe('email', (value) => {
|
|
56
59
|
const isValid = validateEmail(value);
|
|
57
60
|
store.set('emailValid', isValid);
|
|
58
|
-
|
|
61
|
+
|
|
59
62
|
const error = document.getElementById('emailError');
|
|
60
63
|
error.textContent = value && !isValid ? 'Invalid email address' : '';
|
|
61
64
|
});
|
|
@@ -64,7 +67,7 @@
|
|
|
64
67
|
store.subscribe('password', (value) => {
|
|
65
68
|
const isValid = validatePassword(value);
|
|
66
69
|
store.set('passwordValid', isValid);
|
|
67
|
-
|
|
70
|
+
|
|
68
71
|
const error = document.getElementById('passwordError');
|
|
69
72
|
error.textContent = value && !isValid ? 'Password must be at least 8 characters' : '';
|
|
70
73
|
});
|
|
@@ -87,7 +90,7 @@
|
|
|
87
90
|
const email = store.get('email');
|
|
88
91
|
const message = document.getElementById('submitMessage');
|
|
89
92
|
message.textContent = `✓ Form submitted for ${email}`;
|
|
90
|
-
|
|
93
|
+
|
|
91
94
|
// Clear form
|
|
92
95
|
document.getElementById('email').value = '';
|
|
93
96
|
document.getElementById('password').value = '';
|
|
@@ -4,25 +4,28 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>008 Undo/Redo - EventState</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
9
|
+
</script>
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
9
12
|
<h1>Counter with Time Travel</h1>
|
|
10
|
-
|
|
13
|
+
|
|
11
14
|
<h2>Count: <span id="count">0</span></h2>
|
|
12
|
-
|
|
15
|
+
|
|
13
16
|
<button id="increment">+1</button>
|
|
14
17
|
<button id="decrement">-1</button>
|
|
15
18
|
<button id="double">×2</button>
|
|
16
|
-
|
|
19
|
+
|
|
17
20
|
<hr>
|
|
18
|
-
|
|
21
|
+
|
|
19
22
|
<button id="undo" disabled>⟲ Undo</button>
|
|
20
23
|
<button id="redo" disabled>⟳ Redo</button>
|
|
21
|
-
|
|
24
|
+
|
|
22
25
|
<p><small>History: <span id="historyInfo">0 states</span></small></p>
|
|
23
26
|
|
|
24
27
|
<script type="module">
|
|
25
|
-
import { createEventState } from '
|
|
28
|
+
import { createEventState } from '@uistate/core';
|
|
26
29
|
|
|
27
30
|
// Create store
|
|
28
31
|
const store = createEventState({ count: 0 });
|
|
@@ -36,29 +39,29 @@
|
|
|
36
39
|
const updateUI = () => {
|
|
37
40
|
const count = store.get('count');
|
|
38
41
|
document.getElementById('count').textContent = count;
|
|
39
|
-
|
|
42
|
+
|
|
40
43
|
// Update button states
|
|
41
44
|
document.getElementById('undo').disabled = historyIndex === 0;
|
|
42
45
|
document.getElementById('redo').disabled = historyIndex === history.length - 1;
|
|
43
|
-
|
|
46
|
+
|
|
44
47
|
// Update history info
|
|
45
|
-
document.getElementById('historyInfo').textContent =
|
|
48
|
+
document.getElementById('historyInfo').textContent =
|
|
46
49
|
`${historyIndex + 1} of ${history.length} states`;
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
// Subscribe to count changes and record history
|
|
50
53
|
store.subscribe('count', (value) => {
|
|
51
54
|
updateUI();
|
|
52
|
-
|
|
55
|
+
|
|
53
56
|
// Only record if not time traveling
|
|
54
57
|
if (!isTimeTravel) {
|
|
55
58
|
// Remove any "future" states if we're in the middle of history
|
|
56
59
|
history = history.slice(0, historyIndex + 1);
|
|
57
|
-
|
|
60
|
+
|
|
58
61
|
// Add new state
|
|
59
62
|
history.push(value);
|
|
60
63
|
historyIndex = history.length - 1;
|
|
61
|
-
|
|
64
|
+
|
|
62
65
|
updateUI();
|
|
63
66
|
}
|
|
64
67
|
});
|
|
@@ -4,20 +4,23 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>009 localStorage Side Effects - EventState</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
9
|
+
</script>
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
9
12
|
<h1>Persistent Counter</h1>
|
|
10
|
-
|
|
13
|
+
|
|
11
14
|
<h2>Count: <span id="count">0</span></h2>
|
|
12
|
-
|
|
15
|
+
|
|
13
16
|
<button id="increment">+1</button>
|
|
14
17
|
<button id="decrement">-1</button>
|
|
15
18
|
<button id="reset">Reset</button>
|
|
16
|
-
|
|
19
|
+
|
|
17
20
|
<p><small>Reload the page - your count persists!</small></p>
|
|
18
21
|
|
|
19
22
|
<script type="module">
|
|
20
|
-
import { createEventState } from '
|
|
23
|
+
import { createEventState } from '@uistate/core';
|
|
21
24
|
|
|
22
25
|
const STORAGE_KEY = 'eventstate-counter';
|
|
23
26
|
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
margin: 10px 0;
|
|
12
12
|
}
|
|
13
13
|
</style>
|
|
14
|
+
<script type="importmap">
|
|
15
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
16
|
+
</script>
|
|
14
17
|
</head>
|
|
15
18
|
<body>
|
|
16
19
|
<h1>Decoupled Components</h1>
|
|
@@ -37,10 +40,10 @@
|
|
|
37
40
|
</div>
|
|
38
41
|
|
|
39
42
|
<script type="module">
|
|
40
|
-
import { createEventState } from '
|
|
43
|
+
import { createEventState } from '@uistate/core';
|
|
41
44
|
|
|
42
45
|
// Shared store - the ONLY connection between components
|
|
43
|
-
const store = createEventState({
|
|
46
|
+
const store = createEventState({
|
|
44
47
|
message: '',
|
|
45
48
|
messageCount: 0
|
|
46
49
|
});
|
|
@@ -50,7 +53,7 @@
|
|
|
50
53
|
document.getElementById('sendBtn').onclick = () => {
|
|
51
54
|
const input = document.getElementById('messageInput');
|
|
52
55
|
const value = input.value.trim();
|
|
53
|
-
|
|
56
|
+
|
|
54
57
|
if (value) {
|
|
55
58
|
store.set('message', value);
|
|
56
59
|
store.set('messageCount', store.get('messageCount') + 1);
|
|
@@ -4,27 +4,30 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>011 Async Patterns - EventState</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
9
|
+
</script>
|
|
7
10
|
</head>
|
|
8
11
|
<body>
|
|
9
12
|
<h1>Async Patterns</h1>
|
|
10
|
-
|
|
13
|
+
|
|
11
14
|
<h2>Debounced Search</h2>
|
|
12
15
|
<input type="text" id="searchInput" placeholder="Type to search...">
|
|
13
16
|
<p id="searchStatus"></p>
|
|
14
17
|
<p><strong>Search result:</strong> <span id="searchResult">(type something)</span></p>
|
|
15
|
-
|
|
18
|
+
|
|
16
19
|
<hr>
|
|
17
|
-
|
|
20
|
+
|
|
18
21
|
<h2>Simulated API Call</h2>
|
|
19
22
|
<button id="fetchBtn">Fetch Data</button>
|
|
20
23
|
<p id="loadingStatus"></p>
|
|
21
24
|
<p><strong>Data:</strong> <span id="apiData">(not loaded)</span></p>
|
|
22
25
|
|
|
23
26
|
<script type="module">
|
|
24
|
-
import { createEventState } from '
|
|
27
|
+
import { createEventState } from '@uistate/core';
|
|
25
28
|
|
|
26
29
|
// Create store
|
|
27
|
-
const store = createEventState({
|
|
30
|
+
const store = createEventState({
|
|
28
31
|
searchQuery: '',
|
|
29
32
|
searchResult: '',
|
|
30
33
|
isSearching: false,
|
|
@@ -48,16 +51,16 @@
|
|
|
48
51
|
store.subscribe('searchQuery', async (value) => {
|
|
49
52
|
// Clear previous timeout
|
|
50
53
|
clearTimeout(searchTimeout);
|
|
51
|
-
|
|
54
|
+
|
|
52
55
|
if (!value) {
|
|
53
56
|
store.set('searchResult', '');
|
|
54
57
|
store.set('isSearching', false);
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
57
|
-
|
|
60
|
+
|
|
58
61
|
// Show searching indicator
|
|
59
62
|
store.set('isSearching', true);
|
|
60
|
-
|
|
63
|
+
|
|
61
64
|
// Debounce: wait 300ms before searching
|
|
62
65
|
searchTimeout = setTimeout(async () => {
|
|
63
66
|
const result = await performSearch(value);
|
|
@@ -68,12 +71,12 @@
|
|
|
68
71
|
|
|
69
72
|
// Update UI for search status
|
|
70
73
|
store.subscribe('isSearching', (value) => {
|
|
71
|
-
document.getElementById('searchStatus').textContent =
|
|
72
|
-
value ? '
|
|
74
|
+
document.getElementById('searchStatus').textContent =
|
|
75
|
+
value ? 'Searching...' : '';
|
|
73
76
|
});
|
|
74
77
|
|
|
75
78
|
store.subscribe('searchResult', (value) => {
|
|
76
|
-
document.getElementById('searchResult').textContent =
|
|
79
|
+
document.getElementById('searchResult').textContent =
|
|
77
80
|
value || '(type something)';
|
|
78
81
|
});
|
|
79
82
|
|
|
@@ -83,12 +86,12 @@
|
|
|
83
86
|
};
|
|
84
87
|
|
|
85
88
|
// ===== SIMULATED API CALL =====
|
|
86
|
-
|
|
89
|
+
|
|
87
90
|
// Simulate API fetch
|
|
88
91
|
const fetchData = () => {
|
|
89
92
|
return new Promise((resolve) => {
|
|
90
93
|
setTimeout(() => {
|
|
91
|
-
resolve({
|
|
94
|
+
resolve({
|
|
92
95
|
id: Math.floor(Math.random() * 1000),
|
|
93
96
|
timestamp: new Date().toLocaleTimeString()
|
|
94
97
|
});
|
|
@@ -98,7 +101,7 @@
|
|
|
98
101
|
|
|
99
102
|
// Subscribe to loading state
|
|
100
103
|
store.subscribe('isLoading', (value) => {
|
|
101
|
-
document.getElementById('loadingStatus').textContent =
|
|
104
|
+
document.getElementById('loadingStatus').textContent =
|
|
102
105
|
value ? '⏳ Loading...' : '';
|
|
103
106
|
document.getElementById('fetchBtn').disabled = value;
|
|
104
107
|
});
|
|
@@ -106,7 +109,7 @@
|
|
|
106
109
|
// Subscribe to API data
|
|
107
110
|
store.subscribe('apiData', (value) => {
|
|
108
111
|
if (value) {
|
|
109
|
-
document.getElementById('apiData').textContent =
|
|
112
|
+
document.getElementById('apiData').textContent =
|
|
110
113
|
`ID: ${value.id}, Time: ${value.timestamp}`;
|
|
111
114
|
}
|
|
112
115
|
});
|
|
@@ -114,7 +117,7 @@
|
|
|
114
117
|
// Fetch button
|
|
115
118
|
document.getElementById('fetchBtn').onclick = async () => {
|
|
116
119
|
store.set('isLoading', true);
|
|
117
|
-
|
|
120
|
+
|
|
118
121
|
try {
|
|
119
122
|
const data = await fetchData();
|
|
120
123
|
store.set('apiData', data);
|