@uistate/core 4.1.2 → 5.0.1
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/cssState.js +32 -1
- package/eventState.js +60 -49
- 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 +14 -3
- package/package.json +16 -7
- package/stateSerializer.js +99 -4
- package/templateManager.js +50 -2
|
@@ -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/cssState.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UIstate - CSS-based state management module with integrated serialization
|
|
3
|
+
* Part of the UIstate declarative state management system
|
|
4
|
+
* Uses CSS custom properties and data attributes for state representation
|
|
5
|
+
* Features modular extension capabilities for DOM binding and events
|
|
6
|
+
*/
|
|
1
7
|
import StateSerializer from './stateSerializer.js';
|
|
2
8
|
|
|
3
9
|
const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
@@ -6,7 +12,7 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
6
12
|
_observers: new Map(),
|
|
7
13
|
_serializer: serializer,
|
|
8
14
|
_specialHandlers: {},
|
|
9
|
-
_eventHandlers: new Map(),
|
|
15
|
+
_eventHandlers: new Map(), // Store custom event binding handlers
|
|
10
16
|
|
|
11
17
|
init(serializerConfig) {
|
|
12
18
|
if (!this._sheet) {
|
|
@@ -16,10 +22,12 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
16
22
|
this._addRule(':root {}');
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
// Configure serializer if options provided
|
|
19
26
|
if (serializerConfig && typeof serializerConfig === 'object') {
|
|
20
27
|
this._serializer.configure(serializerConfig);
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
// Initialize with any provided state
|
|
23
31
|
if (initialState && typeof initialState === 'object') {
|
|
24
32
|
Object.entries(initialState).forEach(([key, value]) => {
|
|
25
33
|
this.setState(key, value);
|
|
@@ -30,11 +38,14 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
30
38
|
},
|
|
31
39
|
|
|
32
40
|
setState(key, value) {
|
|
41
|
+
// Use serializer for CSS variables
|
|
33
42
|
const cssValue = this._serializer.serialize(key, value);
|
|
34
43
|
document.documentElement.style.setProperty(`--${key}`, cssValue);
|
|
35
44
|
|
|
45
|
+
// Use serializer to handle all attribute application consistently
|
|
36
46
|
this._serializer.applyToAttributes(key, value);
|
|
37
47
|
|
|
48
|
+
// Notify any registered observers of the state change
|
|
38
49
|
this._notifyObservers(key, value);
|
|
39
50
|
return value;
|
|
40
51
|
},
|
|
@@ -50,6 +61,7 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
50
61
|
const value = getComputedStyle(document.documentElement).getPropertyValue(`--${key}`).trim();
|
|
51
62
|
if (!value) return '';
|
|
52
63
|
|
|
64
|
+
// Use serializer for deserialization
|
|
53
65
|
return this._serializer.deserialize(key, value);
|
|
54
66
|
},
|
|
55
67
|
|
|
@@ -78,6 +90,7 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
78
90
|
return this;
|
|
79
91
|
},
|
|
80
92
|
|
|
93
|
+
// New method for registering event bindings
|
|
81
94
|
registerEventBinding(eventType, handler) {
|
|
82
95
|
this._eventHandlers.set(eventType, handler);
|
|
83
96
|
return this;
|
|
@@ -88,21 +101,27 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
88
101
|
const stateKey = el.dataset.observe;
|
|
89
102
|
|
|
90
103
|
this.observe(stateKey, (value) => {
|
|
104
|
+
// Special handlers should run first to set data-state
|
|
91
105
|
if (this._specialHandlers[stateKey]?.observe) {
|
|
92
106
|
this._specialHandlers[stateKey].observe(value, el);
|
|
93
107
|
} else if (stateKey.endsWith('-state') && el.hasAttribute('data-state')) {
|
|
108
|
+
// Only update data-state for elements that already have this attribute
|
|
94
109
|
el.dataset.state = value;
|
|
95
110
|
} else {
|
|
111
|
+
// For normal state observers like theme, counter, etc.
|
|
96
112
|
el.textContent = value;
|
|
97
113
|
}
|
|
98
114
|
});
|
|
99
115
|
|
|
116
|
+
// Trigger initial state
|
|
100
117
|
const initialValue = this.getState(stateKey);
|
|
101
118
|
if (this._specialHandlers[stateKey]?.observe) {
|
|
102
119
|
this._specialHandlers[stateKey].observe(initialValue, el);
|
|
103
120
|
} else if (stateKey.endsWith('-state') && el.hasAttribute('data-state')) {
|
|
121
|
+
// Only set data-state for elements that should have this attribute
|
|
104
122
|
el.dataset.state = initialValue;
|
|
105
123
|
} else {
|
|
124
|
+
// For normal elements
|
|
106
125
|
el.textContent = initialValue;
|
|
107
126
|
}
|
|
108
127
|
|
|
@@ -112,6 +131,7 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
112
131
|
return this;
|
|
113
132
|
},
|
|
114
133
|
|
|
134
|
+
// Default event handlers available for implementations to use
|
|
115
135
|
defaultClickHandler(e) {
|
|
116
136
|
const target = e.target.closest('[data-state-action]');
|
|
117
137
|
if (!target) return;
|
|
@@ -119,11 +139,13 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
119
139
|
const stateAction = target.dataset.stateAction;
|
|
120
140
|
if (!stateAction) return;
|
|
121
141
|
|
|
142
|
+
// Special handlers get first priority
|
|
122
143
|
if (this._specialHandlers[stateAction]?.action) {
|
|
123
144
|
this._specialHandlers[stateAction].action(target);
|
|
124
145
|
return;
|
|
125
146
|
}
|
|
126
147
|
|
|
148
|
+
// Handle direct value setting via data-state-value
|
|
127
149
|
if (target.dataset.stateValue !== undefined) {
|
|
128
150
|
const valueToSet = target.dataset.stateValue;
|
|
129
151
|
this.setState(stateAction, valueToSet);
|
|
@@ -136,16 +158,20 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
136
158
|
|
|
137
159
|
if (!stateAction) return;
|
|
138
160
|
|
|
161
|
+
// Special handlers should access any needed data directly from the target
|
|
139
162
|
if (this._specialHandlers[stateAction]?.action) {
|
|
140
163
|
this._specialHandlers[stateAction].action(target);
|
|
141
164
|
}
|
|
142
165
|
},
|
|
143
166
|
|
|
167
|
+
// Updated setupStateActions to use registered event handlers
|
|
144
168
|
setupStateActions(container = document) {
|
|
169
|
+
// Only bind the registered event types
|
|
145
170
|
this._eventHandlers.forEach((handler, eventType) => {
|
|
146
171
|
container.addEventListener(eventType, handler);
|
|
147
172
|
});
|
|
148
173
|
|
|
174
|
+
// If no event handlers registered, register the default ones
|
|
149
175
|
if (this._eventHandlers.size === 0) {
|
|
150
176
|
container.addEventListener('click', (e) => this.defaultClickHandler(e));
|
|
151
177
|
container.addEventListener('input', (e) => this.defaultInputHandler(e));
|
|
@@ -160,6 +186,7 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
160
186
|
}
|
|
161
187
|
},
|
|
162
188
|
|
|
189
|
+
// Add serializer configuration method
|
|
163
190
|
configureSerializer(config) {
|
|
164
191
|
if (this._serializer.configure) {
|
|
165
192
|
this._serializer.configure(config);
|
|
@@ -167,14 +194,18 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
|
167
194
|
return this;
|
|
168
195
|
},
|
|
169
196
|
|
|
197
|
+
// Clean up resources
|
|
170
198
|
destroy() {
|
|
171
199
|
this._observers.clear();
|
|
200
|
+
// The style element will remain in the DOM
|
|
201
|
+
// as removing it would affect the UI state
|
|
172
202
|
}
|
|
173
203
|
};
|
|
174
204
|
|
|
175
205
|
return state.init();
|
|
176
206
|
};
|
|
177
207
|
|
|
208
|
+
// Create a singleton instance for easy usage
|
|
178
209
|
const UIstate = createCssState();
|
|
179
210
|
|
|
180
211
|
export { createCssState };
|