@uistate/core 5.5.0 → 5.5.2

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 (2) hide show
  1. package/README.md +58 -386
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,21 +1,11 @@
1
- # UIstate v5
1
+ # @uistate/core
2
2
 
3
- **Lightweight event-driven state management for modern web applications**
3
+ Path-based state management with wildcard subscriptions and async support.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@uistate/core.svg)](https://www.npmjs.com/package/@uistate/core)
6
- [![License: MIT + Commercial](https://img.shields.io/badge/License-MIT%20%2B%20Commercial-blue.svg)](#license)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
7
7
 
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)
17
-
18
- ## Installation
8
+ ## Install
19
9
 
20
10
  ```bash
21
11
  npm install @uistate/core
@@ -26,427 +16,109 @@ npm install @uistate/core
26
16
  ```javascript
27
17
  import { createEventState } from '@uistate/core';
28
18
 
29
- const store = createEventState({
30
- user: { name: 'Alice' },
31
- count: 0
32
- });
19
+ const store = createEventState({ user: { name: 'Alice' }, count: 0 });
33
20
 
34
- // Subscribe to specific paths
35
- store.subscribe('user.name', (value) => {
36
- console.log('Name changed:', value);
37
- });
21
+ // Exact path
22
+ store.subscribe('count', (value) => console.log('Count:', value));
38
23
 
39
- // Subscribe with wildcards
40
- store.subscribe('user.*', ({ path, value }) => {
41
- console.log(`${path} changed to:`, value);
42
- });
24
+ // Wildcard fires on any child of 'user'
25
+ store.subscribe('user.*', ({ path, value }) => console.log(path, value));
26
+
27
+ // Global — fires on every change
28
+ store.subscribe('*', ({ path, value }) => console.log(path, value));
43
29
 
44
- // Update state
45
- store.set('user.name', 'Bob'); // → Name changed: Bob
30
+ store.set('user.name', 'Bob');
46
31
  store.set('count', 1);
47
32
  ```
48
33
 
49
- ## Core API
50
-
51
- ### `createEventState(initial?)`
34
+ ## API
52
35
 
53
- Creates a new reactive store.
54
-
55
- ```javascript
56
- const store = createEventState({ count: 0 });
57
- ```
36
+ ### `createEventState(initialState)`
58
37
 
59
- ### `store.get(path)`
38
+ Returns a store with `get`, `set`, `subscribe`, `setAsync`, `cancel`, `destroy`.
60
39
 
61
- Retrieves a value by path.
40
+ ### `store.get(path?)`
62
41
 
63
42
  ```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
43
+ store.get('user.name'); // 'Alice'
44
+ store.get('user'); // { name: 'Alice' }
45
+ store.get(); // entire state
67
46
  ```
68
47
 
69
48
  ### `store.set(path, value)`
70
49
 
71
- Sets a value and triggers subscriptions.
50
+ Sets a value and notifies subscribers. Creates intermediate objects if needed.
72
51
 
73
52
  ```javascript
74
- store.set('count', 42);
75
53
  store.set('user.email', 'alice@example.com');
54
+ // state.user.email now exists even if it didn't before
76
55
  ```
77
56
 
78
57
  ### `store.subscribe(path, handler)`
79
58
 
80
- Subscribes to changes. Returns an unsubscribe function.
59
+ Returns an unsubscribe function. Exact subscribers get `(value, detail)`. Wildcard/global subscribers get `(detail)` where detail is `{ path, value, oldValue }`.
81
60
 
82
61
  ```javascript
83
- // Exact path
84
- const unsub = store.subscribe('count', (value) => {
85
- console.log('Count:', value);
86
- });
87
-
88
- // Wildcard (child changes)
89
- store.subscribe('user.*', ({ path, value }) => {
90
- console.log(`User property ${path} changed`);
62
+ const unsub = store.subscribe('count', (value, { oldValue }) => {
63
+ console.log(`${oldValue} → ${value}`);
91
64
  });
92
-
93
- // Global wildcard (all changes)
94
- store.subscribe('*', ({ path, value }) => {
95
- console.log('Something changed');
96
- });
97
-
98
- // Cleanup
99
- unsub();
65
+ unsub(); // cleanup
100
66
  ```
101
67
 
102
- ### `store.destroy()`
68
+ ### `store.setAsync(path, fetcher)`
103
69
 
104
- Cleans up the event bus and prevents further updates.
70
+ Manages async state at `${path}.status`, `${path}.data`, `${path}.error`. Supports abort on re-call.
105
71
 
106
72
  ```javascript
107
- store.destroy();
108
- ```
109
-
110
- # Changelog
111
-
112
- ## [5.0.0] - 2025-12-09
113
-
114
- ### Breaking Changes
115
- - eventState is now primary export (was cssState)
116
- - DOM element event bus replaced with EventTarget
117
- - ...
118
-
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
125
-
126
- ## Architecture Patterns
127
-
128
- UIstate v5 introduces battle-tested patterns from many experiments:
129
-
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 });
73
+ await store.setAsync('users', async (signal) => {
74
+ const res = await fetch('/api/users', { signal });
75
+ return res.json();
144
76
  });
77
+ // store.get('users.data') → [...]
78
+ // store.get('users.status') → 'success'
145
79
  ```
146
80
 
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)
81
+ ### `store.cancel(path)` / `store.destroy()`
261
82
 
262
- One layout HTML can adapt to different use cases by subscribing to state and toggling CSS classes:
83
+ Cancel an in-flight async operation, or tear down the entire store.
263
84
 
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
- ```
85
+ ## Query Client
290
86
 
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
87
+ Convenience wrapper around `setAsync` for data-fetching patterns.
297
88
 
298
- **Real-world applications**:
299
89
  ```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 .
90
+ import { createQueryClient } from '@uistate/core/query';
91
+
92
+ const qc = createQueryClient(store);
93
+ await qc.query('users', (signal) => fetch('/api/users', { signal }).then(r => r.json()));
94
+ qc.getData('users');
95
+ qc.getStatus('users');
96
+ qc.cancel('users');
97
+ qc.invalidate('users');
328
98
  ```
329
99
 
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:
100
+ ## Ecosystem
364
101
 
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
102
+ | Package | Description | License |
103
+ |---|---|---|
104
+ | [@uistate/core](https://www.npmjs.com/package/@uistate/core) | Path-based state management with wildcard subscriptions and async support | MIT |
105
+ | [@uistate/react](https://www.npmjs.com/package/@uistate/react) | React adapter `usePath`, `useIntent`, `useAsync` hooks and `EventStateProvider` | MIT |
106
+ | [@uistate/css](https://www.npmjs.com/package/@uistate/css) | CSS-native state via custom properties and data attributes | MIT |
107
+ | [@uistate/event-test](https://www.npmjs.com/package/@uistate/event-test) | Event-sequence testing for UIstate stores | Proprietary |
108
+ | [@uistate/examples](https://www.npmjs.com/package/@uistate/examples) | Example applications and patterns | MIT |
109
+ | [@uistate/aliases](https://www.npmjs.com/package/@uistate/aliases) | Ergonomic single-character and short-name DOM aliases for vanilla JS | MIT |
371
110
 
372
- ## Browser Support
373
-
374
- - Chrome 60+
375
- - Firefox 54+
376
- - Safari 10.1+
377
- - Edge 79+
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
-
388
- ## Philosophy
389
-
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
406
-
407
- The core insight: **Most web UIs need reactive state and component orchestration, not a full framework.**
408
-
409
- ## Testing (Experimental)
410
-
411
- UIstate includes `eventTest.js` for event-sequence testing:
412
-
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.
111
+ 📖 **Documentation:** [uistate.com](https://uistate.com)
427
112
 
428
113
  ## License
429
114
 
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)
115
+ MIT see [LICENSE](./LICENSE).
444
116
 
445
- - Core library: MIT License
446
- - eventTest.js: Commercial License (free for personal/OSS)
117
+ Copyright © 2025 Ajdin Imsirovic
447
118
 
448
119
  ## Links
449
120
 
121
+ - [Documentation](https://uistate.com)
450
122
  - [GitHub](https://github.com/ImsirovicAjdin/uistate)
451
123
  - [npm](https://www.npmjs.com/package/@uistate/core)
452
124
  - [Issues](https://github.com/ImsirovicAjdin/uistate/issues)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uistate/core",
3
- "version": "5.5.0",
3
+ "version": "5.5.2",
4
4
  "description": "Lightweight event-driven state management with path-based subscriptions, wildcards, and async support",
5
5
  "type": "module",
6
6
  "main": "index.js",