@uistate/examples 1.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/README.md +40 -0
- package/cssState/.gitkeep +0 -0
- package/eventState/001-counter/README.md +44 -0
- package/eventState/001-counter/index.html +33 -0
- package/eventState/002-counter-improved/README.md +44 -0
- package/eventState/002-counter-improved/index.html +47 -0
- package/eventState/003-input-reactive/README.md +44 -0
- package/eventState/003-input-reactive/index.html +33 -0
- package/eventState/004-computed-state/README.md +45 -0
- package/eventState/004-computed-state/index.html +65 -0
- package/eventState/005-conditional-rendering/README.md +42 -0
- package/eventState/005-conditional-rendering/index.html +39 -0
- package/eventState/006-list-rendering/README.md +49 -0
- package/eventState/006-list-rendering/index.html +63 -0
- package/eventState/007-form-validation/README.md +52 -0
- package/eventState/007-form-validation/index.html +102 -0
- package/eventState/008-undo-redo/README.md +70 -0
- package/eventState/008-undo-redo/index.html +108 -0
- package/eventState/009-localStorage-side-effects/README.md +72 -0
- package/eventState/009-localStorage-side-effects/index.html +57 -0
- package/eventState/010-decoupled-components/README.md +74 -0
- package/eventState/010-decoupled-components/index.html +93 -0
- package/eventState/011-async-patterns/README.md +98 -0
- package/eventState/011-async-patterns/index.html +132 -0
- package/eventState/028-counter-improved-eventTest/LICENSE +55 -0
- package/eventState/028-counter-improved-eventTest/README.md +131 -0
- package/eventState/028-counter-improved-eventTest/app/store.js +9 -0
- package/eventState/028-counter-improved-eventTest/index.html +49 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/eventState.js +100 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/eventStateNew.js +149 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/helpers.js +212 -0
- package/eventState/028-counter-improved-eventTest/runtime/core/router.js +271 -0
- package/eventState/028-counter-improved-eventTest/store.d.ts +8 -0
- package/eventState/028-counter-improved-eventTest/style.css +170 -0
- package/eventState/028-counter-improved-eventTest/tests/README.md +208 -0
- package/eventState/028-counter-improved-eventTest/tests/counter.test.js +116 -0
- package/eventState/028-counter-improved-eventTest/tests/eventTest.js +176 -0
- package/eventState/028-counter-improved-eventTest/tests/generateTypes.js +168 -0
- package/eventState/028-counter-improved-eventTest/tests/run.js +20 -0
- package/eventState/030-todo-app-with-eventTest/LICENSE +55 -0
- package/eventState/030-todo-app-with-eventTest/README.md +121 -0
- package/eventState/030-todo-app-with-eventTest/app/router.js +25 -0
- package/eventState/030-todo-app-with-eventTest/app/store.js +16 -0
- package/eventState/030-todo-app-with-eventTest/app/views/home.js +11 -0
- package/eventState/030-todo-app-with-eventTest/app/views/todoDemo.js +88 -0
- package/eventState/030-todo-app-with-eventTest/index.html +65 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/eventState/030-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/eventState/030-todo-app-with-eventTest/store.d.ts +18 -0
- package/eventState/030-todo-app-with-eventTest/style.css +170 -0
- package/eventState/030-todo-app-with-eventTest/tests/README.md +208 -0
- package/eventState/030-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/eventState/030-todo-app-with-eventTest/tests/generateTypes.js +189 -0
- package/eventState/030-todo-app-with-eventTest/tests/run.js +20 -0
- package/eventState/030-todo-app-with-eventTest/tests/todos.test.js +167 -0
- package/eventState/031-todo-app-with-eventTest/LICENSE +55 -0
- package/eventState/031-todo-app-with-eventTest/README.md +54 -0
- package/eventState/031-todo-app-with-eventTest/TUTORIAL.md +390 -0
- package/eventState/031-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
- package/eventState/031-todo-app-with-eventTest/app/bridges.js +113 -0
- package/eventState/031-todo-app-with-eventTest/app/router.js +26 -0
- package/eventState/031-todo-app-with-eventTest/app/store.js +15 -0
- package/eventState/031-todo-app-with-eventTest/app/views/home.js +46 -0
- package/eventState/031-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
- package/eventState/031-todo-app-with-eventTest/devtools/dock.js +41 -0
- package/eventState/031-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
- package/eventState/031-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
- package/eventState/031-todo-app-with-eventTest/devtools/telemetry.js +104 -0
- package/eventState/031-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
- package/eventState/031-todo-app-with-eventTest/index.html +103 -0
- package/eventState/031-todo-app-with-eventTest/package-lock.json +2184 -0
- package/eventState/031-todo-app-with-eventTest/package.json +24 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/eventState/031-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
- package/eventState/031-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
- package/eventState/031-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
- package/eventState/031-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
- package/eventState/031-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
- package/eventState/031-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
- package/eventState/031-todo-app-with-eventTest/store.d.ts +23 -0
- package/eventState/031-todo-app-with-eventTest/style.css +170 -0
- package/eventState/031-todo-app-with-eventTest/tests/README.md +208 -0
- package/eventState/031-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/eventState/031-todo-app-with-eventTest/tests/generateTypes.js +191 -0
- package/eventState/031-todo-app-with-eventTest/tests/run.js +20 -0
- package/eventState/031-todo-app-with-eventTest/tests/todos.test.js +192 -0
- package/eventState/032-todo-app-with-eventTest/LICENSE +55 -0
- package/eventState/032-todo-app-with-eventTest/README.md +54 -0
- package/eventState/032-todo-app-with-eventTest/TUTORIAL.md +390 -0
- package/eventState/032-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
- package/eventState/032-todo-app-with-eventTest/app/actions/index.js +153 -0
- package/eventState/032-todo-app-with-eventTest/app/bridges.js +113 -0
- package/eventState/032-todo-app-with-eventTest/app/router.js +26 -0
- package/eventState/032-todo-app-with-eventTest/app/store.js +15 -0
- package/eventState/032-todo-app-with-eventTest/app/views/home.js +46 -0
- package/eventState/032-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
- package/eventState/032-todo-app-with-eventTest/devtools/dock.js +41 -0
- package/eventState/032-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
- package/eventState/032-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
- package/eventState/032-todo-app-with-eventTest/devtools/telemetry.js +104 -0
- package/eventState/032-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
- package/eventState/032-todo-app-with-eventTest/index.html +87 -0
- package/eventState/032-todo-app-with-eventTest/package-lock.json +2184 -0
- package/eventState/032-todo-app-with-eventTest/package.json +24 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/eventState/032-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
- package/eventState/032-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
- package/eventState/032-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
- package/eventState/032-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
- package/eventState/032-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
- package/eventState/032-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
- package/eventState/032-todo-app-with-eventTest/store.d.ts +23 -0
- package/eventState/032-todo-app-with-eventTest/style.css +170 -0
- package/eventState/032-todo-app-with-eventTest/tests/README.md +208 -0
- package/eventState/032-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/eventState/032-todo-app-with-eventTest/tests/generateTypes.js +191 -0
- package/eventState/032-todo-app-with-eventTest/tests/run.js +20 -0
- package/eventState/032-todo-app-with-eventTest/tests/todos.test.js +192 -0
- package/package.json +27 -0
|
@@ -0,0 +1,102 @@
|
|
|
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>007 Form Validation - EventState</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
9
|
+
</script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<h1>Sign Up Form</h1>
|
|
13
|
+
|
|
14
|
+
<label>
|
|
15
|
+
Email:
|
|
16
|
+
<input type="email" id="email" placeholder="you@example.com">
|
|
17
|
+
</label>
|
|
18
|
+
<p id="emailError" style="color: red;"></p>
|
|
19
|
+
|
|
20
|
+
<label>
|
|
21
|
+
Password:
|
|
22
|
+
<input type="password" id="password" placeholder="Min 8 characters">
|
|
23
|
+
</label>
|
|
24
|
+
<p id="passwordError" style="color: red;"></p>
|
|
25
|
+
|
|
26
|
+
<button id="submitBtn" disabled>Submit</button>
|
|
27
|
+
<p id="submitMessage" style="color: green;"></p>
|
|
28
|
+
|
|
29
|
+
<script type="module">
|
|
30
|
+
import { createEventState } from '@uistate/core';
|
|
31
|
+
|
|
32
|
+
// Create store
|
|
33
|
+
const store = createEventState({
|
|
34
|
+
email: '',
|
|
35
|
+
password: '',
|
|
36
|
+
emailValid: false,
|
|
37
|
+
passwordValid: false
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Validate email
|
|
41
|
+
const validateEmail = (email) => {
|
|
42
|
+
return email.includes('@') && email.includes('.');
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Validate password
|
|
46
|
+
const validatePassword = (password) => {
|
|
47
|
+
return password.length >= 8;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Update submit button state
|
|
51
|
+
const updateSubmitButton = () => {
|
|
52
|
+
const emailValid = store.get('emailValid');
|
|
53
|
+
const passwordValid = store.get('passwordValid');
|
|
54
|
+
document.getElementById('submitBtn').disabled = !(emailValid && passwordValid);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Subscribe to email changes
|
|
58
|
+
store.subscribe('email', (value) => {
|
|
59
|
+
const isValid = validateEmail(value);
|
|
60
|
+
store.set('emailValid', isValid);
|
|
61
|
+
|
|
62
|
+
const error = document.getElementById('emailError');
|
|
63
|
+
error.textContent = value && !isValid ? 'Invalid email address' : '';
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Subscribe to password changes
|
|
67
|
+
store.subscribe('password', (value) => {
|
|
68
|
+
const isValid = validatePassword(value);
|
|
69
|
+
store.set('passwordValid', isValid);
|
|
70
|
+
|
|
71
|
+
const error = document.getElementById('passwordError');
|
|
72
|
+
error.textContent = value && !isValid ? 'Password must be at least 8 characters' : '';
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Subscribe to validation state changes
|
|
76
|
+
store.subscribe('emailValid', updateSubmitButton);
|
|
77
|
+
store.subscribe('passwordValid', updateSubmitButton);
|
|
78
|
+
|
|
79
|
+
// Wire up inputs
|
|
80
|
+
document.getElementById('email').oninput = (e) => {
|
|
81
|
+
store.set('email', e.target.value);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
document.getElementById('password').oninput = (e) => {
|
|
85
|
+
store.set('password', e.target.value);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Handle submit
|
|
89
|
+
document.getElementById('submitBtn').onclick = () => {
|
|
90
|
+
const email = store.get('email');
|
|
91
|
+
const message = document.getElementById('submitMessage');
|
|
92
|
+
message.textContent = `✓ Form submitted for ${email}`;
|
|
93
|
+
|
|
94
|
+
// Clear form
|
|
95
|
+
document.getElementById('email').value = '';
|
|
96
|
+
document.getElementById('password').value = '';
|
|
97
|
+
store.set('email', '');
|
|
98
|
+
store.set('password', '');
|
|
99
|
+
};
|
|
100
|
+
</script>
|
|
101
|
+
</body>
|
|
102
|
+
</html>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# 008 Undo/Redo - Time Travel Debugging
|
|
2
|
+
|
|
3
|
+
Demonstrates how EventState's event-driven architecture makes undo/redo trivial.
|
|
4
|
+
|
|
5
|
+
## What's Here
|
|
6
|
+
|
|
7
|
+
- Counter with multiple operations
|
|
8
|
+
- Undo/Redo buttons
|
|
9
|
+
- History tracking showing current position
|
|
10
|
+
- **No special library needed** - just an array
|
|
11
|
+
|
|
12
|
+
## How It Works
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
// 1. Track history in an array
|
|
16
|
+
let history = [0];
|
|
17
|
+
let historyIndex = 0;
|
|
18
|
+
|
|
19
|
+
// 2. Record state changes via subscription
|
|
20
|
+
store.subscribe('count', (value) => {
|
|
21
|
+
if (!isTimeTravel) {
|
|
22
|
+
history.push(value);
|
|
23
|
+
historyIndex = history.length - 1;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// 3. Undo = go back in history
|
|
28
|
+
document.getElementById('undo').onclick = () => {
|
|
29
|
+
historyIndex--;
|
|
30
|
+
isTimeTravel = true;
|
|
31
|
+
store.set('count', history[historyIndex]);
|
|
32
|
+
isTimeTravel = false;
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Key Insight
|
|
37
|
+
|
|
38
|
+
**Time travel is just replaying state.**
|
|
39
|
+
|
|
40
|
+
Because EventState uses **events** for all state changes, you can:
|
|
41
|
+
- ✅ Record every change automatically (wildcard subscription)
|
|
42
|
+
- ✅ Replay any previous state
|
|
43
|
+
- ✅ Build undo/redo in ~30 lines
|
|
44
|
+
|
|
45
|
+
Other frameworks need:
|
|
46
|
+
- React: Redux DevTools, Immer, or custom middleware
|
|
47
|
+
- Vue: Vuex plugins or manual history tracking
|
|
48
|
+
- Svelte: Custom stores with history logic
|
|
49
|
+
|
|
50
|
+
**EventState:** Just subscribe to `'*'` and push to an array. That's it.
|
|
51
|
+
|
|
52
|
+
## This is Telemetry
|
|
53
|
+
|
|
54
|
+
This example shows the **foundation of telemetry**:
|
|
55
|
+
- Every state change is an event
|
|
56
|
+
- Events can be logged, replayed, or analyzed
|
|
57
|
+
- Time-travel debugging comes for free
|
|
58
|
+
|
|
59
|
+
In the full UIstate framework, this becomes:
|
|
60
|
+
- Automatic telemetry logging
|
|
61
|
+
- Event sequence testing (`eventTest.js`)
|
|
62
|
+
- Production debugging and replay
|
|
63
|
+
|
|
64
|
+
## Run It
|
|
65
|
+
|
|
66
|
+
Open `index.html` in a browser. Click operations, then use Undo/Redo to travel through time.
|
|
67
|
+
|
|
68
|
+
## Try This
|
|
69
|
+
|
|
70
|
+
Make multiple changes, undo halfway, then make a new change. Notice how the "future" states are discarded (like Git branches).
|
|
@@ -0,0 +1,108 @@
|
|
|
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>008 Undo/Redo - EventState</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
9
|
+
</script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<h1>Counter with Time Travel</h1>
|
|
13
|
+
|
|
14
|
+
<h2>Count: <span id="count">0</span></h2>
|
|
15
|
+
|
|
16
|
+
<button id="increment">+1</button>
|
|
17
|
+
<button id="decrement">-1</button>
|
|
18
|
+
<button id="double">×2</button>
|
|
19
|
+
|
|
20
|
+
<hr>
|
|
21
|
+
|
|
22
|
+
<button id="undo" disabled>⟲ Undo</button>
|
|
23
|
+
<button id="redo" disabled>⟳ Redo</button>
|
|
24
|
+
|
|
25
|
+
<p><small>History: <span id="historyInfo">0 states</span></small></p>
|
|
26
|
+
|
|
27
|
+
<script type="module">
|
|
28
|
+
import { createEventState } from '@uistate/core';
|
|
29
|
+
|
|
30
|
+
// Create store
|
|
31
|
+
const store = createEventState({ count: 0 });
|
|
32
|
+
|
|
33
|
+
// History tracking
|
|
34
|
+
let history = [0]; // Start with initial state
|
|
35
|
+
let historyIndex = 0;
|
|
36
|
+
let isTimeTravel = false; // Flag to prevent recording during undo/redo
|
|
37
|
+
|
|
38
|
+
// Update UI
|
|
39
|
+
const updateUI = () => {
|
|
40
|
+
const count = store.get('count');
|
|
41
|
+
document.getElementById('count').textContent = count;
|
|
42
|
+
|
|
43
|
+
// Update button states
|
|
44
|
+
document.getElementById('undo').disabled = historyIndex === 0;
|
|
45
|
+
document.getElementById('redo').disabled = historyIndex === history.length - 1;
|
|
46
|
+
|
|
47
|
+
// Update history info
|
|
48
|
+
document.getElementById('historyInfo').textContent =
|
|
49
|
+
`${historyIndex + 1} of ${history.length} states`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Subscribe to count changes and record history
|
|
53
|
+
store.subscribe('count', (value) => {
|
|
54
|
+
updateUI();
|
|
55
|
+
|
|
56
|
+
// Only record if not time traveling
|
|
57
|
+
if (!isTimeTravel) {
|
|
58
|
+
// Remove any "future" states if we're in the middle of history
|
|
59
|
+
history = history.slice(0, historyIndex + 1);
|
|
60
|
+
|
|
61
|
+
// Add new state
|
|
62
|
+
history.push(value);
|
|
63
|
+
historyIndex = history.length - 1;
|
|
64
|
+
|
|
65
|
+
updateUI();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Counter operations
|
|
70
|
+
document.getElementById('increment').onclick = () => {
|
|
71
|
+
store.set('count', store.get('count') + 1);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
document.getElementById('decrement').onclick = () => {
|
|
75
|
+
store.set('count', store.get('count') - 1);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
document.getElementById('double').onclick = () => {
|
|
79
|
+
store.set('count', store.get('count') * 2);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Undo
|
|
83
|
+
document.getElementById('undo').onclick = () => {
|
|
84
|
+
if (historyIndex > 0) {
|
|
85
|
+
historyIndex--;
|
|
86
|
+
isTimeTravel = true;
|
|
87
|
+
store.set('count', history[historyIndex]);
|
|
88
|
+
isTimeTravel = false;
|
|
89
|
+
updateUI();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Redo
|
|
94
|
+
document.getElementById('redo').onclick = () => {
|
|
95
|
+
if (historyIndex < history.length - 1) {
|
|
96
|
+
historyIndex++;
|
|
97
|
+
isTimeTravel = true;
|
|
98
|
+
store.set('count', history[historyIndex]);
|
|
99
|
+
isTimeTravel = false;
|
|
100
|
+
updateUI();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Initial UI update
|
|
105
|
+
updateUI();
|
|
106
|
+
</script>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# 009 localStorage Side Effects - Persistent State
|
|
2
|
+
|
|
3
|
+
Demonstrates how to sync state with localStorage for persistence across page reloads.
|
|
4
|
+
|
|
5
|
+
## What's Here
|
|
6
|
+
|
|
7
|
+
- Counter that persists across browser sessions
|
|
8
|
+
- Automatic save on every state change
|
|
9
|
+
- Load saved state on page load
|
|
10
|
+
- **No persistence library needed** - just localStorage API
|
|
11
|
+
|
|
12
|
+
## How It Works
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
// 1. Load initial state from localStorage
|
|
16
|
+
const savedCount = localStorage.getItem('counter');
|
|
17
|
+
const initialCount = savedCount !== null ? parseInt(savedCount, 10) : 0;
|
|
18
|
+
|
|
19
|
+
// 2. Create store with saved state
|
|
20
|
+
const store = createEventState({ count: initialCount });
|
|
21
|
+
|
|
22
|
+
// 3. Side effect: save on every change
|
|
23
|
+
store.subscribe('count', (value) => {
|
|
24
|
+
localStorage.setItem('counter', value);
|
|
25
|
+
// Also update UI
|
|
26
|
+
document.getElementById('count').textContent = value;
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Key Insight
|
|
31
|
+
|
|
32
|
+
**Side effects are just subscribers.**
|
|
33
|
+
|
|
34
|
+
Subscriptions aren't only for UI updates. They're for:
|
|
35
|
+
- ✅ Saving to localStorage
|
|
36
|
+
- ✅ Logging to analytics
|
|
37
|
+
- ✅ Syncing to server
|
|
38
|
+
- ✅ Triggering other actions
|
|
39
|
+
|
|
40
|
+
Other frameworks need:
|
|
41
|
+
- React: `useEffect` with dependency arrays
|
|
42
|
+
- Vue: `watch` or `watchEffect`
|
|
43
|
+
- Svelte: Reactive statements with side effects
|
|
44
|
+
|
|
45
|
+
**EventState:** Just subscribe and do whatever you want. That's it.
|
|
46
|
+
|
|
47
|
+
## Pattern: Separation of Concerns
|
|
48
|
+
|
|
49
|
+
Notice the subscriber does TWO things:
|
|
50
|
+
1. Saves to localStorage (side effect)
|
|
51
|
+
2. Updates the DOM (UI effect)
|
|
52
|
+
|
|
53
|
+
You could split these into separate subscribers:
|
|
54
|
+
```javascript
|
|
55
|
+
store.subscribe('count', (value) => {
|
|
56
|
+
localStorage.setItem('counter', value);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
store.subscribe('count', (value) => {
|
|
60
|
+
document.getElementById('count').textContent = value;
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is the **single responsibility principle** in action.
|
|
65
|
+
|
|
66
|
+
## Run It
|
|
67
|
+
|
|
68
|
+
Open `index.html`, increment the counter, then reload the page. Your count persists!
|
|
69
|
+
|
|
70
|
+
## Try This
|
|
71
|
+
|
|
72
|
+
Open DevTools → Application → Local Storage to see the saved value update in real-time.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>009 localStorage Side Effects - EventState</title>
|
|
7
|
+
<script type="importmap">
|
|
8
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
9
|
+
</script>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<h1>Persistent Counter</h1>
|
|
13
|
+
|
|
14
|
+
<h2>Count: <span id="count">0</span></h2>
|
|
15
|
+
|
|
16
|
+
<button id="increment">+1</button>
|
|
17
|
+
<button id="decrement">-1</button>
|
|
18
|
+
<button id="reset">Reset</button>
|
|
19
|
+
|
|
20
|
+
<p><small>Reload the page - your count persists!</small></p>
|
|
21
|
+
|
|
22
|
+
<script type="module">
|
|
23
|
+
import { createEventState } from '@uistate/core';
|
|
24
|
+
|
|
25
|
+
const STORAGE_KEY = 'eventstate-counter';
|
|
26
|
+
|
|
27
|
+
// Load initial state from localStorage
|
|
28
|
+
const savedCount = localStorage.getItem(STORAGE_KEY);
|
|
29
|
+
const initialCount = savedCount !== null ? parseInt(savedCount, 10) : 0;
|
|
30
|
+
|
|
31
|
+
// Create store with saved state
|
|
32
|
+
const store = createEventState({ count: initialCount });
|
|
33
|
+
|
|
34
|
+
// Side effect: save to localStorage on every change
|
|
35
|
+
store.subscribe('count', (value) => {
|
|
36
|
+
localStorage.setItem(STORAGE_KEY, value);
|
|
37
|
+
document.getElementById('count').textContent = value;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Operations
|
|
41
|
+
document.getElementById('increment').onclick = () => {
|
|
42
|
+
store.set('count', store.get('count') + 1);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
document.getElementById('decrement').onclick = () => {
|
|
46
|
+
store.set('count', store.get('count') - 1);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
document.getElementById('reset').onclick = () => {
|
|
50
|
+
store.set('count', 0);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Initial render
|
|
54
|
+
document.getElementById('count').textContent = initialCount;
|
|
55
|
+
</script>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# 010 Decoupled Components - Shared State
|
|
2
|
+
|
|
3
|
+
Demonstrates how components can communicate through shared state without direct coupling.
|
|
4
|
+
|
|
5
|
+
## What's Here
|
|
6
|
+
|
|
7
|
+
- **Component A (Writer):** Sends messages
|
|
8
|
+
- **Component B (Reader):** Displays current message
|
|
9
|
+
- **Component C (Logger):** Logs all messages with timestamps
|
|
10
|
+
- **Zero coupling:** Components don't know about each other
|
|
11
|
+
|
|
12
|
+
## How It Works
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
// Shared store - the ONLY connection
|
|
16
|
+
const store = createEventState({ message: '' });
|
|
17
|
+
|
|
18
|
+
// Component A: Writes to state
|
|
19
|
+
document.getElementById('sendBtn').onclick = () => {
|
|
20
|
+
store.set('message', inputValue);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Component B: Reads from state
|
|
24
|
+
store.subscribe('message', (value) => {
|
|
25
|
+
document.getElementById('display').textContent = value;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Component C: Also reads from state
|
|
29
|
+
store.subscribe('message', (value) => {
|
|
30
|
+
logMessage(value);
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Key Insight
|
|
35
|
+
|
|
36
|
+
**State is the interface between components.**
|
|
37
|
+
|
|
38
|
+
Components communicate by:
|
|
39
|
+
1. Writing to shared state
|
|
40
|
+
2. Subscribing to shared state
|
|
41
|
+
3. **Never** calling each other directly
|
|
42
|
+
|
|
43
|
+
This is the **pub/sub pattern** in action:
|
|
44
|
+
- ✅ Components are decoupled
|
|
45
|
+
- ✅ Easy to add/remove components
|
|
46
|
+
- ✅ No prop drilling
|
|
47
|
+
- ✅ No callbacks passed down
|
|
48
|
+
|
|
49
|
+
Other frameworks need:
|
|
50
|
+
- React: Context API, prop drilling, or state management libraries
|
|
51
|
+
- Vue: Provide/inject or Vuex
|
|
52
|
+
- Svelte: Context API or stores
|
|
53
|
+
|
|
54
|
+
**EventState:** Just share the store. That's it.
|
|
55
|
+
|
|
56
|
+
## Architecture Pattern
|
|
57
|
+
|
|
58
|
+
This demonstrates the **mediator pattern**:
|
|
59
|
+
- Components don't talk to each other
|
|
60
|
+
- They talk to the store
|
|
61
|
+
- The store mediates all communication
|
|
62
|
+
|
|
63
|
+
This is how your full UIstate framework works:
|
|
64
|
+
- Intents → State changes
|
|
65
|
+
- State changes → UI updates
|
|
66
|
+
- Everything is decoupled through events
|
|
67
|
+
|
|
68
|
+
## Run It
|
|
69
|
+
|
|
70
|
+
Open `index.html`, type messages, and watch all three components react independently.
|
|
71
|
+
|
|
72
|
+
## Try This
|
|
73
|
+
|
|
74
|
+
Add a fourth component that counts vowels in messages. It only needs to subscribe to `'message'` - no other code changes needed.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>010 Decoupled Components - EventState</title>
|
|
7
|
+
<style>
|
|
8
|
+
.component {
|
|
9
|
+
border: 2px solid #333;
|
|
10
|
+
padding: 20px;
|
|
11
|
+
margin: 10px 0;
|
|
12
|
+
}
|
|
13
|
+
</style>
|
|
14
|
+
<script type="importmap">
|
|
15
|
+
{ "imports": { "@uistate/core": "../../index.js" } }
|
|
16
|
+
</script>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<h1>Decoupled Components</h1>
|
|
20
|
+
<p>Two independent "components" sharing state without knowing about each other.</p>
|
|
21
|
+
|
|
22
|
+
<!-- Component A: Writer -->
|
|
23
|
+
<div class="component">
|
|
24
|
+
<h2>Component A: Writer</h2>
|
|
25
|
+
<input type="text" id="messageInput" placeholder="Type a message...">
|
|
26
|
+
<button id="sendBtn">Send</button>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- Component B: Reader -->
|
|
30
|
+
<div class="component">
|
|
31
|
+
<h2>Component B: Reader</h2>
|
|
32
|
+
<p><strong>Received:</strong> <span id="messageDisplay">(nothing yet)</span></p>
|
|
33
|
+
<p><small>Message count: <span id="messageCount">0</span></small></p>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- Component C: Logger -->
|
|
37
|
+
<div class="component">
|
|
38
|
+
<h2>Component C: Logger</h2>
|
|
39
|
+
<ul id="messageLog"></ul>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<script type="module">
|
|
43
|
+
import { createEventState } from '@uistate/core';
|
|
44
|
+
|
|
45
|
+
// Shared store - the ONLY connection between components
|
|
46
|
+
const store = createEventState({
|
|
47
|
+
message: '',
|
|
48
|
+
messageCount: 0
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ===== COMPONENT A: Writer =====
|
|
52
|
+
// Knows nothing about B or C, just writes to state
|
|
53
|
+
document.getElementById('sendBtn').onclick = () => {
|
|
54
|
+
const input = document.getElementById('messageInput');
|
|
55
|
+
const value = input.value.trim();
|
|
56
|
+
|
|
57
|
+
if (value) {
|
|
58
|
+
store.set('message', value);
|
|
59
|
+
store.set('messageCount', store.get('messageCount') + 1);
|
|
60
|
+
input.value = '';
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
document.getElementById('messageInput').onkeypress = (e) => {
|
|
65
|
+
if (e.key === 'Enter') {
|
|
66
|
+
document.getElementById('sendBtn').click();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ===== COMPONENT B: Reader =====
|
|
71
|
+
// Knows nothing about A or C, just reads from state
|
|
72
|
+
store.subscribe('message', (value) => {
|
|
73
|
+
document.getElementById('messageDisplay').textContent = value || '(nothing yet)';
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
store.subscribe('messageCount', (value) => {
|
|
77
|
+
document.getElementById('messageCount').textContent = value;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ===== COMPONENT C: Logger =====
|
|
81
|
+
// Knows nothing about A or B, just logs all messages
|
|
82
|
+
store.subscribe('message', (value) => {
|
|
83
|
+
if (value) {
|
|
84
|
+
const log = document.getElementById('messageLog');
|
|
85
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
86
|
+
const li = document.createElement('li');
|
|
87
|
+
li.textContent = `[${timestamp}] ${value}`;
|
|
88
|
+
log.appendChild(li);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
</script>
|
|
92
|
+
</body>
|
|
93
|
+
</html>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# 011 Async Patterns - Debouncing & Loading States
|
|
2
|
+
|
|
3
|
+
Demonstrates how to handle async operations with EventState.
|
|
4
|
+
|
|
5
|
+
## What's Here
|
|
6
|
+
|
|
7
|
+
- **Debounced search:** Wait for user to stop typing before searching
|
|
8
|
+
- **Loading states:** Show loading indicator during async operations
|
|
9
|
+
- **Error handling:** Graceful failure with try/catch
|
|
10
|
+
- **No async library needed** - just Promises and state
|
|
11
|
+
|
|
12
|
+
## How It Works
|
|
13
|
+
|
|
14
|
+
### Debounced Search
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
let searchTimeout = null;
|
|
18
|
+
|
|
19
|
+
store.subscribe('searchQuery', async (value) => {
|
|
20
|
+
clearTimeout(searchTimeout);
|
|
21
|
+
|
|
22
|
+
store.set('isSearching', true);
|
|
23
|
+
|
|
24
|
+
// Wait 300ms before searching
|
|
25
|
+
searchTimeout = setTimeout(async () => {
|
|
26
|
+
const result = await performSearch(value);
|
|
27
|
+
store.set('searchResult', result);
|
|
28
|
+
store.set('isSearching', false);
|
|
29
|
+
}, 300);
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Loading States
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
document.getElementById('fetchBtn').onclick = async () => {
|
|
37
|
+
store.set('isLoading', true);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const data = await fetchData();
|
|
41
|
+
store.set('apiData', data);
|
|
42
|
+
} finally {
|
|
43
|
+
store.set('isLoading', false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Key Insight
|
|
49
|
+
|
|
50
|
+
**Async is just state changes over time.**
|
|
51
|
+
|
|
52
|
+
Pattern for any async operation:
|
|
53
|
+
1. Set loading state to `true`
|
|
54
|
+
2. Perform async operation
|
|
55
|
+
3. Set result state
|
|
56
|
+
4. Set loading state to `false`
|
|
57
|
+
|
|
58
|
+
Other frameworks need:
|
|
59
|
+
- React: `useState` + `useEffect` + cleanup functions
|
|
60
|
+
- Vue: `ref` + `watch` + async watchers
|
|
61
|
+
- Svelte: Reactive statements + `#await` blocks
|
|
62
|
+
|
|
63
|
+
**EventState:** Just use async/await in subscribers. That's it.
|
|
64
|
+
|
|
65
|
+
## Patterns Demonstrated
|
|
66
|
+
|
|
67
|
+
### 1. Debouncing
|
|
68
|
+
Wait for user to stop typing before triggering expensive operations.
|
|
69
|
+
|
|
70
|
+
### 2. Loading Indicators
|
|
71
|
+
Show feedback during async operations to improve UX.
|
|
72
|
+
|
|
73
|
+
### 3. Disabling Actions
|
|
74
|
+
Prevent duplicate requests by disabling buttons during loading.
|
|
75
|
+
|
|
76
|
+
### 4. Cleanup
|
|
77
|
+
Clear timeouts to prevent race conditions.
|
|
78
|
+
|
|
79
|
+
## Run It
|
|
80
|
+
|
|
81
|
+
Open `index.html`:
|
|
82
|
+
- Type in the search box (notice the 300ms delay)
|
|
83
|
+
- Click "Fetch Data" (notice the loading state)
|
|
84
|
+
|
|
85
|
+
## Try This
|
|
86
|
+
|
|
87
|
+
Open DevTools Network tab and throttle to "Slow 3G" to see loading states more clearly.
|
|
88
|
+
|
|
89
|
+
## Real-World Usage
|
|
90
|
+
|
|
91
|
+
This pattern works for:
|
|
92
|
+
- ✅ API calls
|
|
93
|
+
- ✅ Autocomplete
|
|
94
|
+
- ✅ Form submission
|
|
95
|
+
- ✅ File uploads
|
|
96
|
+
- ✅ Any async operation
|
|
97
|
+
|
|
98
|
+
Just manage loading/error/success states in the store.
|