@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,271 @@
|
|
|
1
|
+
// runtime/core/router.js — Generic SPA router factory for eventState stores
|
|
2
|
+
// Usage:
|
|
3
|
+
// const router = createRouter({
|
|
4
|
+
// routes: [{ path: '/', view: 'home', component: HomeView }],
|
|
5
|
+
// store,
|
|
6
|
+
// rootSelector: '[data-route-root]',
|
|
7
|
+
// debug: true
|
|
8
|
+
// });
|
|
9
|
+
// router.start();
|
|
10
|
+
|
|
11
|
+
export function createRouter(config) {
|
|
12
|
+
const {
|
|
13
|
+
routes = [],
|
|
14
|
+
store,
|
|
15
|
+
rootSelector = '[data-route-root]',
|
|
16
|
+
fallback = null,
|
|
17
|
+
debug = false,
|
|
18
|
+
linkSelector = 'a[data-link]',
|
|
19
|
+
navSelector = 'nav a[data-link]',
|
|
20
|
+
} = config;
|
|
21
|
+
|
|
22
|
+
// Detect base path from <base href> if present
|
|
23
|
+
const BASE_PATH = (() => {
|
|
24
|
+
const b = document.querySelector('base[href]');
|
|
25
|
+
if (!b) return '';
|
|
26
|
+
try {
|
|
27
|
+
const u = new URL(b.getAttribute('href'), location.href);
|
|
28
|
+
let p = u.pathname;
|
|
29
|
+
if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
|
|
30
|
+
return p;
|
|
31
|
+
} catch { return ''; }
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
function stripBase(pathname) {
|
|
35
|
+
if (BASE_PATH && pathname.startsWith(BASE_PATH)) {
|
|
36
|
+
const rest = pathname.slice(BASE_PATH.length) || '/';
|
|
37
|
+
return rest.startsWith('/') ? rest : ('/' + rest);
|
|
38
|
+
}
|
|
39
|
+
return pathname;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function withBase(pathname) {
|
|
43
|
+
if (!BASE_PATH) return pathname;
|
|
44
|
+
if (pathname === '/') return BASE_PATH || '/';
|
|
45
|
+
return (BASE_PATH + (pathname.startsWith('/') ? '' : '/') + pathname);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizePath(p) {
|
|
49
|
+
if (!p) return '/';
|
|
50
|
+
try {
|
|
51
|
+
if (p[0] !== '/') p = '/' + p;
|
|
52
|
+
if (p === '/index.html') return '/';
|
|
53
|
+
if (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1);
|
|
54
|
+
return p;
|
|
55
|
+
} catch { return '/'; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolve(pathname) {
|
|
59
|
+
const p = normalizePath(pathname);
|
|
60
|
+
const r = routes.find(r => r.path === p);
|
|
61
|
+
if (r) return { ...r, params: {} };
|
|
62
|
+
if (fallback) return { ...fallback, params: {} };
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getRoot() {
|
|
67
|
+
const el = document.querySelector(rootSelector);
|
|
68
|
+
if (!el) throw new Error('Route root not found: ' + rootSelector);
|
|
69
|
+
return el;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function setActiveNav(pathname) {
|
|
73
|
+
document.querySelectorAll(navSelector).forEach(a => {
|
|
74
|
+
const url = new URL(a.getAttribute('href'), location.href);
|
|
75
|
+
const linkPath = normalizePath(stripBase(url.pathname));
|
|
76
|
+
const here = normalizePath(pathname);
|
|
77
|
+
const isExact = linkPath === here;
|
|
78
|
+
const isParent = !isExact && linkPath !== '/' && here.startsWith(linkPath);
|
|
79
|
+
const active = isExact || isParent;
|
|
80
|
+
a.classList.toggle('active', active);
|
|
81
|
+
if (isExact) a.setAttribute('aria-current', 'page');
|
|
82
|
+
else a.removeAttribute('aria-current');
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// State
|
|
87
|
+
let current = { viewKey: null, unboot: null, path: null, search: '' };
|
|
88
|
+
let navController = null;
|
|
89
|
+
const scrollPositions = new Map();
|
|
90
|
+
history.scrollRestoration = 'manual';
|
|
91
|
+
|
|
92
|
+
// Core navigate function
|
|
93
|
+
async function navigate(pathname, { replace = false, search = '', restoreScroll = false } = {}) {
|
|
94
|
+
const root = getRoot();
|
|
95
|
+
const appPath = normalizePath(stripBase(pathname));
|
|
96
|
+
const resolved = resolve(appPath);
|
|
97
|
+
|
|
98
|
+
if (!resolved) {
|
|
99
|
+
if (debug) console.warn('[router] No route found for:', appPath);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const viewKey = resolved.view;
|
|
104
|
+
const component = resolved.component;
|
|
105
|
+
const html = document.documentElement;
|
|
106
|
+
const prevViewKey = current.viewKey;
|
|
107
|
+
|
|
108
|
+
const searchStr = search && search.startsWith('?') ? search : (search ? ('?' + search) : '');
|
|
109
|
+
|
|
110
|
+
// Always log navigation for telemetry
|
|
111
|
+
console.log('[nav] navigate', { from: current.path, to: appPath, view: viewKey });
|
|
112
|
+
|
|
113
|
+
if (debug) {
|
|
114
|
+
console.debug('[router] navigate', { pathname, appPath, searchStr, view: viewKey, params: resolved.params });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Same-route no-op guard
|
|
118
|
+
if (current.path === appPath && current.search === searchStr) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Abort in-flight boot
|
|
123
|
+
if (navController) navController.abort();
|
|
124
|
+
navController = new AbortController();
|
|
125
|
+
const { signal } = navController;
|
|
126
|
+
|
|
127
|
+
// Transition start
|
|
128
|
+
if (store) {
|
|
129
|
+
try { store.set('ui.route.transitioning', true); } catch {}
|
|
130
|
+
}
|
|
131
|
+
html.setAttribute('data-transitioning', 'on');
|
|
132
|
+
|
|
133
|
+
// Save scroll position
|
|
134
|
+
if (current.path) {
|
|
135
|
+
scrollPositions.set(current.path, { x: scrollX, y: scrollY });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Unboot previous view
|
|
139
|
+
if (typeof current.unboot === 'function') {
|
|
140
|
+
try { await current.unboot(); } catch (_e) {}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Clear DOM
|
|
144
|
+
root.replaceChildren();
|
|
145
|
+
|
|
146
|
+
// Boot new view
|
|
147
|
+
const unboot = await (component.boot?.({ store, el: root, signal }) || Promise.resolve(() => {}));
|
|
148
|
+
current = { viewKey, unboot, path: appPath, search: searchStr };
|
|
149
|
+
|
|
150
|
+
// Update nav active state
|
|
151
|
+
setActiveNav(appPath);
|
|
152
|
+
|
|
153
|
+
// Parse query params
|
|
154
|
+
const urlForQuery = new URL(location.origin + withBase(appPath) + searchStr);
|
|
155
|
+
const q = {};
|
|
156
|
+
urlForQuery.searchParams.forEach((v, k) => { q[k] = v; });
|
|
157
|
+
|
|
158
|
+
// Update store
|
|
159
|
+
if (store) {
|
|
160
|
+
try {
|
|
161
|
+
if (store.setMany) {
|
|
162
|
+
store.setMany({
|
|
163
|
+
'ui.route.view': viewKey,
|
|
164
|
+
'ui.route.path': appPath,
|
|
165
|
+
'ui.route.params': resolved.params || {},
|
|
166
|
+
'ui.route.query': q,
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
store.set('ui.route.view', viewKey);
|
|
170
|
+
store.set('ui.route.path', appPath);
|
|
171
|
+
store.set('ui.route.params', resolved.params || {});
|
|
172
|
+
store.set('ui.route.query', q);
|
|
173
|
+
}
|
|
174
|
+
} catch {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Update browser history
|
|
178
|
+
const useReplace = replace || (prevViewKey === viewKey);
|
|
179
|
+
if (useReplace) history.replaceState({}, '', withBase(appPath) + searchStr);
|
|
180
|
+
else history.pushState({}, '', withBase(appPath) + searchStr);
|
|
181
|
+
|
|
182
|
+
// Set view attribute
|
|
183
|
+
html.setAttribute('data-view', viewKey);
|
|
184
|
+
|
|
185
|
+
if (debug) {
|
|
186
|
+
console.debug('[router] view', { viewKey, path: appPath, query: q });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Transition end
|
|
190
|
+
if (store) {
|
|
191
|
+
try { store.set('ui.route.transitioning', false); } catch {}
|
|
192
|
+
}
|
|
193
|
+
html.setAttribute('data-transitioning', 'off');
|
|
194
|
+
|
|
195
|
+
// Focus management
|
|
196
|
+
if (!root.hasAttribute('tabindex')) root.setAttribute('tabindex', '-1');
|
|
197
|
+
try { root.focus({ preventScroll: true }); } catch {}
|
|
198
|
+
|
|
199
|
+
// Scroll restoration
|
|
200
|
+
if (restoreScroll) {
|
|
201
|
+
const pos = scrollPositions.get(appPath);
|
|
202
|
+
if (pos) scrollTo(pos.x, pos.y);
|
|
203
|
+
} else {
|
|
204
|
+
scrollTo(0, 0);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function navigateQuery(patch = {}, { replace = true } = {}) {
|
|
209
|
+
const params = new URLSearchParams(current.search || '');
|
|
210
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
211
|
+
if (v === null || v === undefined || v === '') params.delete(k);
|
|
212
|
+
else params.set(k, String(v));
|
|
213
|
+
}
|
|
214
|
+
const searchStr = params.toString();
|
|
215
|
+
const prefixed = searchStr ? ('?' + searchStr) : '';
|
|
216
|
+
const path = current.path || normalizePath(stripBase(location.pathname));
|
|
217
|
+
return navigate(path, { search: prefixed, replace });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function navigatePath(path, { replace = true } = {}) {
|
|
221
|
+
const appPath = normalizePath(stripBase(path));
|
|
222
|
+
const searchStr = current.search || '';
|
|
223
|
+
return navigate(appPath, { search: searchStr, replace });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function onClick(e) {
|
|
227
|
+
const a = e.target.closest(linkSelector);
|
|
228
|
+
if (!a) return;
|
|
229
|
+
if (e.defaultPrevented) return;
|
|
230
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
|
|
231
|
+
const url = new URL(a.getAttribute('href'), location.href);
|
|
232
|
+
if (url.origin !== location.origin) return;
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
|
|
235
|
+
// Log navigation click
|
|
236
|
+
console.log('[nav] click', { href: a.getAttribute('href'), text: a.textContent.trim() });
|
|
237
|
+
|
|
238
|
+
navigate(url.pathname, { search: url.search }).catch(() => {});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function onPop() {
|
|
242
|
+
navigate(location.pathname, { replace: true, search: location.search, restoreScroll: true }).catch(() => {});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Public API
|
|
246
|
+
return {
|
|
247
|
+
navigate,
|
|
248
|
+
navigateQuery,
|
|
249
|
+
navigatePath,
|
|
250
|
+
|
|
251
|
+
start() {
|
|
252
|
+
window.addEventListener('click', onClick);
|
|
253
|
+
window.addEventListener('popstate', onPop);
|
|
254
|
+
navigate(location.pathname, { replace: true, search: location.search, restoreScroll: true });
|
|
255
|
+
return this;
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
stop() {
|
|
259
|
+
window.removeEventListener('click', onClick);
|
|
260
|
+
window.removeEventListener('popstate', onPop);
|
|
261
|
+
if (typeof current.unboot === 'function') {
|
|
262
|
+
try { current.unboot(); } catch {}
|
|
263
|
+
}
|
|
264
|
+
return this;
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
getCurrent() {
|
|
268
|
+
return { ...current };
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
body {
|
|
2
|
+
font: 14px/1.4 system-ui, sans-serif;
|
|
3
|
+
margin: 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/* Hide focus outline on route container (set programmatically for a11y) */
|
|
7
|
+
[data-route-root]:focus {
|
|
8
|
+
outline: none;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
nav {
|
|
12
|
+
padding: 10px 14px;
|
|
13
|
+
border-bottom: 1px solid #eee;
|
|
14
|
+
position: sticky;
|
|
15
|
+
top: 0;
|
|
16
|
+
background: #fff;
|
|
17
|
+
z-index: 10;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
nav a {
|
|
21
|
+
margin-right: 12px;
|
|
22
|
+
text-decoration: none;
|
|
23
|
+
color: #0366d6;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
nav a.active {
|
|
27
|
+
font-weight: 600;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
main {
|
|
31
|
+
padding: 16px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.loading-badge {
|
|
35
|
+
margin-left: 8px;
|
|
36
|
+
font-size: 12px;
|
|
37
|
+
color: #666;
|
|
38
|
+
display: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
html[data-transitioning="on"] .loading-badge {
|
|
42
|
+
display: inline;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* 404 view demo: tint the top bar only on notfound */
|
|
46
|
+
html[data-view="notfound"] nav {
|
|
47
|
+
background: #fff7f7;
|
|
48
|
+
border-bottom-color: #f5c2c2;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
html[data-view="notfound"] nav a {
|
|
52
|
+
color: #b91c1c;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
html[data-view="notfound"] .loading-badge {
|
|
56
|
+
color: #b91c1c;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Tabs styling (used by views/demo.js) */
|
|
60
|
+
.tabs {
|
|
61
|
+
margin-top: 12px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.tab-bar {
|
|
65
|
+
display: flex;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
border-bottom: 1px solid #eee;
|
|
68
|
+
padding-bottom: 6px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.tab {
|
|
72
|
+
appearance: none;
|
|
73
|
+
border: 1px solid #ddd;
|
|
74
|
+
background: #fafafa;
|
|
75
|
+
border-radius: 6px;
|
|
76
|
+
padding: 6px 10px;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.tab:hover {
|
|
81
|
+
background: #f3f4f6;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.tab.active {
|
|
85
|
+
background: #e5efff;
|
|
86
|
+
border-color: #bcd1ff;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.tab-panels {
|
|
90
|
+
padding-top: 10px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.tab-panel {
|
|
94
|
+
display: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.tab-panel[data-active] {
|
|
98
|
+
display: block;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
:root {
|
|
102
|
+
--bg: #ffffff;
|
|
103
|
+
--fg: #111111;
|
|
104
|
+
--muted: #555;
|
|
105
|
+
--card: #f6f6f6;
|
|
106
|
+
--border: #e5e5e5;
|
|
107
|
+
--btn-bg: #f8f8f8;
|
|
108
|
+
--btn-fg: #111;
|
|
109
|
+
--btn-border: #ddd;
|
|
110
|
+
--input-bg: #fff;
|
|
111
|
+
--input-fg: #222;
|
|
112
|
+
--input-border: #d6d6d6;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
body[data-theme='dark'] {
|
|
116
|
+
--bg: #0f1115;
|
|
117
|
+
--fg: #cfd3dc;
|
|
118
|
+
--muted: #9aa3b2;
|
|
119
|
+
--card: #151923;
|
|
120
|
+
--border: #242a36;
|
|
121
|
+
--btn-bg: #1a2030;
|
|
122
|
+
--btn-fg: #cfd3dc;
|
|
123
|
+
--btn-border: #2b3242;
|
|
124
|
+
--input-bg: #1b2130;
|
|
125
|
+
--input-fg: #cfd3dc;
|
|
126
|
+
--input-border: #2b3242;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
body {
|
|
130
|
+
background: var(--bg);
|
|
131
|
+
color: var(--fg);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
a {
|
|
135
|
+
color: inherit;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
nav {
|
|
139
|
+
background: var(--card);
|
|
140
|
+
border-bottom: 1px solid var(--border);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.btn {
|
|
144
|
+
appearance: none;
|
|
145
|
+
border: 1px solid var(--btn-border);
|
|
146
|
+
background: var(--btn-bg);
|
|
147
|
+
color: var(--btn-fg);
|
|
148
|
+
border-radius: 6px;
|
|
149
|
+
padding: 6px 10px;
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
input,
|
|
154
|
+
select,
|
|
155
|
+
textarea {
|
|
156
|
+
background: var(--input-bg);
|
|
157
|
+
color: var(--input-fg);
|
|
158
|
+
border: 1px solid var(--input-border);
|
|
159
|
+
border-radius: 6px;
|
|
160
|
+
padding: 6px 8px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
input::placeholder,
|
|
164
|
+
textarea::placeholder {
|
|
165
|
+
color: color-mix(in srgb, var(--input-fg) 55%, transparent);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.btn:hover {
|
|
169
|
+
filter: brightness(1.05);
|
|
170
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# EventTest - Event-Sequence Testing
|
|
2
|
+
|
|
3
|
+
Event-driven TDD for EventState applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Event-sequence testing** - Test state changes through event flows
|
|
8
|
+
- ✅ **Type extraction** - Generate `.d.ts` files from test assertions
|
|
9
|
+
- ✅ **No DOM required** - Tests run in Node.js
|
|
10
|
+
- ✅ **Fluent API** - Chainable assertions
|
|
11
|
+
- ✅ **Type-safe** - Tests define types, not manual definitions
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Writing Tests
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
import { createEventTest, test } from './eventTest.js';
|
|
19
|
+
|
|
20
|
+
test('add todo', () => {
|
|
21
|
+
const t = createEventTest({
|
|
22
|
+
domain: { todos: { items: [] } }
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Trigger intent
|
|
26
|
+
t.trigger('intent.todo.add', { text: 'Buy milk' });
|
|
27
|
+
|
|
28
|
+
// Assert types (for .d.ts generation)
|
|
29
|
+
t.assertArrayOf('domain.todos.items', {
|
|
30
|
+
id: 'number',
|
|
31
|
+
text: 'string',
|
|
32
|
+
done: 'boolean'
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Assert values
|
|
36
|
+
t.assertArrayLength('domain.todos.items', 1);
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Running Tests
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Run all tests
|
|
44
|
+
node tests/run.js
|
|
45
|
+
|
|
46
|
+
# Or run specific test file
|
|
47
|
+
node tests/todos.test.js
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Generating Types
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Generate store.d.ts from test assertions
|
|
54
|
+
node tests/generateTypes.js
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
### `createEventTest(initialState)`
|
|
60
|
+
|
|
61
|
+
Creates a test instance with isolated store.
|
|
62
|
+
|
|
63
|
+
**Returns:** Test API object
|
|
64
|
+
|
|
65
|
+
### Test API
|
|
66
|
+
|
|
67
|
+
#### `.trigger(path, value)`
|
|
68
|
+
|
|
69
|
+
Set a value in the store (trigger state change).
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
t.trigger('intent.todo.add', { text: 'Buy milk' });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### `.assertPath(path, expected)`
|
|
76
|
+
|
|
77
|
+
Assert exact value at path.
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
t.assertPath('ui.theme', 'dark');
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `.assertType(path, type)`
|
|
84
|
+
|
|
85
|
+
Assert primitive type. Stores type info for `.d.ts` generation.
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
t.assertType('ui.theme', 'string');
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### `.assertArrayOf(path, elementShape)`
|
|
92
|
+
|
|
93
|
+
Assert array with element shape. Stores type info for `.d.ts` generation.
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
t.assertArrayOf('domain.todos.items', {
|
|
97
|
+
id: 'number',
|
|
98
|
+
text: 'string',
|
|
99
|
+
done: 'boolean'
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### `.assertShape(path, objectShape)`
|
|
104
|
+
|
|
105
|
+
Assert object shape. Stores type info for `.d.ts` generation.
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
t.assertShape('ui.route', {
|
|
109
|
+
path: 'string',
|
|
110
|
+
view: 'string'
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### `.assertArrayLength(path, length)`
|
|
115
|
+
|
|
116
|
+
Assert array length.
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
t.assertArrayLength('domain.todos.items', 3);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### `.assertEventFired(path, times)`
|
|
123
|
+
|
|
124
|
+
Assert event fired N times.
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
t.assertEventFired('domain.todos.items', 1);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### `.getEventLog()`
|
|
131
|
+
|
|
132
|
+
Get all captured events.
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
const log = t.getEventLog();
|
|
136
|
+
// [{ timestamp: 123, path: 'domain.todos.items', value: [...] }]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### `.getTypeAssertions()`
|
|
140
|
+
|
|
141
|
+
Get all type assertions (for type generation).
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
const assertions = t.getTypeAssertions();
|
|
145
|
+
// [{ path: 'domain.todos.items', type: 'array', elementShape: {...} }]
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Workflow
|
|
149
|
+
|
|
150
|
+
### 1. Write Tests (TDD)
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
// tests/todos.test.js
|
|
154
|
+
test('add todo', () => {
|
|
155
|
+
const t = createEventTest({ domain: { todos: { items: [] } } });
|
|
156
|
+
t.trigger('intent.todo.add', { text: 'Buy milk' });
|
|
157
|
+
t.assertArrayOf('domain.todos.items', {
|
|
158
|
+
id: 'number',
|
|
159
|
+
text: 'string',
|
|
160
|
+
done: 'boolean'
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 2. Implement Bridges
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
// app/bridges.js
|
|
169
|
+
store.subscribe('intent.todo.add', ({ text }) => {
|
|
170
|
+
const items = store.get('domain.todos.items') || [];
|
|
171
|
+
const id = items.length + 1;
|
|
172
|
+
store.set('domain.todos.items', [...items, { id, text, done: false }]);
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 3. Generate Types
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
node tests/generateTypes.js
|
|
180
|
+
# Creates store.d.ts with perfect types!
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 4. Build UI
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
// Now you have autocomplete!
|
|
187
|
+
const items = store.get('domain.todos.items');
|
|
188
|
+
// TypeScript knows: Array<{ id: number, text: string, done: boolean }>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Benefits
|
|
192
|
+
|
|
193
|
+
- **Tests define behavior** - TDD workflow
|
|
194
|
+
- **Types derived from tests** - No manual type definitions
|
|
195
|
+
- **Perfect coverage** - Every test adds type information
|
|
196
|
+
- **No DOM needed** - Fast feedback loop
|
|
197
|
+
- **Deterministic** - Same tests = same types
|
|
198
|
+
- **Watcher-friendly** - Auto-regenerate on test changes
|
|
199
|
+
|
|
200
|
+
## Philosophy
|
|
201
|
+
|
|
202
|
+
EventTest follows EventState's core principles:
|
|
203
|
+
|
|
204
|
+
1. **State-first** - Tests work with state paths
|
|
205
|
+
2. **Dot-paths everywhere** - No special syntax
|
|
206
|
+
3. **Types are optional** - Tests work without TypeScript
|
|
207
|
+
4. **Types are derived** - Not hand-written
|
|
208
|
+
5. **JS-first** - Clean, readable code
|