@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.
Files changed (137) hide show
  1. package/README.md +40 -0
  2. package/cssState/.gitkeep +0 -0
  3. package/eventState/001-counter/README.md +44 -0
  4. package/eventState/001-counter/index.html +33 -0
  5. package/eventState/002-counter-improved/README.md +44 -0
  6. package/eventState/002-counter-improved/index.html +47 -0
  7. package/eventState/003-input-reactive/README.md +44 -0
  8. package/eventState/003-input-reactive/index.html +33 -0
  9. package/eventState/004-computed-state/README.md +45 -0
  10. package/eventState/004-computed-state/index.html +65 -0
  11. package/eventState/005-conditional-rendering/README.md +42 -0
  12. package/eventState/005-conditional-rendering/index.html +39 -0
  13. package/eventState/006-list-rendering/README.md +49 -0
  14. package/eventState/006-list-rendering/index.html +63 -0
  15. package/eventState/007-form-validation/README.md +52 -0
  16. package/eventState/007-form-validation/index.html +102 -0
  17. package/eventState/008-undo-redo/README.md +70 -0
  18. package/eventState/008-undo-redo/index.html +108 -0
  19. package/eventState/009-localStorage-side-effects/README.md +72 -0
  20. package/eventState/009-localStorage-side-effects/index.html +57 -0
  21. package/eventState/010-decoupled-components/README.md +74 -0
  22. package/eventState/010-decoupled-components/index.html +93 -0
  23. package/eventState/011-async-patterns/README.md +98 -0
  24. package/eventState/011-async-patterns/index.html +132 -0
  25. package/eventState/028-counter-improved-eventTest/LICENSE +55 -0
  26. package/eventState/028-counter-improved-eventTest/README.md +131 -0
  27. package/eventState/028-counter-improved-eventTest/app/store.js +9 -0
  28. package/eventState/028-counter-improved-eventTest/index.html +49 -0
  29. package/eventState/028-counter-improved-eventTest/runtime/core/behaviors.runtime.js +282 -0
  30. package/eventState/028-counter-improved-eventTest/runtime/core/eventState.js +100 -0
  31. package/eventState/028-counter-improved-eventTest/runtime/core/eventStateNew.js +149 -0
  32. package/eventState/028-counter-improved-eventTest/runtime/core/helpers.js +212 -0
  33. package/eventState/028-counter-improved-eventTest/runtime/core/router.js +271 -0
  34. package/eventState/028-counter-improved-eventTest/store.d.ts +8 -0
  35. package/eventState/028-counter-improved-eventTest/style.css +170 -0
  36. package/eventState/028-counter-improved-eventTest/tests/README.md +208 -0
  37. package/eventState/028-counter-improved-eventTest/tests/counter.test.js +116 -0
  38. package/eventState/028-counter-improved-eventTest/tests/eventTest.js +176 -0
  39. package/eventState/028-counter-improved-eventTest/tests/generateTypes.js +168 -0
  40. package/eventState/028-counter-improved-eventTest/tests/run.js +20 -0
  41. package/eventState/030-todo-app-with-eventTest/LICENSE +55 -0
  42. package/eventState/030-todo-app-with-eventTest/README.md +121 -0
  43. package/eventState/030-todo-app-with-eventTest/app/router.js +25 -0
  44. package/eventState/030-todo-app-with-eventTest/app/store.js +16 -0
  45. package/eventState/030-todo-app-with-eventTest/app/views/home.js +11 -0
  46. package/eventState/030-todo-app-with-eventTest/app/views/todoDemo.js +88 -0
  47. package/eventState/030-todo-app-with-eventTest/index.html +65 -0
  48. package/eventState/030-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  49. package/eventState/030-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  50. package/eventState/030-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
  51. package/eventState/030-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  52. package/eventState/030-todo-app-with-eventTest/runtime/core/router.js +271 -0
  53. package/eventState/030-todo-app-with-eventTest/store.d.ts +18 -0
  54. package/eventState/030-todo-app-with-eventTest/style.css +170 -0
  55. package/eventState/030-todo-app-with-eventTest/tests/README.md +208 -0
  56. package/eventState/030-todo-app-with-eventTest/tests/eventTest.js +176 -0
  57. package/eventState/030-todo-app-with-eventTest/tests/generateTypes.js +189 -0
  58. package/eventState/030-todo-app-with-eventTest/tests/run.js +20 -0
  59. package/eventState/030-todo-app-with-eventTest/tests/todos.test.js +167 -0
  60. package/eventState/031-todo-app-with-eventTest/LICENSE +55 -0
  61. package/eventState/031-todo-app-with-eventTest/README.md +54 -0
  62. package/eventState/031-todo-app-with-eventTest/TUTORIAL.md +390 -0
  63. package/eventState/031-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
  64. package/eventState/031-todo-app-with-eventTest/app/bridges.js +113 -0
  65. package/eventState/031-todo-app-with-eventTest/app/router.js +26 -0
  66. package/eventState/031-todo-app-with-eventTest/app/store.js +15 -0
  67. package/eventState/031-todo-app-with-eventTest/app/views/home.js +46 -0
  68. package/eventState/031-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
  69. package/eventState/031-todo-app-with-eventTest/devtools/dock.js +41 -0
  70. package/eventState/031-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
  71. package/eventState/031-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
  72. package/eventState/031-todo-app-with-eventTest/devtools/telemetry.js +104 -0
  73. package/eventState/031-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
  74. package/eventState/031-todo-app-with-eventTest/index.html +103 -0
  75. package/eventState/031-todo-app-with-eventTest/package-lock.json +2184 -0
  76. package/eventState/031-todo-app-with-eventTest/package.json +24 -0
  77. package/eventState/031-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  78. package/eventState/031-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  79. package/eventState/031-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
  80. package/eventState/031-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  81. package/eventState/031-todo-app-with-eventTest/runtime/core/router.js +271 -0
  82. package/eventState/031-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
  83. package/eventState/031-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
  84. package/eventState/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
  85. package/eventState/031-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
  86. package/eventState/031-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
  87. package/eventState/031-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
  88. package/eventState/031-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
  89. package/eventState/031-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
  90. package/eventState/031-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
  91. package/eventState/031-todo-app-with-eventTest/store.d.ts +23 -0
  92. package/eventState/031-todo-app-with-eventTest/style.css +170 -0
  93. package/eventState/031-todo-app-with-eventTest/tests/README.md +208 -0
  94. package/eventState/031-todo-app-with-eventTest/tests/eventTest.js +176 -0
  95. package/eventState/031-todo-app-with-eventTest/tests/generateTypes.js +191 -0
  96. package/eventState/031-todo-app-with-eventTest/tests/run.js +20 -0
  97. package/eventState/031-todo-app-with-eventTest/tests/todos.test.js +192 -0
  98. package/eventState/032-todo-app-with-eventTest/LICENSE +55 -0
  99. package/eventState/032-todo-app-with-eventTest/README.md +54 -0
  100. package/eventState/032-todo-app-with-eventTest/TUTORIAL.md +390 -0
  101. package/eventState/032-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
  102. package/eventState/032-todo-app-with-eventTest/app/actions/index.js +153 -0
  103. package/eventState/032-todo-app-with-eventTest/app/bridges.js +113 -0
  104. package/eventState/032-todo-app-with-eventTest/app/router.js +26 -0
  105. package/eventState/032-todo-app-with-eventTest/app/store.js +15 -0
  106. package/eventState/032-todo-app-with-eventTest/app/views/home.js +46 -0
  107. package/eventState/032-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
  108. package/eventState/032-todo-app-with-eventTest/devtools/dock.js +41 -0
  109. package/eventState/032-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
  110. package/eventState/032-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
  111. package/eventState/032-todo-app-with-eventTest/devtools/telemetry.js +104 -0
  112. package/eventState/032-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
  113. package/eventState/032-todo-app-with-eventTest/index.html +87 -0
  114. package/eventState/032-todo-app-with-eventTest/package-lock.json +2184 -0
  115. package/eventState/032-todo-app-with-eventTest/package.json +24 -0
  116. package/eventState/032-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
  117. package/eventState/032-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
  118. package/eventState/032-todo-app-with-eventTest/runtime/core/eventStateNew.js +149 -0
  119. package/eventState/032-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
  120. package/eventState/032-todo-app-with-eventTest/runtime/core/router.js +271 -0
  121. package/eventState/032-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
  122. package/eventState/032-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
  123. package/eventState/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
  124. package/eventState/032-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
  125. package/eventState/032-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
  126. package/eventState/032-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
  127. package/eventState/032-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
  128. package/eventState/032-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
  129. package/eventState/032-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
  130. package/eventState/032-todo-app-with-eventTest/store.d.ts +23 -0
  131. package/eventState/032-todo-app-with-eventTest/style.css +170 -0
  132. package/eventState/032-todo-app-with-eventTest/tests/README.md +208 -0
  133. package/eventState/032-todo-app-with-eventTest/tests/eventTest.js +176 -0
  134. package/eventState/032-todo-app-with-eventTest/tests/generateTypes.js +191 -0
  135. package/eventState/032-todo-app-with-eventTest/tests/run.js +20 -0
  136. package/eventState/032-todo-app-with-eventTest/tests/todos.test.js +192 -0
  137. package/package.json +27 -0
@@ -0,0 +1,16 @@
1
+ // store.js — singleton eventState store for the SPA
2
+ import { createEventState } from '../runtime/core/eventStateNew.js';
3
+
4
+ const initial = {
5
+ ui: {
6
+ route: { path: '/', view: 'home', params: {}, query: {}, transitioning: false },
7
+ theme: 'light'
8
+ },
9
+ todos: {
10
+ items: [],
11
+ filter: 'all'
12
+ }
13
+ };
14
+
15
+ const store = createEventState(initial);
16
+ export default store;
@@ -0,0 +1,11 @@
1
+ // views/home.js — Home route
2
+ export async function boot({ store, el, signal }){
3
+ const container = document.createElement('main');
4
+ container.style.cssText = 'max-width: 600px; margin: 2rem auto; padding: 0 1rem;';
5
+ container.innerHTML = `
6
+ <h1>Home works!</h1>
7
+ <p>This is the home page of the todo app example.</p>
8
+ <p><a href="/todo-demo" data-link>Go to Todo App Demo →</a></p>
9
+ `;
10
+ el.appendChild(container);
11
+ }
@@ -0,0 +1,88 @@
1
+ // views/todoDemo.js — Minimal Todo app demo (imperative renderer, no bootstrap)
2
+ export async function boot({ store, el, signal }){
3
+ const main = document.createElement('main');
4
+ main.innerHTML = `
5
+ <h1>Todo app demo</h1>
6
+ <section class="beh-section">
7
+ <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap">
8
+ <input id="newTodo" placeholder="New todo" />
9
+ <button id="addTodo" class="btn">Add</button>
10
+ <div style="display:flex; gap:6px; align-items:center">
11
+ <button id="fAll" class="btn">All</button>
12
+ <button id="fActive" class="btn">Active</button>
13
+ <button id="fCompleted" class="btn">Completed</button>
14
+ <button id="clearCompleted" class="btn" style="background:#b91c1c; color:white">Clear Completed</button>
15
+ </div>
16
+ <span id="filterBadge" style="padding:2px 6px; border:1px solid #888; border-radius:8px; font-size:.85em; color:#555">filter: all</span>
17
+ </div>
18
+ <ul id="todos" style="margin-top:10px; padding-left: 18px;"></ul>
19
+ </section>
20
+ `;
21
+ el.appendChild(main);
22
+
23
+ const input = main.querySelector('#newTodo');
24
+ const ul = main.querySelector('#todos');
25
+ const btnAdd = main.querySelector('#addTodo');
26
+ const btnAll = main.querySelector('#fAll');
27
+ const btnAct = main.querySelector('#fActive');
28
+ const btnDone = main.querySelector('#fCompleted');
29
+ const btnClear = main.querySelector('#clearCompleted');
30
+
31
+ // Direct state manipulation (no intent pattern)
32
+ const itemsPath = 'todos.items';
33
+ const filterPath = 'todos.filter';
34
+
35
+ // Add todo
36
+ btnAdd?.addEventListener('click', () => {
37
+ const text = (input?.value || '').trim();
38
+ if (!text) return;
39
+ const items = store.get(itemsPath) || [];
40
+ const newTodo = { id: Date.now(), text, done: false };
41
+ store.set(itemsPath, [...items, newTodo]);
42
+ input.value = '';
43
+ });
44
+ input?.addEventListener('keydown', (e) => { if (e.key === 'Enter') btnAdd?.click(); });
45
+
46
+ // Filter buttons
47
+ btnAll?.addEventListener('click', () => store.set(filterPath, 'all'));
48
+ btnAct?.addEventListener('click', () => store.set(filterPath, 'active'));
49
+ btnDone?.addEventListener('click', () => store.set(filterPath, 'completed'));
50
+
51
+ // Clear completed
52
+ btnClear?.addEventListener('click', () => {
53
+ const items = store.get(itemsPath) || [];
54
+ store.set(itemsPath, items.filter(t => !t.done));
55
+ });
56
+
57
+ // Render
58
+ function render(){
59
+ const items = store.get(itemsPath) || [];
60
+ const filter = store.get(filterPath) || 'all';
61
+ const badge = main.querySelector('#filterBadge');
62
+ if (badge) badge.textContent = `filter: ${filter}`;
63
+ ul.replaceChildren();
64
+ let rows = items;
65
+ if (filter === 'active') rows = items.filter(t => !t.done);
66
+ else if (filter === 'completed') rows = items.filter(t => !!t.done);
67
+ rows.forEach((t) => {
68
+ const li = document.createElement('li');
69
+ li.style.display = 'flex'; li.style.gap = '8px'; li.style.alignItems = 'center';
70
+ const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !!t.done;
71
+ cb.addEventListener('change', () => {
72
+ const items = store.get(itemsPath) || [];
73
+ const updated = items.map(item =>
74
+ item.id === t.id ? { ...item, done: !item.done } : item
75
+ );
76
+ store.set(itemsPath, updated);
77
+ });
78
+ const span = document.createElement('span'); span.textContent = t.text; if (t.done) span.style.textDecoration = 'line-through';
79
+ li.appendChild(cb); li.appendChild(span); ul.appendChild(li);
80
+ });
81
+ }
82
+ const off1 = store.subscribe(itemsPath, render);
83
+ const off2 = store.subscribe(filterPath, render);
84
+ render();
85
+
86
+ if (signal) signal.addEventListener('abort', () => { try { off1 && off1(); off2 && off2(); } catch {} });
87
+ return () => { try { off1 && off1(); off2 && off2(); } catch {} };
88
+ }
@@ -0,0 +1,65 @@
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" />
6
+ <title>030 Todo App with eventTest</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body data-theme="light" data-bind="data-theme: ui.theme">
10
+ <nav style="display:flex; gap:10px; align-items:center; padding:10px 12px;">
11
+ <a href="/" data-link>Home</a>
12
+ <a href="/todo-demo" data-link>Todo app demo</a>
13
+ <span style="flex:1"></span>
14
+ <button class="btn" title="Toggle theme" data-on="click: toggleTheme() | log('toggle theme') | logPath('ui.theme')">Theme</button>
15
+ </nav>
16
+ <div data-route-root>
17
+ <main style="max-width: 600px; margin: 2rem auto; padding: 0 1rem;">
18
+ <h1>Home works!</h1>
19
+ <p>This is the home page of the todo app example.</p>
20
+ <p><a href="/todo-demo" data-link>Go to Todo App Demo →</a></p>
21
+ </main>
22
+ </div>
23
+
24
+ <!-- Router setup -->
25
+ <script type="module" src="./app/router.js"></script>
26
+ <!-- Behaviors runtime (declarative data-bind and data-on) -->
27
+ <script type="module">
28
+ import store from './app/store.js';
29
+ import { installBehaviors } from './runtime/core/behaviors.runtime.js';
30
+
31
+ // Inline actions for nav buttons
32
+ const registry = {
33
+ toggleTheme(ctx) {
34
+ const cur = ctx.get('ui.theme');
35
+ const next = (cur === 'dark') ? 'light' : 'dark';
36
+ ctx.set('ui.theme', next);
37
+ },
38
+ log(ctx, ...args) {
39
+ console.log('[action]', ...args);
40
+ },
41
+ logPath(ctx, path) {
42
+ console.log('[action]', String(path), ctx.get(String(path)));
43
+ }
44
+ };
45
+
46
+ installBehaviors(store, {
47
+ registry,
48
+ root: document,
49
+ writablePrefixes: ['ui.', 'todos.'],
50
+ debug: false
51
+ });
52
+
53
+ // Initialize theme
54
+ if (!store.get('ui.theme')) {
55
+ const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches;
56
+ store.set('ui.theme', prefersDark ? 'dark' : 'light');
57
+ }
58
+
59
+ // Persist theme to localStorage
60
+ store.subscribe('ui.theme', (theme) => {
61
+ localStorage.setItem('ui.theme', theme);
62
+ });
63
+ </script>
64
+ </body>
65
+ </html>
@@ -0,0 +1,282 @@
1
+ // behaviors.runtime.js — minimal parser + dispatcher for data-on/data-bind
2
+ // Install with: installBehaviors(store, { registry, root, writablePrefixes })
3
+
4
+ export function installBehaviors(store, { registry = {}, root = document, writablePrefixes = ['ui.'], writableWhitelist = [], debug = false, onStep = null } = {}){
5
+ const subsByPath = new Map();
6
+ const repeaters = [];
7
+ const trackOff = (el, off) => { const arr = subsByPath.get(el) || []; arr.push(off); subsByPath.set(el, arr); };
8
+ const unbindNode = (node) => {
9
+ // Unsubscribe any subs registered for this node and its descendants
10
+ const cleanup = (n) => {
11
+ const offs = subsByPath.get(n);
12
+ if (offs) { offs.forEach(off => { try { off(); } catch{} }); subsByPath.delete(n); }
13
+ n.childNodes && n.childNodes.forEach && n.childNodes.forEach(cleanup);
14
+ };
15
+ try { cleanup(node); } catch {}
16
+ };
17
+
18
+ const bindEl = (el) => {
19
+ // data-bind: "text: ui.counter; value: ui.name"
20
+ const bindAttr = el.getAttribute?.('data-bind');
21
+ if (bindAttr){
22
+ for (const part of bindAttr.split(';')){
23
+ const seg = part.trim(); if (!seg) continue;
24
+ const [propRaw, pathRaw] = seg.split(':');
25
+ const prop = (propRaw||'').trim();
26
+ const path = (pathRaw||'').trim();
27
+ const render = () => {
28
+ const v = store.get(path);
29
+ if (prop === 'text') el.textContent = v ?? '';
30
+ else if (prop === 'value') el.value = v ?? '';
31
+ else el.setAttribute(prop, v ?? '');
32
+ };
33
+ render();
34
+ const off = store.subscribe(path, render);
35
+ trackOff(el, off);
36
+ }
37
+ }
38
+
39
+ // data-on: "click: inc(ui.counter,1) | set(ui.last,'clicked'); input: setFrom(ui.name,target.value)"
40
+ const onAttr = el.getAttribute?.('data-on');
41
+ if (onAttr){
42
+ for (const rule of onAttr.split(';')){
43
+ const seg = rule.trim(); if (!seg) continue;
44
+ // Split only at the first ':' to avoid breaking on URLs like https://
45
+ const colonAt = seg.indexOf(':');
46
+ if (colonAt === -1) continue;
47
+ const evt = seg.slice(0, colonAt).trim();
48
+ const pipeRaw = seg.slice(colonAt + 1);
49
+ const steps = String(pipeRaw||'').split('|').map(s=>s.trim()).filter(Boolean);
50
+ el.addEventListener(evt, (event) => {
51
+ for (const step of steps){
52
+ const m = step.match(/^(\w+)\((.*)\)$/);
53
+ const name = m ? m[1] : step;
54
+ const argsStr = m ? m[2] : '';
55
+ const args = parseArgs(argsStr);
56
+ const fn = registry[name]; if (!fn) { if (console && console.warn) console.warn('[behaviors] missing action', name); continue; }
57
+ const ctx = {
58
+ store,
59
+ el,
60
+ event,
61
+ get: (p) => store.get(p),
62
+ set: (p, v) => {
63
+ // writable guards: prefixes AND optional whitelist patterns
64
+ const path = String(p);
65
+ const okPrefix = writablePrefixes.some(pref => path === pref.slice(0, -1) || path.startsWith(pref));
66
+ const okWhitelist = !writableWhitelist.length || writableWhitelist.some(pat => matchPattern(pat, path));
67
+ if (!(okPrefix && okWhitelist)) {
68
+ if (debug) console.warn('[behaviors] blocked write', { path, reason: 'guard' });
69
+ try { onStep && onStep({ el, event, name, args, phase: 'blocked', blocked: true, write: path, reason: 'guard' }); } catch{}
70
+ return;
71
+ }
72
+ store.set(p, v);
73
+ // annotate successful write
74
+ try { onStep && onStep({ el, event, name, args, phase: 'applied', blocked: false, write: path }); } catch{}
75
+ },
76
+ };
77
+ try {
78
+ try { onStep && onStep({ el, event, name, args, phase: 'started', blocked: false }); } catch{}
79
+ fn(ctx, ...args);
80
+ } catch (e) { console.warn('[behaviors] action error', name, e); }
81
+ }
82
+ });
83
+ }
84
+ }
85
+
86
+ // data-repeat: very small, read-only repeater with simple scope for item.*
87
+ // Example:
88
+ // <li data-repeat="item in ui.items" data-key="item.id"><span class="txt" data-bind="text: item.name"></span></li>
89
+ // <li data-repeat-empty>Empty</li>
90
+ const repExpr = el.getAttribute?.('data-repeat');
91
+ if (repExpr) {
92
+ const parsed = parseRepeat(repExpr);
93
+ if (!parsed) return;
94
+ const { itemName, idxName, listPath, keyExpr } = parsed;
95
+ // Detach template and install renderer anchored by a comment
96
+ const parent = el.parentNode;
97
+ if (!parent) return;
98
+ const anchor = document.createComment('repeat-anchor');
99
+ parent.insertBefore(anchor, el);
100
+ parent.removeChild(el);
101
+
102
+ const emptyEl = parent.querySelector?.('[data-repeat-empty]') || null;
103
+
104
+ // State for keyed reconciliation
105
+ const nodesByKey = new Map();
106
+
107
+ const renderList = () => {
108
+ const list = store.get(listPath) || [];
109
+ // Toggle empty placeholder
110
+ if (emptyEl) {
111
+ try {
112
+ const isEmpty = !Array.isArray(list) || list.length === 0;
113
+ if (isEmpty) { emptyEl.removeAttribute('hidden'); emptyEl.style && (emptyEl.style.display = ''); }
114
+ else { emptyEl.setAttribute('hidden', ''); emptyEl.style && (emptyEl.style.display = 'none'); }
115
+ } catch {}
116
+ }
117
+
118
+ if (!Array.isArray(list)) return;
119
+
120
+ // Build a fragment in order with keyed clones
121
+ const frag = document.createDocumentFragment();
122
+ const nextNodesByKey = new Map();
123
+
124
+ for (let idx = 0; idx < list.length; idx++) {
125
+ const item = list[idx];
126
+ const key = resolveKey(item, idx, keyExpr);
127
+ let node = nodesByKey.get(key);
128
+ if (!node) {
129
+ node = el.cloneNode(true);
130
+ // Simple scoped binding for common cases used in tests:
131
+ // - text: item.name
132
+ // - attributes with {{item.id}}
133
+ // Apply text bindings manually
134
+ node.querySelectorAll?.('[data-bind]').forEach((n) => {
135
+ const bindAttr = n.getAttribute('data-bind') || '';
136
+ bindAttr.split(';').map(s => s.trim()).filter(Boolean).forEach((seg) => {
137
+ const [propRaw, pathRaw] = seg.split(':');
138
+ const prop = (propRaw||'').trim();
139
+ const pth = (pathRaw||'').trim();
140
+ let val;
141
+ if (pth === idxName && idxName) {
142
+ val = idx;
143
+ } else if (pth.startsWith(itemName + '.')) {
144
+ val = getByPath(item, pth.slice(itemName.length + 1));
145
+ }
146
+ if (pth === itemName) val = item; // not deeply rendered, but allow truthy check
147
+ if (val !== undefined) {
148
+ if (prop === 'text') n.textContent = val ?? '';
149
+ else if (prop === 'value') n.value = val ?? '';
150
+ else n.setAttribute(prop, val ?? '');
151
+ }
152
+ });
153
+ // Prevent behaviors runtime from also binding this node to store paths like "item.*"
154
+ n.removeAttribute('data-bind');
155
+ });
156
+ // Interpolate {{item.*}} in attributes used by tests (e.g., data-id)
157
+ node.querySelectorAll('*').forEach((n) => {
158
+ for (const attr of Array.from(n.attributes || [])){
159
+ const m = /\{\{\s*(?:item\.(.+?)|(idx))\s*\}\}/g;
160
+ if (m.test(attr.value)){
161
+ const replaced = attr.value.replace(/\{\{\s*(?:item\.(.+?)|(idx))\s*\}\}/g, (_, pItem, pIdx) => {
162
+ if (pIdx && idxName) return String(idx);
163
+ const v = getByPath(item, pItem);
164
+ return v == null ? '' : String(v);
165
+ });
166
+ n.setAttribute(attr.name, replaced);
167
+ }
168
+ }
169
+ });
170
+ }
171
+ nextNodesByKey.set(key, node);
172
+ frag.appendChild(node);
173
+ }
174
+
175
+ // Replace current range (between anchor and next non-render node) with frag
176
+ // Simple strategy: remove all nodes after anchor until a repeat-empty (which we keep)
177
+ let cursor = anchor.nextSibling;
178
+ while (cursor && cursor !== emptyEl) {
179
+ const next = cursor.nextSibling;
180
+ parent.removeChild(cursor);
181
+ cursor = next;
182
+ }
183
+ parent.insertBefore(frag, emptyEl || null);
184
+ nodesByKey.clear();
185
+ nextNodesByKey.forEach((v,k)=>nodesByKey.set(k,v));
186
+ };
187
+
188
+ renderList();
189
+ const off = store.subscribe(listPath, renderList);
190
+ repeaters.push({ off });
191
+ trackOff(el, off);
192
+ }
193
+ };
194
+
195
+ // initial scan
196
+ const all = root.querySelectorAll?.('[data-bind], [data-on], [data-repeat]') || [];
197
+ all.forEach(bindEl);
198
+
199
+ // observe new nodes
200
+ const mo = new MutationObserver((mutList) => {
201
+ for (const m of mutList){
202
+ m.addedNodes && m.addedNodes.forEach(node => {
203
+ if (node.nodeType !== 1) return;
204
+ if (node.matches?.('[data-bind], [data-on], [data-repeat]')) bindEl(node);
205
+ node.querySelectorAll?.('[data-bind], [data-on], [data-repeat]').forEach(bindEl);
206
+ });
207
+ // cleanup removed
208
+ m.removedNodes && m.removedNodes.forEach(node => {
209
+ if (node.nodeType !== 1) return;
210
+ unbindNode(node);
211
+ });
212
+ }
213
+ });
214
+ if (root && root instanceof Document) mo.observe(document.body, { childList: true, subtree: true });
215
+
216
+ return () => {
217
+ mo.disconnect();
218
+ subsByPath.forEach(offs => offs.forEach(off => { try { off(); } catch{} }));
219
+ subsByPath.clear();
220
+ };
221
+ }
222
+
223
+ function parseArgs(s){
224
+ if (!s.trim()) return [];
225
+ // split by commas not inside quotes
226
+ const parts = s.split(',').map(p=>p.trim());
227
+ return parts.map(coerceArg);
228
+ }
229
+
230
+ function coerceArg(x){
231
+ if (x === '') return '';
232
+ if ((x.startsWith('"') && x.endsWith('"')) || (x.startsWith("'") && x.endsWith("'"))) return x.slice(1,-1);
233
+ if (x === 'true') return true;
234
+ if (x === 'false') return false;
235
+ if (x === 'null') return null;
236
+ if (!Number.isNaN(Number(x))) return Number(x);
237
+ return x; // pass path strings etc.
238
+ }
239
+
240
+ function matchPattern(pattern, path){
241
+ // Very small wildcard: '*' matches a single segment, '**' matches the rest
242
+ if (pattern === path) return true;
243
+ const pSegs = String(pattern).split('.');
244
+ const sSegs = String(path).split('.');
245
+ let i = 0, j = 0;
246
+ while (i < pSegs.length && j < sSegs.length){
247
+ const part = pSegs[i];
248
+ if (part === '**'){ return true; }
249
+ if (part === '*' || part === sSegs[j]){ i++; j++; continue; }
250
+ return false;
251
+ }
252
+ // Allow trailing '**'
253
+ while (i < pSegs.length && pSegs[i] === '**') i++;
254
+ return i === pSegs.length && j === sSegs.length;
255
+ }
256
+
257
+ // Dev-only explicit export to support unit tests in 010-005
258
+ export { matchPattern };
259
+
260
+ function parseRepeat(expr){
261
+ // "item in ui.items" or "(item, idx) in ui.items"
262
+ const m = String(expr).match(/^\s*\(?\s*([a-zA-Z_$][\w$]*)\s*(?:,\s*([a-zA-Z_$][\w$]*))?\s*\)?\s+in\s+([\w$.]+)\s*$/);
263
+ if (!m) return null;
264
+ const itemName = m[1];
265
+ const idxName = m[2] || null;
266
+ const listPath = m[3];
267
+ // Optional data-key attribute is read from element attribute when rendering
268
+ return { itemName, idxName, listPath, keyExpr: `${itemName}.id` };
269
+ }
270
+
271
+ function resolveKey(item, idx, keyExpr){
272
+ // For now only support item.id; fallback to index
273
+ const v = (item && (item.id != null)) ? item.id : idx;
274
+ return String(v);
275
+ }
276
+
277
+ function getByPath(obj, path){
278
+ const segs = String(path).split('.');
279
+ let cur = obj;
280
+ for (const s of segs){ if (cur == null) return undefined; cur = cur[s]; }
281
+ return cur;
282
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * UIstate - Event-based hierarchical state management module
3
+ * Part of the UIstate declarative state management system
4
+ * Uses DOM events for pub/sub with hierarchical path support
5
+ */
6
+
7
+ const createEventState = (initial = {}) => {
8
+ // // Clone the initial state to avoid direct mutations to the passed object
9
+ const store = JSON.parse(JSON.stringify(initial));
10
+
11
+ // // Create a dedicated DOM element to use as an event bus
12
+ // const bus = document.createElement("x-store");
13
+
14
+ // // Optional: Keep the bus element off the actual DOM for better encapsulation
15
+ // // but this isn't strictly necessary for functionality
16
+ // bus.style.display = "none";
17
+ // document.documentElement.appendChild(bus);
18
+
19
+ const bus = new EventTarget();
20
+ return {
21
+ // get a value from the store by path
22
+ get: (path) => {
23
+ if (!path) return store;
24
+ return path
25
+ .split(".")
26
+ .reduce(
27
+ (obj, prop) =>
28
+ obj && obj[prop] !== undefined ? obj[prop] : undefined,
29
+ store
30
+ );
31
+ },
32
+
33
+ // set a value in the store by path
34
+ set: (path, value) => {
35
+ if(!path) return;
36
+
37
+ // Update the store
38
+ let target = store;
39
+ const parts = path.split(".");
40
+ const last = parts.pop();
41
+
42
+ // Create the path if it doesn't exist
43
+ parts.forEach((part) => {
44
+ if (!target[part] || typeof target[part] !== "object") {
45
+ target[part] = {};
46
+ }
47
+ target = target[part];
48
+ });
49
+
50
+ // Set the value
51
+ target[last] = value;
52
+
53
+ // Notify subscribers with a DOM event
54
+ bus.dispatchEvent(new CustomEvent(path, { detail: value }));
55
+
56
+ // Also dispatch events for parent paths to support wildcards
57
+ if (parts.length > 0) {
58
+ let parentPath = "";
59
+ for (const part of parts) {
60
+ parentPath = parentPath ? `${parentPath}.${part}` : part;
61
+ bus.dispatchEvent(
62
+ new CustomEvent(`${parentPath}.*`, {
63
+ detail: { path, value },
64
+ })
65
+ );
66
+ }
67
+
68
+ // Dispatch root wildcard for any state change
69
+ bus.dispatchEvent(
70
+ new CustomEvent("*", {
71
+ detail: { path, value},
72
+ })
73
+ );
74
+ }
75
+
76
+ return value;
77
+ },
78
+
79
+ // Subscribe to changes on a path
80
+ subscribe: (path, callback) => {
81
+ if (!path || typeof callback !== "function") return () => {};
82
+
83
+ const handler = (e) => callback(e.detail, path);
84
+ bus.addEventListener(path, handler);
85
+
86
+ return () => bus.removeEventListener(path, handler);
87
+ },
88
+
89
+ // Optional method to clean up resources
90
+ destroy: () => {
91
+ if (bus.parentNode) {
92
+ bus.parentNode.removeChild(bus);
93
+ }
94
+ },
95
+
96
+ };
97
+ };
98
+
99
+ export default createEventState;
100
+ export { createEventState };