@uistate/core 4.1.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-eventTest.md +26 -0
- package/README.md +409 -42
- package/eventState.js +58 -69
- package/eventTest.js +196 -0
- package/examples/001-counter/README.md +44 -0
- package/examples/001-counter/eventState.js +86 -0
- package/examples/001-counter/index.html +30 -0
- package/examples/002-counter-improved/README.md +44 -0
- package/examples/002-counter-improved/eventState.js +86 -0
- package/examples/002-counter-improved/index.html +44 -0
- package/examples/003-input-reactive/README.md +44 -0
- package/examples/003-input-reactive/eventState.js +86 -0
- package/examples/003-input-reactive/index.html +30 -0
- package/examples/004-computed-state/README.md +45 -0
- package/examples/004-computed-state/eventState.js +86 -0
- package/examples/004-computed-state/index.html +62 -0
- package/examples/005-conditional-rendering/README.md +42 -0
- package/examples/005-conditional-rendering/eventState.js +86 -0
- package/examples/005-conditional-rendering/index.html +36 -0
- package/examples/006-list-rendering/README.md +49 -0
- package/examples/006-list-rendering/eventState.js +86 -0
- package/examples/006-list-rendering/index.html +60 -0
- package/examples/007-form-validation/README.md +52 -0
- package/examples/007-form-validation/eventState.js +86 -0
- package/examples/007-form-validation/index.html +99 -0
- package/examples/008-undo-redo/README.md +70 -0
- package/examples/008-undo-redo/eventState.js +86 -0
- package/examples/008-undo-redo/index.html +105 -0
- package/examples/009-localStorage-side-effects/README.md +72 -0
- package/examples/009-localStorage-side-effects/eventState.js +86 -0
- package/examples/009-localStorage-side-effects/index.html +54 -0
- package/examples/010-decoupled-components/README.md +74 -0
- package/examples/010-decoupled-components/eventState.js +86 -0
- package/examples/010-decoupled-components/index.html +90 -0
- package/examples/011-async-patterns/README.md +98 -0
- package/examples/011-async-patterns/eventState.js +86 -0
- package/examples/011-async-patterns/index.html +129 -0
- package/examples/028-counter-improved-eventTest/LICENSE +55 -0
- package/examples/028-counter-improved-eventTest/README.md +131 -0
- package/examples/028-counter-improved-eventTest/app/store.js +9 -0
- package/examples/028-counter-improved-eventTest/index.html +49 -0
- package/examples/028-counter-improved-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/examples/028-counter-improved-eventTest/runtime/core/eventState.js +100 -0
- package/examples/028-counter-improved-eventTest/runtime/core/helpers.js +212 -0
- package/examples/028-counter-improved-eventTest/runtime/core/router.js +271 -0
- package/examples/028-counter-improved-eventTest/store.d.ts +8 -0
- package/examples/028-counter-improved-eventTest/style.css +170 -0
- package/examples/028-counter-improved-eventTest/tests/README.md +208 -0
- package/examples/028-counter-improved-eventTest/tests/counter.test.js +116 -0
- package/examples/028-counter-improved-eventTest/tests/eventTest.js +176 -0
- package/examples/028-counter-improved-eventTest/tests/generateTypes.js +168 -0
- package/examples/028-counter-improved-eventTest/tests/run.js +20 -0
- package/examples/030-todo-app-with-eventTest/LICENSE +55 -0
- package/examples/030-todo-app-with-eventTest/README.md +121 -0
- package/examples/030-todo-app-with-eventTest/app/router.js +25 -0
- package/examples/030-todo-app-with-eventTest/app/store.js +16 -0
- package/examples/030-todo-app-with-eventTest/app/views/home.js +11 -0
- package/examples/030-todo-app-with-eventTest/app/views/todoDemo.js +88 -0
- package/examples/030-todo-app-with-eventTest/index.html +65 -0
- package/examples/030-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/examples/030-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/examples/030-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/examples/030-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/examples/030-todo-app-with-eventTest/store.d.ts +18 -0
- package/examples/030-todo-app-with-eventTest/style.css +170 -0
- package/examples/030-todo-app-with-eventTest/tests/README.md +208 -0
- package/examples/030-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/examples/030-todo-app-with-eventTest/tests/generateTypes.js +189 -0
- package/examples/030-todo-app-with-eventTest/tests/run.js +20 -0
- package/examples/030-todo-app-with-eventTest/tests/todos.test.js +167 -0
- package/examples/031-todo-app-with-eventTest/LICENSE +55 -0
- package/examples/031-todo-app-with-eventTest/README.md +54 -0
- package/examples/031-todo-app-with-eventTest/TUTORIAL.md +390 -0
- package/examples/031-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
- package/examples/031-todo-app-with-eventTest/app/bridges.js +113 -0
- package/examples/031-todo-app-with-eventTest/app/router.js +26 -0
- package/examples/031-todo-app-with-eventTest/app/store.js +15 -0
- package/examples/031-todo-app-with-eventTest/app/views/home.js +46 -0
- package/examples/031-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
- package/examples/031-todo-app-with-eventTest/devtools/dock.js +41 -0
- package/examples/031-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
- package/examples/031-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
- package/examples/031-todo-app-with-eventTest/devtools/telemetry.js +104 -0
- package/examples/031-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
- package/examples/031-todo-app-with-eventTest/index.html +103 -0
- package/examples/031-todo-app-with-eventTest/package-lock.json +2184 -0
- package/examples/031-todo-app-with-eventTest/package.json +24 -0
- package/examples/031-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/examples/031-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/examples/031-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/examples/031-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
- package/examples/031-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
- package/examples/031-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
- package/examples/031-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
- package/examples/031-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
- package/examples/031-todo-app-with-eventTest/store.d.ts +23 -0
- package/examples/031-todo-app-with-eventTest/style.css +170 -0
- package/examples/031-todo-app-with-eventTest/tests/README.md +208 -0
- package/examples/031-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/examples/031-todo-app-with-eventTest/tests/generateTypes.js +191 -0
- package/examples/031-todo-app-with-eventTest/tests/run.js +20 -0
- package/examples/031-todo-app-with-eventTest/tests/todos.test.js +192 -0
- package/examples/032-todo-app-with-eventTest/LICENSE +55 -0
- package/examples/032-todo-app-with-eventTest/README.md +54 -0
- package/examples/032-todo-app-with-eventTest/TUTORIAL.md +390 -0
- package/examples/032-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
- package/examples/032-todo-app-with-eventTest/app/actions/index.js +153 -0
- package/examples/032-todo-app-with-eventTest/app/bridges.js +113 -0
- package/examples/032-todo-app-with-eventTest/app/router.js +26 -0
- package/examples/032-todo-app-with-eventTest/app/store.js +15 -0
- package/examples/032-todo-app-with-eventTest/app/views/home.js +46 -0
- package/examples/032-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
- package/examples/032-todo-app-with-eventTest/devtools/dock.js +41 -0
- package/examples/032-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
- package/examples/032-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
- package/examples/032-todo-app-with-eventTest/devtools/telemetry.js +104 -0
- package/examples/032-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
- package/examples/032-todo-app-with-eventTest/index.html +87 -0
- package/examples/032-todo-app-with-eventTest/package-lock.json +2184 -0
- package/examples/032-todo-app-with-eventTest/package.json +24 -0
- package/examples/032-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/examples/032-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/examples/032-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/examples/032-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
- package/examples/032-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
- package/examples/032-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
- package/examples/032-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
- package/examples/032-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
- package/examples/032-todo-app-with-eventTest/store.d.ts +23 -0
- package/examples/032-todo-app-with-eventTest/style.css +170 -0
- package/examples/032-todo-app-with-eventTest/tests/README.md +208 -0
- package/examples/032-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/examples/032-todo-app-with-eventTest/tests/generateTypes.js +191 -0
- package/examples/032-todo-app-with-eventTest/tests/run.js +20 -0
- package/examples/032-todo-app-with-eventTest/tests/todos.test.js +192 -0
- package/index.js +10 -11
- package/package.json +16 -7
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Proprietary License for eventTest.js
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ajdin Imsirovic
|
|
4
|
+
|
|
5
|
+
## Permitted Uses
|
|
6
|
+
|
|
7
|
+
You may use `eventTest.js` for:
|
|
8
|
+
- Personal projects
|
|
9
|
+
- Open-source projects
|
|
10
|
+
- Educational purposes
|
|
11
|
+
|
|
12
|
+
## Restrictions
|
|
13
|
+
|
|
14
|
+
You may NOT:
|
|
15
|
+
- Use this file in commercial products without a license
|
|
16
|
+
- Modify or create derivative works
|
|
17
|
+
- Redistribute this file separately from @uistate/core
|
|
18
|
+
- Remove or alter this license notice
|
|
19
|
+
|
|
20
|
+
## Commercial Licensing
|
|
21
|
+
|
|
22
|
+
For commercial use, please contact: your@email.com
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
|
package/README.md
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
# UIstate
|
|
1
|
+
# UIstate v5
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Lightweight event-driven state management for modern web applications**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@uistate/core)
|
|
6
|
+
[](#license)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
After many experiments exploring different state management paradigms, UIstate v5 emerges as a focused, production-ready solution for complex UIs. Born from real-world challenges like building data tables, dashboards, and interactive applications.
|
|
9
|
+
|
|
10
|
+
## What's New in v5
|
|
11
|
+
|
|
12
|
+
- **EventState Core**: Path-based subscriptions with wildcard support (~80 LOC)
|
|
13
|
+
- **Slot Orchestration**: Atomic component swapping without layout shift
|
|
14
|
+
- **Intent-Driven**: Declarative command handling for decoupled architecture
|
|
15
|
+
- **Zero Dependencies**: Pure JavaScript, works in any modern browser
|
|
16
|
+
- **Framework-Free**: No build step required (but works great with Vite)
|
|
9
17
|
|
|
10
18
|
## Installation
|
|
11
19
|
|
|
@@ -15,48 +23,351 @@ npm install @uistate/core
|
|
|
15
23
|
|
|
16
24
|
## Quick Start
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
```javascript
|
|
27
|
+
import { createEventState } from '@uistate/core';
|
|
28
|
+
|
|
29
|
+
const store = createEventState({
|
|
30
|
+
user: { name: 'Alice' },
|
|
31
|
+
count: 0
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Subscribe to specific paths
|
|
35
|
+
store.subscribe('user.name', (value) => {
|
|
36
|
+
console.log('Name changed:', value);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Subscribe with wildcards
|
|
40
|
+
store.subscribe('user.*', ({ path, value }) => {
|
|
41
|
+
console.log(`${path} changed to:`, value);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Update state
|
|
45
|
+
store.set('user.name', 'Bob'); // → Name changed: Bob
|
|
46
|
+
store.set('count', 1);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Core API
|
|
50
|
+
|
|
51
|
+
### `createEventState(initial?)`
|
|
52
|
+
|
|
53
|
+
Creates a new reactive store.
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
const store = createEventState({ count: 0 });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `store.get(path)`
|
|
60
|
+
|
|
61
|
+
Retrieves a value by path.
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
const count = store.get('count');
|
|
65
|
+
const user = store.get('user'); // Returns entire user object
|
|
66
|
+
const all = store.get(); // Returns entire store
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### `store.set(path, value)`
|
|
70
|
+
|
|
71
|
+
Sets a value and triggers subscriptions.
|
|
19
72
|
|
|
20
73
|
```javascript
|
|
21
|
-
|
|
74
|
+
store.set('count', 42);
|
|
75
|
+
store.set('user.email', 'alice@example.com');
|
|
76
|
+
```
|
|
22
77
|
|
|
23
|
-
|
|
24
|
-
const cssState = createCssState();
|
|
78
|
+
### `store.subscribe(path, handler)`
|
|
25
79
|
|
|
26
|
-
|
|
27
|
-
const eventState = createEventState();
|
|
80
|
+
Subscribes to changes. Returns an unsubscribe function.
|
|
28
81
|
|
|
29
|
-
|
|
30
|
-
|
|
82
|
+
```javascript
|
|
83
|
+
// Exact path
|
|
84
|
+
const unsub = store.subscribe('count', (value) => {
|
|
85
|
+
console.log('Count:', value);
|
|
86
|
+
});
|
|
31
87
|
|
|
32
|
-
//
|
|
33
|
-
|
|
88
|
+
// Wildcard (child changes)
|
|
89
|
+
store.subscribe('user.*', ({ path, value }) => {
|
|
90
|
+
console.log(`User property ${path} changed`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Global wildcard (all changes)
|
|
94
|
+
store.subscribe('*', ({ path, value }) => {
|
|
95
|
+
console.log('Something changed');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Cleanup
|
|
99
|
+
unsub();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `store.destroy()`
|
|
103
|
+
|
|
104
|
+
Cleans up the event bus and prevents further updates.
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
store.destroy();
|
|
34
108
|
```
|
|
35
109
|
|
|
36
|
-
|
|
110
|
+
# Changelog
|
|
37
111
|
|
|
38
|
-
|
|
39
|
-
Manages state using CSS custom properties for optimal performance and automatic reactivity.
|
|
112
|
+
## [5.0.0] - 2025-12-09
|
|
40
113
|
|
|
41
|
-
###
|
|
42
|
-
|
|
114
|
+
### Breaking Changes
|
|
115
|
+
- eventState is now primary export (was cssState)
|
|
116
|
+
- DOM element event bus replaced with EventTarget
|
|
117
|
+
- ...
|
|
43
118
|
|
|
44
|
-
###
|
|
45
|
-
|
|
119
|
+
### Added
|
|
120
|
+
- Slot orchestration pattern
|
|
121
|
+
- Intent-driven architecture
|
|
122
|
+
- Event-sequence testing (experimental)
|
|
123
|
+
- Example 009: Financial dashboard
|
|
124
|
+
- Example 010: Event testing
|
|
46
125
|
|
|
47
|
-
|
|
48
|
-
Declarative template management system for building UIs with CSS-based templates.
|
|
126
|
+
## Architecture Patterns
|
|
49
127
|
|
|
50
|
-
|
|
128
|
+
UIstate v5 introduces battle-tested patterns from many experiments:
|
|
51
129
|
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
130
|
+
### 1. **Intent-Driven Updates** (Declarative Commands)
|
|
131
|
+
|
|
132
|
+
Intents are just regular state paths—no special API needed. Components subscribe to intent paths and execute logic when triggered:
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
// Subscribe to an intent path
|
|
136
|
+
store.subscribe('intent.todo.add', ({ text }) => {
|
|
137
|
+
const items = store.get('todos.items') || [];
|
|
138
|
+
store.set('todos.items', [...items, { id: Date.now(), text, done: false }]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Trigger intent from UI by setting the path
|
|
142
|
+
button.addEventListener('click', () => {
|
|
143
|
+
store.set('intent.todo.add', { text: input.value });
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Key insight**: Intents are paths, not events. This means:
|
|
148
|
+
- Subscribe once at component mount
|
|
149
|
+
- Multiple components can listen to the same intent
|
|
150
|
+
- Easy to test (just `set()` the intent path)
|
|
151
|
+
- No special event emitter needed
|
|
152
|
+
|
|
153
|
+
### 2. **Slot Orchestration** (Flicker-free component loading)
|
|
154
|
+
|
|
155
|
+
Load layouts and components separately for atomic, layout-shift-free updates:
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
// 1. Mount persistent layout once
|
|
159
|
+
async function mountMaster(master) {
|
|
160
|
+
const layout = await fetchHTML(master.layout);
|
|
161
|
+
document.getElementById('app').innerHTML = layout;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 2. Load components into slots atomically
|
|
165
|
+
async function loadSlot({ slot, url }) {
|
|
166
|
+
const config = await fetchJSON(url);
|
|
167
|
+
const html = await fetchHTML(config.component);
|
|
168
|
+
|
|
169
|
+
// Find slot container
|
|
170
|
+
const slotHost = document.querySelector(`[data-slot="${slot}"]`);
|
|
171
|
+
|
|
172
|
+
// Atomic replacement - no layout shift!
|
|
173
|
+
const fragment = createFragment(html);
|
|
174
|
+
slotHost.replaceChildren(fragment);
|
|
175
|
+
|
|
176
|
+
// Wire up component subscriptions
|
|
177
|
+
wireComponent(slotHost, config.bindings);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Trigger slot loading via intent
|
|
181
|
+
store.subscribe('intent.slot.load', loadSlot);
|
|
182
|
+
store.set('intent.slot.load', { slot: 'main', url: '/slots/todo.json' });
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Key insight**: Separate layout (skeleton) from content (slots):
|
|
186
|
+
- Layout provides stable structure
|
|
187
|
+
- Slots swap atomically without reflow
|
|
188
|
+
- Each component manages its own subscriptions
|
|
189
|
+
- Clean separation of concerns
|
|
190
|
+
|
|
191
|
+
### 3. **Component Bindings** (Path-based reactivity)
|
|
192
|
+
|
|
193
|
+
Components subscribe to specific state paths, not global wildcards:
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
function wireComponent(root, bindings) {
|
|
197
|
+
const input = root.querySelector('#input');
|
|
198
|
+
const list = root.querySelector('#list');
|
|
199
|
+
const itemsPath = bindings.items || 'domain.items';
|
|
200
|
+
|
|
201
|
+
// Subscribe to specific path for this component
|
|
202
|
+
const unsubscribe = store.subscribe(itemsPath, (items) => {
|
|
203
|
+
list.replaceChildren();
|
|
204
|
+
items.forEach(item => {
|
|
205
|
+
const li = document.createElement('li');
|
|
206
|
+
li.textContent = item.text;
|
|
207
|
+
list.appendChild(li);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Initial render
|
|
212
|
+
const items = store.get(itemsPath);
|
|
213
|
+
if (items) store.set(itemsPath, items);
|
|
214
|
+
|
|
215
|
+
// Return cleanup function
|
|
216
|
+
return unsubscribe;
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Key insight**: Each component subscribes to its own paths:
|
|
221
|
+
- No global `*` wildcards in production code
|
|
222
|
+
- Components are isolated and testable
|
|
223
|
+
- Easy cleanup when unmounting
|
|
224
|
+
- Clear data dependencies
|
|
225
|
+
|
|
226
|
+
### 4. **Component Lifecycle Pattern**
|
|
227
|
+
|
|
228
|
+
Proper mount/unmount with subscription cleanup:
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
const componentRegistry = new Map();
|
|
232
|
+
|
|
233
|
+
async function loadSlot({ slot, url }) {
|
|
234
|
+
// Cleanup previous component in this slot
|
|
235
|
+
const existing = componentRegistry.get(slot);
|
|
236
|
+
if (existing?.cleanup) {
|
|
237
|
+
existing.cleanup();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Load and mount new component
|
|
241
|
+
const config = await fetchJSON(url);
|
|
242
|
+
const html = await fetchHTML(config.component);
|
|
243
|
+
const slotHost = document.querySelector(`[data-slot="${slot}"]`);
|
|
244
|
+
slotHost.replaceChildren(createFragment(html));
|
|
245
|
+
|
|
246
|
+
// Wire up with cleanup function
|
|
247
|
+
const cleanup = wireComponent(slotHost, config.bindings);
|
|
248
|
+
|
|
249
|
+
// Store for later cleanup
|
|
250
|
+
componentRegistry.set(slot, { cleanup });
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Key insight**: Always clean up subscriptions:
|
|
255
|
+
- Prevents memory leaks
|
|
256
|
+
- Allows safe component swapping
|
|
257
|
+
- Components are truly hot-swappable
|
|
258
|
+
- No zombie subscriptions
|
|
259
|
+
|
|
260
|
+
### 5. **Content-Driven Layout** (Layout morphs based on active content)
|
|
261
|
+
|
|
262
|
+
One layout HTML can adapt to different use cases by subscribing to state and toggling CSS classes:
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
function wireLayout(root, bindings) {
|
|
266
|
+
// ...existing button wiring...
|
|
267
|
+
|
|
268
|
+
// Layout adapts based on which slot is active
|
|
269
|
+
store.subscribe('ui.view.active', (active) => {
|
|
270
|
+
const kpiCol = root.querySelector('[data-slot="kpi"]').closest('.col-12');
|
|
271
|
+
const newsCol = root.querySelector('[data-slot="news"]').closest('.col-12');
|
|
272
|
+
const positionsCol = root.querySelector('[data-slot="positions"]').closest('.col-12');
|
|
273
|
+
|
|
274
|
+
if (active === 'todos') {
|
|
275
|
+
// Full-width editing mode
|
|
276
|
+
kpiCol.classList.add('d-none');
|
|
277
|
+
newsCol.classList.add('d-none');
|
|
278
|
+
positionsCol.classList.remove('col-md-6', 'col-lg-4');
|
|
279
|
+
positionsCol.classList.add('col-md-12', 'col-lg-8');
|
|
280
|
+
} else {
|
|
281
|
+
// Multi-column dashboard mode
|
|
282
|
+
kpiCol.classList.remove('d-none');
|
|
283
|
+
newsCol.classList.remove('d-none');
|
|
284
|
+
positionsCol.classList.remove('col-md-12', 'col-lg-8');
|
|
285
|
+
positionsCol.classList.add('col-md-6', 'col-lg-4');
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Key insight**: Content configures layout, not the other way around:
|
|
292
|
+
- One layout HTML handles all scenarios
|
|
293
|
+
- Panels show/hide based on active content
|
|
294
|
+
- Sections expand/collapse responsively
|
|
295
|
+
- Pure CSS class manipulation - no DOM restructuring
|
|
296
|
+
- Declarative layout rules via state subscription
|
|
297
|
+
|
|
298
|
+
**Real-world applications**:
|
|
299
|
+
```javascript
|
|
300
|
+
// E-commerce: hide sidebar, expand product grid
|
|
301
|
+
if (active === 'products') hideSlots(['filters', 'cart']); expandSlot('grid');
|
|
302
|
+
|
|
303
|
+
// Admin: hide navigation, expand table
|
|
304
|
+
if (active === 'report') hideSlots(['nav', 'sidebar']); expandSlot('table');
|
|
305
|
+
|
|
306
|
+
// Editor: focus mode - hide everything except content
|
|
307
|
+
if (active === 'editor') hideSlots(['toolbar', 'preview']); expandSlot('content');
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
This pattern eliminates the "rigid layout" criticism - UIstate layouts are as flexible as any framework, with zero VDOM overhead.
|
|
311
|
+
|
|
312
|
+
## Complete Example
|
|
313
|
+
|
|
314
|
+
See `examples/009-financial-dashboard` for a real-world application featuring:
|
|
315
|
+
|
|
316
|
+
- Multi-slot dashboard layout
|
|
317
|
+
- TodoList with filtering
|
|
318
|
+
- Financial data tables (KPI, Positions, Debts, Sales, News)
|
|
319
|
+
- Intent-driven architecture
|
|
320
|
+
- Atomic slot swapping
|
|
321
|
+
- State inspector
|
|
322
|
+
|
|
323
|
+
Run the example:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
cd node_modules/@uistate/core/examples/009-financial-dashboard
|
|
327
|
+
npx serve .
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Open http://localhost:3000 and explore the multi-slot dashboard!
|
|
331
|
+
|
|
332
|
+
## Why UIstate v5?
|
|
333
|
+
|
|
334
|
+
### vs. React/Vue
|
|
335
|
+
|
|
336
|
+
- ✅ **No VDOM overhead** - Direct DOM manipulation with surgical updates
|
|
337
|
+
- ✅ **No build step required** - Works in browsers directly
|
|
338
|
+
- ✅ **Simpler mental model** - Paths + subscriptions, not components + lifecycle hooks
|
|
339
|
+
- ✅ **Smaller bundle** - ~3KB core vs ~40KB+ frameworks
|
|
340
|
+
- ✅ **Explicit reactivity** - Know exactly what triggers what
|
|
341
|
+
- ❌ No component ecosystem
|
|
342
|
+
- ❌ Manual DOM updates (but that's the point!)
|
|
343
|
+
|
|
344
|
+
### vs. Alpine.js
|
|
345
|
+
|
|
346
|
+
- ✅ **Better for complex state** - Nested paths, cross-component communication
|
|
347
|
+
- ✅ **First-class SPA patterns** - Slot orchestration, intent-driven updates
|
|
348
|
+
- ✅ **More powerful subscriptions** - Path-based, wildcard support, granular control
|
|
349
|
+
- ✅ **Explicit data flow** - No magic, clear cause and effect
|
|
350
|
+
- ❌ More JavaScript required (Alpine is more declarative HTML)
|
|
351
|
+
|
|
352
|
+
### vs. Zustand/Jotai
|
|
353
|
+
|
|
354
|
+
- ✅ **Framework-agnostic** - Not tied to React
|
|
355
|
+
- ✅ **Path-based state** - More intuitive for deeply nested data
|
|
356
|
+
- ✅ **Wildcard subscriptions** - Subscribe to entire subtrees when needed
|
|
357
|
+
- ✅ **Simpler API** - Just get/set/subscribe, no atoms or stores
|
|
358
|
+
- ❌ Smaller ecosystem
|
|
359
|
+
- ❌ No React DevTools integration
|
|
360
|
+
|
|
361
|
+
## Use Cases
|
|
362
|
+
|
|
363
|
+
UIstate v5 excels at:
|
|
364
|
+
|
|
365
|
+
- ✅ **Internal dashboards** - Complex state, multiple data sources
|
|
366
|
+
- ✅ **Admin panels** - CRUD operations, forms, data tables
|
|
367
|
+
- ✅ **Financial applications** - Real-time updates, calculations
|
|
368
|
+
- ✅ **Data-heavy UIs** - Large datasets, filtered views
|
|
369
|
+
- ✅ **Multi-pane interfaces** - Independent but coordinated components
|
|
370
|
+
- ✅ **Progressive enhancement** - Add interactivity to server-rendered HTML
|
|
60
371
|
|
|
61
372
|
## Browser Support
|
|
62
373
|
|
|
@@ -65,21 +376,77 @@ Declarative template management system for building UIs with CSS-based templates
|
|
|
65
376
|
- Safari 10.1+
|
|
66
377
|
- Edge 79+
|
|
67
378
|
|
|
379
|
+
## Migration from v4
|
|
380
|
+
|
|
381
|
+
UIstate v5 shifts focus from CSS-driven state to event-driven state. Key changes:
|
|
382
|
+
|
|
383
|
+
1. **eventState is now primary** - Import from root or `/eventState`
|
|
384
|
+
2. **cssState still available** - Import from `/cssState`
|
|
385
|
+
3. **New examples** - See `009-financial-dashboard`
|
|
386
|
+
4. **Breaking changes** - Major version bump
|
|
387
|
+
|
|
68
388
|
## Philosophy
|
|
69
389
|
|
|
70
|
-
UIstate challenges traditional assumptions
|
|
390
|
+
UIstate challenges traditional assumptions:
|
|
391
|
+
|
|
392
|
+
- **State should be simple** - Paths + subscriptions, not selectors + reducers
|
|
393
|
+
- **Reactivity should be explicit** - Know what updates what
|
|
394
|
+
- **DOM updates can be fast** - Atomic replacements beat VDOM for many use cases
|
|
395
|
+
- **Components should own their subscriptions** - No global wildcards in production
|
|
396
|
+
- **Frameworks aren't always needed** - Pure JS + patterns can go far
|
|
397
|
+
- **Build steps should be optional** - ESM works in browsers
|
|
398
|
+
- **Intents are just paths** - No special event system needed
|
|
399
|
+
|
|
400
|
+
UIstate v5 represents lessons learned from building:
|
|
401
|
+
- Data tables with 1M+ rows
|
|
402
|
+
- Multi-tab synchronized state
|
|
403
|
+
- Workflowy-style nested lists
|
|
404
|
+
- Financial dashboards
|
|
405
|
+
- Admin panels
|
|
71
406
|
|
|
72
|
-
The
|
|
407
|
+
The core insight: **Most web UIs need reactive state and component orchestration, not a full framework.**
|
|
73
408
|
|
|
74
|
-
##
|
|
409
|
+
## Testing (Experimental)
|
|
75
410
|
|
|
76
|
-
|
|
411
|
+
UIstate includes `eventTest.js` for event-sequence testing:
|
|
77
412
|
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
413
|
+
**Note:** `eventTest.js` is dual-licensed. Free for personal/OSS use. Commercial use requires a separate license. See LICENSE-eventTest.md for details.
|
|
414
|
+
|
|
415
|
+
```javascript
|
|
416
|
+
import { createEventTest } from '@uistate/core/eventTest';
|
|
417
|
+
|
|
418
|
+
const test = createEventTest({ count: 0 });
|
|
419
|
+
|
|
420
|
+
test
|
|
421
|
+
.trigger('intent.increment')
|
|
422
|
+
.assertPath('count', 1)
|
|
423
|
+
.assertEventFired('count', 1);
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
See `examples/010-event-testing` for more.
|
|
82
427
|
|
|
83
428
|
## License
|
|
84
429
|
|
|
85
|
-
MIT
|
|
430
|
+
**@uistate/core** is licensed under the **MIT License**, with an exception, as decribed next.
|
|
431
|
+
|
|
432
|
+
**Exception:** `eventTest.js` is licensed under a **proprietary license** that permits:
|
|
433
|
+
- ✅ Personal use
|
|
434
|
+
- ✅ Open-source projects
|
|
435
|
+
- ✅ Educational use
|
|
436
|
+
|
|
437
|
+
For **commercial use** of `eventTest.js`, please contact: [ajdika@live.com](mailto:ajdika@live.com)
|
|
438
|
+
|
|
439
|
+
See the file header in `eventTest.js` for full terms.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
Copyright © 2025 Ajdin Imsirovic (ajdika@live.com)
|
|
444
|
+
|
|
445
|
+
- Core library: MIT License
|
|
446
|
+
- eventTest.js: Commercial License (free for personal/OSS)
|
|
447
|
+
|
|
448
|
+
## Links
|
|
449
|
+
|
|
450
|
+
- [GitHub](https://github.com/ImsirovicAjdin/uistate)
|
|
451
|
+
- [npm](https://www.npmjs.com/package/@uistate/core)
|
|
452
|
+
- [Issues](https://github.com/ImsirovicAjdin/uistate/issues)
|
package/eventState.js
CHANGED
|
@@ -1,98 +1,87 @@
|
|
|
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
1
|
const createEventState = (initial = {}) => {
|
|
8
|
-
// Clone the initial state to avoid direct mutations to the passed object
|
|
9
2
|
const store = JSON.parse(JSON.stringify(initial));
|
|
10
3
|
|
|
11
|
-
|
|
12
|
-
|
|
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);
|
|
4
|
+
const bus = new EventTarget();
|
|
5
|
+
let destroyed = false;
|
|
18
6
|
|
|
19
7
|
return {
|
|
20
|
-
// get a value from the store by path
|
|
21
8
|
get: (path) => {
|
|
9
|
+
if (destroyed) throw new Error('Cannot get from destroyed store');
|
|
22
10
|
if (!path) return store;
|
|
23
|
-
return path
|
|
24
|
-
.split(".")
|
|
25
|
-
.reduce(
|
|
26
|
-
(obj, prop) =>
|
|
27
|
-
obj && obj[prop] !== undefined ? obj[prop] : undefined,
|
|
28
|
-
store
|
|
29
|
-
);
|
|
30
|
-
},
|
|
31
11
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
12
|
+
// Parse dot-paths: 'items.0' → ['items', '0']
|
|
13
|
+
const parts = path.split('.').flatMap(part => {
|
|
14
|
+
const match = part.match(/([^\[]+)\[(\d+)\]/);
|
|
15
|
+
return match ? [match[1], match[2]] : part;
|
|
16
|
+
});
|
|
35
17
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
18
|
+
return parts.reduce(
|
|
19
|
+
(obj, prop) => (obj && obj[prop] !== undefined ? obj[prop] : undefined),
|
|
20
|
+
store
|
|
21
|
+
);
|
|
22
|
+
},
|
|
23
|
+
set: (path, value) => {
|
|
24
|
+
if (destroyed) throw new Error('Cannot set on destroyed store');
|
|
25
|
+
if (!path) return;
|
|
26
|
+
const parts = path.split('.');
|
|
39
27
|
const last = parts.pop();
|
|
28
|
+
let target = store;
|
|
40
29
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
30
|
+
for (const key of parts) {
|
|
31
|
+
// create intermediate objects as needed
|
|
32
|
+
if (typeof target[key] !== 'object' || target[key] === null) {
|
|
33
|
+
target[key] = {};
|
|
34
|
+
}
|
|
35
|
+
target = target[key];
|
|
36
|
+
}
|
|
48
37
|
|
|
49
|
-
// Set the value
|
|
50
38
|
target[last] = value;
|
|
51
39
|
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
if (!destroyed) {
|
|
41
|
+
// exact path
|
|
42
|
+
bus.dispatchEvent(new CustomEvent(path, { detail: value }));
|
|
54
43
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
parentPath = parentPath ? `${parentPath}.${part}` : part;
|
|
60
|
-
bus.dispatchEvent(
|
|
61
|
-
new CustomEvent(`${parentPath}.*`, {
|
|
62
|
-
detail: { path, value },
|
|
63
|
-
})
|
|
64
|
-
);
|
|
44
|
+
// parent wildcards: a, a.b -> 'a.*', 'a.b.*'
|
|
45
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
46
|
+
const parent = parts.slice(0, i).join('.');
|
|
47
|
+
bus.dispatchEvent(new CustomEvent(`${parent}.*`, { detail: { path, value } }));
|
|
65
48
|
}
|
|
66
49
|
|
|
67
|
-
//
|
|
68
|
-
bus.dispatchEvent(
|
|
69
|
-
new CustomEvent("*", {
|
|
70
|
-
detail: { path, value},
|
|
71
|
-
})
|
|
72
|
-
);
|
|
50
|
+
// root wildcard
|
|
51
|
+
bus.dispatchEvent(new CustomEvent('*', { detail: { path, value } }));
|
|
73
52
|
}
|
|
74
53
|
|
|
75
54
|
return value;
|
|
76
55
|
},
|
|
56
|
+
subscribe(path, handler) {
|
|
57
|
+
if (destroyed) throw new Error('store destroyed');
|
|
58
|
+
if (!path || typeof handler !== 'function') {
|
|
59
|
+
throw new TypeError('subscribe(path, handler) requires a string path and function handler');
|
|
60
|
+
}
|
|
61
|
+
const onEvent = (evt) => handler(evt.detail, evt);
|
|
62
|
+
bus.addEventListener(path, onEvent);
|
|
77
63
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const handler = (e) => callback(e.detail, path);
|
|
83
|
-
bus.addEventListener(path, handler);
|
|
84
|
-
|
|
85
|
-
return () => bus.removeEventListener(path, handler);
|
|
64
|
+
return function unsubscribe() {
|
|
65
|
+
bus.removeEventListener(path, onEvent);
|
|
66
|
+
};
|
|
86
67
|
},
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
bus.parentNode.removeChild(bus);
|
|
68
|
+
addStateListener: subscribe,
|
|
69
|
+
off(unsubscribe) {
|
|
70
|
+
if (typeof unsubscribe !== 'function') {
|
|
71
|
+
throw new TypeError('off(unsubscribe) requires a function returned by subscribe');
|
|
92
72
|
}
|
|
73
|
+
return unsubscribe();
|
|
93
74
|
},
|
|
75
|
+
|
|
76
|
+
destroy() {
|
|
77
|
+
if (!destroyed) {
|
|
78
|
+
destroyed = true;
|
|
79
|
+
// EventTarget has no parentNode - just mark as destroyed
|
|
80
|
+
// Future sets/subscribes will be blocked by destroyed flag
|
|
81
|
+
}
|
|
82
|
+
}
|
|
94
83
|
};
|
|
95
|
-
}
|
|
84
|
+
}
|
|
96
85
|
|
|
97
86
|
export default createEventState;
|
|
98
87
|
export { createEventState };
|