@uistate/core 5.3.0 → 5.5.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/index.js +2 -9
- package/package.json +5 -13
- package/LICENSE-eventTest.md +0 -26
- package/cssState.js +0 -212
- package/eventTest.js +0 -196
- package/stateSerializer.js +0 -267
- package/templateManager.js +0 -216
package/index.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* UIstate v5 - Core barrel exports
|
|
3
3
|
*
|
|
4
|
-
* EventState is
|
|
5
|
-
*
|
|
4
|
+
* EventState is the primary export for application state management.
|
|
5
|
+
* CSS state management has moved to @uistate/css.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// Primary: EventState (recommended for application state)
|
|
9
9
|
export { createEventState } from './eventStateNew.js';
|
|
10
10
|
export { createEventState as default } from './eventStateNew.js';
|
|
11
|
-
|
|
12
|
-
// Specialized: CSS State (for CSS variables and theme management)
|
|
13
|
-
export { createCssState } from './cssState.js';
|
|
14
|
-
|
|
15
|
-
// Utilities
|
|
16
|
-
export { default as stateSerializer } from './stateSerializer.js';
|
|
17
|
-
export { createTemplateManager } from './templateManager.js';
|
package/package.json
CHANGED
|
@@ -1,29 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uistate/core",
|
|
3
|
-
"version": "5.
|
|
4
|
-
"description": "Lightweight event-driven state management with
|
|
3
|
+
"version": "5.5.0",
|
|
4
|
+
"description": "Lightweight event-driven state management with path-based subscriptions, wildcards, and async support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.js",
|
|
9
9
|
"./eventState": "./eventState.js",
|
|
10
10
|
"./eventStateNew": "./eventStateNew.js",
|
|
11
|
-
"./cssState": "./cssState.js",
|
|
12
|
-
"./stateSerializer": "./stateSerializer.js",
|
|
13
|
-
"./templateManager": "./templateManager.js",
|
|
14
11
|
"./query": "./queryClient.js"
|
|
15
12
|
},
|
|
16
13
|
"files": [
|
|
17
14
|
"index.js",
|
|
18
15
|
"eventState.js",
|
|
19
16
|
"eventStateNew.js",
|
|
20
|
-
"cssState.js",
|
|
21
|
-
"stateSerializer.js",
|
|
22
|
-
"templateManager.js",
|
|
23
17
|
"queryClient.js",
|
|
24
|
-
"
|
|
25
|
-
"LICENSE",
|
|
26
|
-
"LICENSE-eventTest.md"
|
|
18
|
+
"LICENSE"
|
|
27
19
|
],
|
|
28
20
|
"keywords": [
|
|
29
21
|
"state-management",
|
|
@@ -34,8 +26,8 @@
|
|
|
34
26
|
"zero-dependency",
|
|
35
27
|
"framework-free",
|
|
36
28
|
"micro-framework",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
29
|
+
"async-state",
|
|
30
|
+
"query-client"
|
|
39
31
|
],
|
|
40
32
|
"author": "Ajdin Imsirovic",
|
|
41
33
|
"license": "MIT",
|
package/LICENSE-eventTest.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
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/cssState.js
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
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
|
-
*/
|
|
7
|
-
import StateSerializer from './stateSerializer.js';
|
|
8
|
-
|
|
9
|
-
const createCssState = (initialState = {}, serializer = StateSerializer) => {
|
|
10
|
-
const state = {
|
|
11
|
-
_sheet: null,
|
|
12
|
-
_observers: new Map(),
|
|
13
|
-
_serializer: serializer,
|
|
14
|
-
_specialHandlers: {},
|
|
15
|
-
_eventHandlers: new Map(), // Store custom event binding handlers
|
|
16
|
-
|
|
17
|
-
init(serializerConfig) {
|
|
18
|
-
if (!this._sheet) {
|
|
19
|
-
const style = document.createElement('style');
|
|
20
|
-
document.head.appendChild(style);
|
|
21
|
-
this._sheet = style.sheet;
|
|
22
|
-
this._addRule(':root {}');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Configure serializer if options provided
|
|
26
|
-
if (serializerConfig && typeof serializerConfig === 'object') {
|
|
27
|
-
this._serializer.configure(serializerConfig);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Initialize with any provided state
|
|
31
|
-
if (initialState && typeof initialState === 'object') {
|
|
32
|
-
Object.entries(initialState).forEach(([key, value]) => {
|
|
33
|
-
this.setState(key, value);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return this;
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
setState(key, value) {
|
|
41
|
-
// Use serializer for CSS variables
|
|
42
|
-
const cssValue = this._serializer.serialize(key, value);
|
|
43
|
-
document.documentElement.style.setProperty(`--${key}`, cssValue);
|
|
44
|
-
|
|
45
|
-
// Use serializer to handle all attribute application consistently
|
|
46
|
-
this._serializer.applyToAttributes(key, value);
|
|
47
|
-
|
|
48
|
-
// Notify any registered observers of the state change
|
|
49
|
-
this._notifyObservers(key, value);
|
|
50
|
-
return value;
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
setStates(stateObject) {
|
|
54
|
-
Object.entries(stateObject).forEach(([key, value]) => {
|
|
55
|
-
this.setState(key, value);
|
|
56
|
-
});
|
|
57
|
-
return this;
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
getState(key) {
|
|
61
|
-
const value = getComputedStyle(document.documentElement).getPropertyValue(`--${key}`).trim();
|
|
62
|
-
if (!value) return '';
|
|
63
|
-
|
|
64
|
-
// Use serializer for deserialization
|
|
65
|
-
return this._serializer.deserialize(key, value);
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
observe(key, callback) {
|
|
69
|
-
if (!this._observers.has(key)) {
|
|
70
|
-
this._observers.set(key, new Set());
|
|
71
|
-
}
|
|
72
|
-
this._observers.get(key).add(callback);
|
|
73
|
-
return () => {
|
|
74
|
-
const observers = this._observers.get(key);
|
|
75
|
-
if (observers) {
|
|
76
|
-
observers.delete(callback);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
_notifyObservers(key, value) {
|
|
82
|
-
const observers = this._observers.get(key);
|
|
83
|
-
if (observers) {
|
|
84
|
-
observers.forEach(cb => cb(value));
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
registerSpecialHandler(stateKey, handlerFn) {
|
|
89
|
-
this._specialHandlers[stateKey] = handlerFn;
|
|
90
|
-
return this;
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
// New method for registering event bindings
|
|
94
|
-
registerEventBinding(eventType, handler) {
|
|
95
|
-
this._eventHandlers.set(eventType, handler);
|
|
96
|
-
return this;
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
setupObservers(container = document) {
|
|
100
|
-
container.querySelectorAll('[data-observe]:not([data-observing])').forEach(el => {
|
|
101
|
-
const stateKey = el.dataset.observe;
|
|
102
|
-
|
|
103
|
-
this.observe(stateKey, (value) => {
|
|
104
|
-
// Special handlers should run first to set data-state
|
|
105
|
-
if (this._specialHandlers[stateKey]?.observe) {
|
|
106
|
-
this._specialHandlers[stateKey].observe(value, el);
|
|
107
|
-
} else if (stateKey.endsWith('-state') && el.hasAttribute('data-state')) {
|
|
108
|
-
// Only update data-state for elements that already have this attribute
|
|
109
|
-
el.dataset.state = value;
|
|
110
|
-
} else {
|
|
111
|
-
// For normal state observers like theme, counter, etc.
|
|
112
|
-
el.textContent = value;
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Trigger initial state
|
|
117
|
-
const initialValue = this.getState(stateKey);
|
|
118
|
-
if (this._specialHandlers[stateKey]?.observe) {
|
|
119
|
-
this._specialHandlers[stateKey].observe(initialValue, el);
|
|
120
|
-
} else if (stateKey.endsWith('-state') && el.hasAttribute('data-state')) {
|
|
121
|
-
// Only set data-state for elements that should have this attribute
|
|
122
|
-
el.dataset.state = initialValue;
|
|
123
|
-
} else {
|
|
124
|
-
// For normal elements
|
|
125
|
-
el.textContent = initialValue;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
el.dataset.observing = 'true';
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
return this;
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
// Default event handlers available for implementations to use
|
|
135
|
-
defaultClickHandler(e) {
|
|
136
|
-
const target = e.target.closest('[data-state-action]');
|
|
137
|
-
if (!target) return;
|
|
138
|
-
|
|
139
|
-
const stateAction = target.dataset.stateAction;
|
|
140
|
-
if (!stateAction) return;
|
|
141
|
-
|
|
142
|
-
// Special handlers get first priority
|
|
143
|
-
if (this._specialHandlers[stateAction]?.action) {
|
|
144
|
-
this._specialHandlers[stateAction].action(target);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Handle direct value setting via data-state-value
|
|
149
|
-
if (target.dataset.stateValue !== undefined) {
|
|
150
|
-
const valueToSet = target.dataset.stateValue;
|
|
151
|
-
this.setState(stateAction, valueToSet);
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
defaultInputHandler(e) {
|
|
156
|
-
const target = e.target;
|
|
157
|
-
const stateAction = target.dataset.stateAction;
|
|
158
|
-
|
|
159
|
-
if (!stateAction) return;
|
|
160
|
-
|
|
161
|
-
// Special handlers should access any needed data directly from the target
|
|
162
|
-
if (this._specialHandlers[stateAction]?.action) {
|
|
163
|
-
this._specialHandlers[stateAction].action(target);
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
// Updated setupStateActions to use registered event handlers
|
|
168
|
-
setupStateActions(container = document) {
|
|
169
|
-
// Only bind the registered event types
|
|
170
|
-
this._eventHandlers.forEach((handler, eventType) => {
|
|
171
|
-
container.addEventListener(eventType, handler);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// If no event handlers registered, register the default ones
|
|
175
|
-
if (this._eventHandlers.size === 0) {
|
|
176
|
-
container.addEventListener('click', (e) => this.defaultClickHandler(e));
|
|
177
|
-
container.addEventListener('input', (e) => this.defaultInputHandler(e));
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return this;
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
_addRule(rule) {
|
|
184
|
-
if (this._sheet) {
|
|
185
|
-
this._sheet.insertRule(rule, this._sheet.cssRules.length);
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
|
|
189
|
-
// Add serializer configuration method
|
|
190
|
-
configureSerializer(config) {
|
|
191
|
-
if (this._serializer.configure) {
|
|
192
|
-
this._serializer.configure(config);
|
|
193
|
-
}
|
|
194
|
-
return this;
|
|
195
|
-
},
|
|
196
|
-
|
|
197
|
-
// Clean up resources
|
|
198
|
-
destroy() {
|
|
199
|
-
this._observers.clear();
|
|
200
|
-
// The style element will remain in the DOM
|
|
201
|
-
// as removing it would affect the UI state
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
return state.init();
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
// Create a singleton instance for easy usage
|
|
209
|
-
const UIstate = createCssState();
|
|
210
|
-
|
|
211
|
-
export { createCssState };
|
|
212
|
-
export default UIstate;
|
package/eventTest.js
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* eventTest.js - Event-Sequence Testing for UIstate
|
|
3
|
-
*
|
|
4
|
-
* Copyright (c) 2025 Ajdin Imsirovic
|
|
5
|
-
*
|
|
6
|
-
* This file is licensed under a PROPRIETARY LICENSE.
|
|
7
|
-
*
|
|
8
|
-
* Permission is hereby granted to USE this software for:
|
|
9
|
-
* - Personal projects
|
|
10
|
-
* - Open-source projects
|
|
11
|
-
* - Educational purposes
|
|
12
|
-
*
|
|
13
|
-
* RESTRICTIONS:
|
|
14
|
-
* - Commercial use requires a separate license (contact: your@email.com)
|
|
15
|
-
* - Modification and redistribution of this file are NOT permitted
|
|
16
|
-
* - This file may not be included in derivative works without permission
|
|
17
|
-
*
|
|
18
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
|
19
|
-
*
|
|
20
|
-
* For commercial licensing inquiries: your@email.com
|
|
21
|
-
*
|
|
22
|
-
* eventTest.js - Event-Sequence Testing for EventState
|
|
23
|
-
*
|
|
24
|
-
* Provides TDD-style testing with type extraction capabilities
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { createEventState } from './eventStateNew.js';
|
|
28
|
-
|
|
29
|
-
export function createEventTest(initialState = {}) {
|
|
30
|
-
const store = createEventState(initialState);
|
|
31
|
-
const eventLog = [];
|
|
32
|
-
const typeAssertions = [];
|
|
33
|
-
|
|
34
|
-
// Spy on all events
|
|
35
|
-
store.subscribe('*', (detail) => {
|
|
36
|
-
const { path, value } = detail;
|
|
37
|
-
eventLog.push({ timestamp: Date.now(), path, value });
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const api = {
|
|
41
|
-
store,
|
|
42
|
-
|
|
43
|
-
// Trigger a state change
|
|
44
|
-
trigger(path, value) {
|
|
45
|
-
store.set(path, value);
|
|
46
|
-
return this;
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
// Assert exact value
|
|
50
|
-
assertPath(path, expected) {
|
|
51
|
-
const actual = store.get(path);
|
|
52
|
-
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
53
|
-
throw new Error(`Expected ${path} to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
54
|
-
}
|
|
55
|
-
return this;
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
// Assert type (for type generation)
|
|
59
|
-
assertType(path, expectedType) {
|
|
60
|
-
const actual = store.get(path);
|
|
61
|
-
const actualType = typeof actual;
|
|
62
|
-
|
|
63
|
-
if (actualType !== expectedType) {
|
|
64
|
-
throw new Error(`Expected ${path} to be type ${expectedType}, got ${actualType}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Store for type generation
|
|
68
|
-
typeAssertions.push({ path, type: expectedType });
|
|
69
|
-
return this;
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
// Assert array with element shape (for type generation)
|
|
73
|
-
assertArrayOf(path, elementShape) {
|
|
74
|
-
const actual = store.get(path);
|
|
75
|
-
|
|
76
|
-
if (!Array.isArray(actual)) {
|
|
77
|
-
throw new Error(`Expected ${path} to be an array, got ${typeof actual}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Validate first element matches shape (if array not empty)
|
|
81
|
-
if (actual.length > 0) {
|
|
82
|
-
validateShape(actual[0], elementShape, path);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Store for type generation
|
|
86
|
-
typeAssertions.push({ path, type: 'array', elementShape });
|
|
87
|
-
return this;
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
// Assert object shape (for type generation)
|
|
91
|
-
assertShape(path, objectShape) {
|
|
92
|
-
const actual = store.get(path);
|
|
93
|
-
|
|
94
|
-
if (typeof actual !== 'object' || actual === null || Array.isArray(actual)) {
|
|
95
|
-
throw new Error(`Expected ${path} to be an object, got ${typeof actual}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
validateShape(actual, objectShape, path);
|
|
99
|
-
|
|
100
|
-
// Store for type generation
|
|
101
|
-
typeAssertions.push({ path, type: 'object', shape: objectShape });
|
|
102
|
-
return this;
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
// Assert array length
|
|
106
|
-
assertArrayLength(path, expectedLength) {
|
|
107
|
-
const actual = store.get(path);
|
|
108
|
-
|
|
109
|
-
if (!Array.isArray(actual)) {
|
|
110
|
-
throw new Error(`Expected ${path} to be an array`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (actual.length !== expectedLength) {
|
|
114
|
-
throw new Error(`Expected ${path} to have length ${expectedLength}, got ${actual.length}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return this;
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
// Assert event fired N times
|
|
121
|
-
assertEventFired(path, times) {
|
|
122
|
-
const count = eventLog.filter(e => e.path === path).length;
|
|
123
|
-
if (times !== undefined && count !== times) {
|
|
124
|
-
throw new Error(`Expected ${path} to fire ${times} times, fired ${count}`);
|
|
125
|
-
}
|
|
126
|
-
return this;
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
// Get event log
|
|
130
|
-
getEventLog() {
|
|
131
|
-
return [...eventLog];
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
// Get type assertions (for type generation)
|
|
135
|
-
getTypeAssertions() {
|
|
136
|
-
return [...typeAssertions];
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
return api;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Helper to validate object shape
|
|
144
|
-
function validateShape(actual, shape, path) {
|
|
145
|
-
for (const [key, expectedType] of Object.entries(shape)) {
|
|
146
|
-
if (!(key in actual)) {
|
|
147
|
-
throw new Error(`Expected ${path} to have property ${key}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const actualValue = actual[key];
|
|
151
|
-
|
|
152
|
-
// Handle nested objects
|
|
153
|
-
if (typeof expectedType === 'object' && !Array.isArray(expectedType)) {
|
|
154
|
-
validateShape(actualValue, expectedType, `${path}.${key}`);
|
|
155
|
-
} else {
|
|
156
|
-
// Primitive type check
|
|
157
|
-
const actualType = typeof actualValue;
|
|
158
|
-
if (actualType !== expectedType) {
|
|
159
|
-
throw new Error(`Expected ${path}.${key} to be type ${expectedType}, got ${actualType}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Simple test runner
|
|
166
|
-
export function test(name, fn) {
|
|
167
|
-
try {
|
|
168
|
-
fn();
|
|
169
|
-
console.log(`✓ ${name}`);
|
|
170
|
-
return true;
|
|
171
|
-
} catch (error) {
|
|
172
|
-
console.error(`✗ ${name}`);
|
|
173
|
-
console.error(` ${error.message}`);
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Run multiple tests
|
|
179
|
-
export function runTests(tests) {
|
|
180
|
-
console.log('\n🧪 Running tests...\n');
|
|
181
|
-
|
|
182
|
-
let passed = 0;
|
|
183
|
-
let failed = 0;
|
|
184
|
-
|
|
185
|
-
for (const [name, fn] of Object.entries(tests)) {
|
|
186
|
-
if (test(name, fn)) {
|
|
187
|
-
passed++;
|
|
188
|
-
} else {
|
|
189
|
-
failed++;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
console.log(`\n📊 Results: ${passed} passed, ${failed} failed\n`);
|
|
194
|
-
|
|
195
|
-
return { passed, failed };
|
|
196
|
-
}
|
package/stateSerializer.js
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* StateSerializer - Configurable serialization module for UIstate
|
|
3
|
-
* Handles transformation between JavaScript values and CSS-compatible string values
|
|
4
|
-
*
|
|
5
|
-
* Supports multiple serialization strategies:
|
|
6
|
-
* - 'escape': Uses custom escaping for all values (original UIstate approach)
|
|
7
|
-
* - 'json': Uses JSON.stringify for complex objects, direct values for primitives
|
|
8
|
-
* - 'hybrid': Automatically selects the best strategy based on value type
|
|
9
|
-
*
|
|
10
|
-
* Also handles serialization of values for data attributes and CSS variables
|
|
11
|
-
* with consistent rules and unified serialization behavior
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
// Utility functions for CSS value escaping/unescaping
|
|
15
|
-
function escapeCssValue(value) {
|
|
16
|
-
if (typeof value !== 'string') return value;
|
|
17
|
-
return value.replace(/[^\x20-\x7E]|[!;{}:()[\]/@,'"]/g, function(char) {
|
|
18
|
-
const hex = char.charCodeAt(0).toString(16);
|
|
19
|
-
return '\\' + hex + ' ';
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function unescapeCssValue(value) {
|
|
24
|
-
if (typeof value !== 'string') return value;
|
|
25
|
-
// Only perform unescaping if there are escape sequences
|
|
26
|
-
if (!value.includes('\\')) return value;
|
|
27
|
-
|
|
28
|
-
return value.replace(/\\([0-9a-f]{1,6})\s?/gi, function(match, hex) {
|
|
29
|
-
return String.fromCharCode(parseInt(hex, 16));
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Create a configured serializer instance
|
|
35
|
-
* @param {Object} config - Configuration options
|
|
36
|
-
* @returns {Object} - Serializer instance
|
|
37
|
-
*/
|
|
38
|
-
function createSerializer(config = {}) {
|
|
39
|
-
// Default configuration
|
|
40
|
-
const defaultConfig = {
|
|
41
|
-
mode: 'hybrid', // 'escape', 'json', or 'hybrid'
|
|
42
|
-
debug: false, // Enable debug logging
|
|
43
|
-
complexThreshold: 3, // Object properties threshold for hybrid mode
|
|
44
|
-
preserveTypes: true // Preserve type information in serialization
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// Merge provided config with defaults
|
|
48
|
-
const options = { ...defaultConfig, ...config };
|
|
49
|
-
|
|
50
|
-
// Serializer instance
|
|
51
|
-
const serializer = {
|
|
52
|
-
// Current configuration
|
|
53
|
-
config: options,
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Update configuration
|
|
57
|
-
* @param {Object} newConfig - New configuration options
|
|
58
|
-
*/
|
|
59
|
-
configure(newConfig) {
|
|
60
|
-
Object.assign(this.config, newConfig);
|
|
61
|
-
if (this.config.debug) {
|
|
62
|
-
console.log('StateSerializer config updated:', this.config);
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Serialize a value for storage in CSS variables
|
|
68
|
-
* @param {string} key - The state key (for context-aware serialization)
|
|
69
|
-
* @param {any} value - The value to serialize
|
|
70
|
-
* @returns {string} - Serialized value
|
|
71
|
-
*/
|
|
72
|
-
serialize(key, value) {
|
|
73
|
-
// Handle null/undefined
|
|
74
|
-
if (value === null || value === undefined) {
|
|
75
|
-
return '';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const valueType = typeof value;
|
|
79
|
-
const isComplex = valueType === 'object' &&
|
|
80
|
-
(Array.isArray(value) ||
|
|
81
|
-
(Object.keys(value).length >= this.config.complexThreshold));
|
|
82
|
-
|
|
83
|
-
// Select serialization strategy based on configuration and value type
|
|
84
|
-
if (this.config.mode === 'escape' ||
|
|
85
|
-
(this.config.mode === 'hybrid' && !isComplex)) {
|
|
86
|
-
// Use escape strategy for primitives or when escape mode is forced
|
|
87
|
-
if (valueType === 'string') {
|
|
88
|
-
return escapeCssValue(value);
|
|
89
|
-
} else if (valueType === 'object') {
|
|
90
|
-
// For simple objects in escape mode, still use JSON but with escaping
|
|
91
|
-
const jsonStr = JSON.stringify(value);
|
|
92
|
-
return escapeCssValue(jsonStr);
|
|
93
|
-
} else {
|
|
94
|
-
// For other primitives, convert to string
|
|
95
|
-
return String(value);
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
// Use JSON strategy for complex objects or when JSON mode is forced
|
|
99
|
-
return JSON.stringify(value);
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Deserialize a value from CSS variable storage
|
|
105
|
-
* @param {string} key - The state key (for context-aware deserialization)
|
|
106
|
-
* @param {string} value - The serialized value
|
|
107
|
-
* @returns {any} - Deserialized value
|
|
108
|
-
*/
|
|
109
|
-
deserialize(key, value) {
|
|
110
|
-
// Handle empty values
|
|
111
|
-
if (!value) return '';
|
|
112
|
-
|
|
113
|
-
// Try JSON parse first for values that look like JSON
|
|
114
|
-
if (this.config.mode !== 'escape' &&
|
|
115
|
-
((value.startsWith('{') && value.endsWith('}')) ||
|
|
116
|
-
(value.startsWith('[') && value.endsWith(']')))) {
|
|
117
|
-
try {
|
|
118
|
-
return JSON.parse(value);
|
|
119
|
-
} catch (e) {
|
|
120
|
-
if (this.config.debug) {
|
|
121
|
-
console.warn(`Failed to parse JSON for key "${key}":`, value);
|
|
122
|
-
}
|
|
123
|
-
// Fall through to unescaping if JSON parse fails
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// For non-JSON or escape mode, try unescaping
|
|
128
|
-
const unescaped = unescapeCssValue(value);
|
|
129
|
-
|
|
130
|
-
// If unescaped looks like JSON (might have been double-escaped), try parsing it
|
|
131
|
-
if (this.config.mode !== 'escape' &&
|
|
132
|
-
((unescaped.startsWith('{') && unescaped.endsWith('}')) ||
|
|
133
|
-
(unescaped.startsWith('[') && unescaped.endsWith(']')))) {
|
|
134
|
-
try {
|
|
135
|
-
return JSON.parse(unescaped);
|
|
136
|
-
} catch (e) {
|
|
137
|
-
// Not valid JSON, return unescaped string
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return unescaped;
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Serialize a value for data-* attribute
|
|
146
|
-
* @param {string} key - The state key
|
|
147
|
-
* @param {any} value - The value to serialize for attribute
|
|
148
|
-
* @returns {string} - Serialized attribute value
|
|
149
|
-
*/
|
|
150
|
-
serializeForAttribute(key, value) {
|
|
151
|
-
if (value === null || value === undefined) return null;
|
|
152
|
-
|
|
153
|
-
// For objects, use the standard serializer
|
|
154
|
-
if (typeof value === 'object') {
|
|
155
|
-
return this.serialize(key, value);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// For primitive values, use direct string conversion
|
|
159
|
-
return String(value);
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Apply serialized state to HTML element attributes and properties
|
|
164
|
-
* @param {string} key - State key
|
|
165
|
-
* @param {any} value - Value to apply
|
|
166
|
-
* @param {HTMLElement} element - Target element (defaults to documentElement)
|
|
167
|
-
*/
|
|
168
|
-
applyToAttributes(key, value, element = document.documentElement) {
|
|
169
|
-
// Skip null/undefined values
|
|
170
|
-
if (value === null || value === undefined) {
|
|
171
|
-
element.removeAttribute(`data-${key}`);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Handle objects specially
|
|
176
|
-
if (typeof value === 'object') {
|
|
177
|
-
// Set the main attribute with serialized value
|
|
178
|
-
element.setAttribute(`data-${key}`, this.serialize(key, value));
|
|
179
|
-
|
|
180
|
-
// For non-array objects, set individual property attributes
|
|
181
|
-
if (!Array.isArray(value)) {
|
|
182
|
-
Object.entries(value).forEach(([propKey, propValue]) => {
|
|
183
|
-
const attributeKey = `data-${key}-${propKey.toLowerCase()}`;
|
|
184
|
-
if (propValue !== null && propValue !== undefined) {
|
|
185
|
-
if (typeof propValue === 'object') {
|
|
186
|
-
element.setAttribute(
|
|
187
|
-
attributeKey,
|
|
188
|
-
this.serialize(`${key}.${propKey}`, propValue)
|
|
189
|
-
);
|
|
190
|
-
} else {
|
|
191
|
-
element.setAttribute(attributeKey, propValue);
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
element.removeAttribute(attributeKey);
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
} else {
|
|
199
|
-
// For primitives, set directly
|
|
200
|
-
element.setAttribute(`data-${key}`, value);
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Utility method to determine if a value needs complex serialization
|
|
206
|
-
* @param {any} value - Value to check
|
|
207
|
-
* @returns {boolean} - True if complex serialization is needed
|
|
208
|
-
*/
|
|
209
|
-
needsComplexSerialization(value) {
|
|
210
|
-
return typeof value === 'object' && value !== null;
|
|
211
|
-
},
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Set state with proper serialization for CSS variables
|
|
215
|
-
* @param {Object} uistate - UIstate instance
|
|
216
|
-
* @param {string} path - State path
|
|
217
|
-
* @param {any} value - Value to set
|
|
218
|
-
* @returns {any} - The set value
|
|
219
|
-
*/
|
|
220
|
-
setStateWithCss(uistate, path, value) {
|
|
221
|
-
// Update UIstate
|
|
222
|
-
uistate.setState(path, value);
|
|
223
|
-
|
|
224
|
-
// Update CSS variable with properly serialized value
|
|
225
|
-
const cssPath = path.replace(/\./g, '-');
|
|
226
|
-
const serialized = this.serialize(path, value);
|
|
227
|
-
document.documentElement.style.setProperty(`--${cssPath}`, serialized);
|
|
228
|
-
|
|
229
|
-
// Update data attribute for root level state
|
|
230
|
-
const segments = path.split('.');
|
|
231
|
-
if (segments.length === 1) {
|
|
232
|
-
document.documentElement.dataset[path] = typeof value === 'object'
|
|
233
|
-
? JSON.stringify(value)
|
|
234
|
-
: value;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return value;
|
|
238
|
-
},
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Get state with fallback to CSS variables
|
|
242
|
-
* @param {Object} uistate - UIstate instance
|
|
243
|
-
* @param {string} path - State path
|
|
244
|
-
* @returns {any} - Retrieved value
|
|
245
|
-
*/
|
|
246
|
-
getStateFromCss(uistate, path) {
|
|
247
|
-
// First try UIstate
|
|
248
|
-
const value = uistate.getState(path);
|
|
249
|
-
if (value !== undefined) return value;
|
|
250
|
-
|
|
251
|
-
// If not found, try CSS variable
|
|
252
|
-
const cssPath = path.replace(/\./g, '-');
|
|
253
|
-
const cssValue = getComputedStyle(document.documentElement)
|
|
254
|
-
.getPropertyValue(`--${cssPath}`).trim();
|
|
255
|
-
|
|
256
|
-
return cssValue ? this.deserialize(path, cssValue) : undefined;
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
return serializer;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Create a default instance with hybrid mode
|
|
264
|
-
const StateSerializer = createSerializer();
|
|
265
|
-
|
|
266
|
-
export default StateSerializer;
|
|
267
|
-
export { createSerializer, escapeCssValue, unescapeCssValue };
|
package/templateManager.js
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TemplateManager - Component mounting and event delegation
|
|
3
|
-
* Handles HTML templating, component mounting, and event delegation
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const createTemplateManager = (stateManager) => {
|
|
7
|
-
const manager = {
|
|
8
|
-
handlers: {},
|
|
9
|
-
|
|
10
|
-
onAction(action, handler) {
|
|
11
|
-
this.handlers[action] = handler;
|
|
12
|
-
return this;
|
|
13
|
-
},
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Register multiple actions with their handlers in a declarative way
|
|
17
|
-
* @param {Object} actionsMap - Map of action names to handlers or handler configs
|
|
18
|
-
* @returns {Object} - The manager instance for chaining
|
|
19
|
-
*
|
|
20
|
-
* Example usage:
|
|
21
|
-
* templateManager.registerActions({
|
|
22
|
-
* 'add-item': addItem,
|
|
23
|
-
* 'delete-item': { fn: deleteItem, extractId: true },
|
|
24
|
-
* 'toggle-state': toggleState
|
|
25
|
-
* });
|
|
26
|
-
*/
|
|
27
|
-
registerActions(actionsMap) {
|
|
28
|
-
Object.entries(actionsMap).forEach(([action, handler]) => {
|
|
29
|
-
if (typeof handler === 'function') {
|
|
30
|
-
// Simple function handler
|
|
31
|
-
this.onAction(action, handler);
|
|
32
|
-
} else if (typeof handler === 'object' && handler !== null) {
|
|
33
|
-
// Handler with configuration
|
|
34
|
-
const { fn, extractId = true, idAttribute = 'id' } = handler;
|
|
35
|
-
|
|
36
|
-
if (typeof fn !== 'function') {
|
|
37
|
-
throw new Error(`Handler for action '${action}' must be a function`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
this.onAction(action, (e) => {
|
|
41
|
-
if (extractId) {
|
|
42
|
-
const target = e.target.closest('[data-action]');
|
|
43
|
-
// Look for common ID attributes in order of preference
|
|
44
|
-
const id = target.dataset[idAttribute] ||
|
|
45
|
-
target.dataset.actionId ||
|
|
46
|
-
target.dataset.cardId ||
|
|
47
|
-
target.dataset.itemId;
|
|
48
|
-
|
|
49
|
-
fn(id, e, target);
|
|
50
|
-
} else {
|
|
51
|
-
fn(e);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
} else {
|
|
55
|
-
throw new Error(`Invalid handler for action '${action}'`);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
return this;
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
attachDelegation(root = document.body) {
|
|
62
|
-
root.addEventListener('click', e => {
|
|
63
|
-
const target = e.target.closest('[data-action]');
|
|
64
|
-
if (!target) return;
|
|
65
|
-
|
|
66
|
-
const action = target.dataset.action;
|
|
67
|
-
if (!action) return;
|
|
68
|
-
|
|
69
|
-
const handler = this.handlers[action];
|
|
70
|
-
if (typeof handler === 'function') {
|
|
71
|
-
handler(e);
|
|
72
|
-
} else if (target.dataset.value !== undefined && stateManager) {
|
|
73
|
-
// If we have a state manager, use it to update state
|
|
74
|
-
stateManager.setState(action, target.dataset.value);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
return this;
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Render a template from a CSS variable
|
|
82
|
-
* @param {string} templateName - Name of the template (will be prefixed with --template-)
|
|
83
|
-
* @param {Object} data - Data to inject into the template
|
|
84
|
-
* @returns {HTMLElement} - The rendered element
|
|
85
|
-
*/
|
|
86
|
-
renderTemplateFromCss(templateName, data = {}) {
|
|
87
|
-
const cssTemplate = getComputedStyle(document.documentElement)
|
|
88
|
-
.getPropertyValue(`--template-${templateName}`)
|
|
89
|
-
.trim()
|
|
90
|
-
.replace(/^['"]|['"]$/g, ''); // Remove surrounding quotes
|
|
91
|
-
|
|
92
|
-
if (!cssTemplate) throw new Error(`Template not found in CSS: --template-${templateName}`);
|
|
93
|
-
|
|
94
|
-
let html = cssTemplate;
|
|
95
|
-
|
|
96
|
-
// Replace all placeholders with actual data
|
|
97
|
-
Object.entries(data).forEach(([key, value]) => {
|
|
98
|
-
const regex = new RegExp(`{{${key}}}`, 'g');
|
|
99
|
-
html = html.replace(regex, value);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Create a temporary container
|
|
103
|
-
const temp = document.createElement('div');
|
|
104
|
-
temp.innerHTML = html;
|
|
105
|
-
|
|
106
|
-
// Return the first child (the rendered template)
|
|
107
|
-
return temp.firstElementChild;
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
mount(componentName, container) {
|
|
111
|
-
const tpl = document.getElementById(`${componentName}-template`);
|
|
112
|
-
if (!tpl) throw new Error(`Template not found: ${componentName}-template`);
|
|
113
|
-
const clone = tpl.content.cloneNode(true);
|
|
114
|
-
|
|
115
|
-
function resolvePlaceholders(fragment) {
|
|
116
|
-
Array.from(fragment.querySelectorAll('*')).forEach(el => {
|
|
117
|
-
const tag = el.tagName.toLowerCase();
|
|
118
|
-
if (tag.endsWith('-placeholder')) {
|
|
119
|
-
const name = tag.replace('-placeholder','');
|
|
120
|
-
const childTpl = document.getElementById(`${name}-template`);
|
|
121
|
-
if (!childTpl) throw new Error(`Template not found: ${name}-template`);
|
|
122
|
-
const childClone = childTpl.content.cloneNode(true);
|
|
123
|
-
resolvePlaceholders(childClone);
|
|
124
|
-
el.replaceWith(childClone);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
resolvePlaceholders(clone);
|
|
130
|
-
container.appendChild(clone);
|
|
131
|
-
return clone.firstElementChild;
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
// Helper to create a reactive component with automatic updates
|
|
135
|
-
createComponent(name, renderFn, stateKeys = []) {
|
|
136
|
-
if (!stateManager) {
|
|
137
|
-
throw new Error('State manager is required for reactive components');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Create template element if it doesn't exist
|
|
141
|
-
let tpl = document.getElementById(`${name}-template`);
|
|
142
|
-
if (!tpl) {
|
|
143
|
-
tpl = document.createElement('template');
|
|
144
|
-
tpl.id = `${name}-template`;
|
|
145
|
-
document.body.appendChild(tpl);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Initial render
|
|
149
|
-
tpl.innerHTML = renderFn(stateManager);
|
|
150
|
-
|
|
151
|
-
// Set up observers for reactive updates
|
|
152
|
-
if (stateKeys.length > 0) {
|
|
153
|
-
stateKeys.forEach(key => {
|
|
154
|
-
stateManager.observe(key, () => {
|
|
155
|
-
tpl.innerHTML = renderFn(stateManager);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
mount: (container) => this.mount(name, container)
|
|
162
|
-
};
|
|
163
|
-
},
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Apply CSS classes to an element based on a state key stored in CSS variables
|
|
167
|
-
* @param {HTMLElement} element - Element to apply classes to
|
|
168
|
-
* @param {string} stateKey - State key to look up in CSS variables
|
|
169
|
-
* @param {Object} options - Options for class application
|
|
170
|
-
* @returns {HTMLElement} - The element for chaining
|
|
171
|
-
*
|
|
172
|
-
* Example usage:
|
|
173
|
-
* // CSS: :root { --card-primary-classes: "bg-primary text-white"; }
|
|
174
|
-
* templateManager.applyClassesFromState(cardElement, 'card-primary');
|
|
175
|
-
*/
|
|
176
|
-
applyClassesFromState(element, stateKey, options = {}) {
|
|
177
|
-
if (!element) return element;
|
|
178
|
-
|
|
179
|
-
const {
|
|
180
|
-
prefix = '',
|
|
181
|
-
clearExisting = false,
|
|
182
|
-
namespace = ''
|
|
183
|
-
} = typeof options === 'string' ? { prefix: options } : options;
|
|
184
|
-
|
|
185
|
-
const prefixPath = prefix ? `${prefix}-` : '';
|
|
186
|
-
const namespacePath = namespace ? `${namespace}-` : '';
|
|
187
|
-
|
|
188
|
-
const classString = getComputedStyle(document.documentElement)
|
|
189
|
-
.getPropertyValue(`--${namespacePath}${stateKey}-classes`)
|
|
190
|
-
.trim()
|
|
191
|
-
.replace(/^['"]|['"]$/g, '');
|
|
192
|
-
|
|
193
|
-
if (classString) {
|
|
194
|
-
// Clear existing classes if specified
|
|
195
|
-
if (clearExisting) {
|
|
196
|
-
element.className = '';
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Add new classes
|
|
200
|
-
classString.split(' ').forEach(cls => {
|
|
201
|
-
if (cls) element.classList.add(cls);
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return element; // For chaining
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
return manager;
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// Create a standalone instance that doesn't depend on any state manager
|
|
213
|
-
const TemplateManager = createTemplateManager();
|
|
214
|
-
|
|
215
|
-
export default createTemplateManager;
|
|
216
|
-
export { createTemplateManager, TemplateManager };
|