@uistate/css 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/cssState.js +212 -0
- package/index.js +15 -0
- package/package.json +39 -0
- package/stateSerializer.js +267 -0
- package/templateManager.js +216 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @uistate/css
|
|
2
|
+
|
|
3
|
+
Zero-build CSS-native state management using CSS custom properties and data attributes.
|
|
4
|
+
|
|
5
|
+
## What is this?
|
|
6
|
+
|
|
7
|
+
A state management library that uses **CSS custom properties** (`--var`) and **`data-*` attributes** as the state transport layer. No bundler, no framework — just drop a `<script>` tag and go.
|
|
8
|
+
|
|
9
|
+
State is inspectable in DevTools (Elements → Computed → CSS variables), and CSS selectors can react to state changes (`[data-theme="dark"]`).
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @uistate/css
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<script type="module">
|
|
21
|
+
import { createCssState } from '@uistate/css';
|
|
22
|
+
|
|
23
|
+
const ui = createCssState({ theme: 'light', count: 0 });
|
|
24
|
+
|
|
25
|
+
// State lives in CSS variables and data attributes
|
|
26
|
+
ui.setState('theme', 'dark');
|
|
27
|
+
// → document.documentElement has --theme: dark and data-theme="dark"
|
|
28
|
+
|
|
29
|
+
// Observe changes
|
|
30
|
+
ui.observe('theme', (value) => {
|
|
31
|
+
console.log('Theme changed to:', value);
|
|
32
|
+
});
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<!-- Declarative observers -->
|
|
36
|
+
<span data-observe="count">0</span>
|
|
37
|
+
|
|
38
|
+
<!-- Declarative actions -->
|
|
39
|
+
<button data-state-action="theme" data-state-value="dark">Dark Mode</button>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Modules
|
|
43
|
+
|
|
44
|
+
### `createCssState(initialState)`
|
|
45
|
+
Core CSS state management. Sets/gets state via CSS custom properties, supports observers, declarative DOM binding via `data-observe` and `data-state-action` attributes.
|
|
46
|
+
|
|
47
|
+
### `createSerializer(config)`
|
|
48
|
+
Configurable serialization between JavaScript values and CSS-compatible strings. Supports `'escape'`, `'json'`, and `'hybrid'` modes.
|
|
49
|
+
|
|
50
|
+
### `createTemplateManager(stateManager)`
|
|
51
|
+
Component mounting via `<template>` elements, event delegation via `data-action` attributes, and CSS-variable-based templating.
|
|
52
|
+
|
|
53
|
+
## Ideal for
|
|
54
|
+
|
|
55
|
+
- **WordPress** — reactive PHP templates without a build step
|
|
56
|
+
- **Jekyll / Hugo / 11ty** — interactivity for static sites
|
|
57
|
+
- **Shopify Liquid / Twig / Blade** — server-rendered templates with client-side state
|
|
58
|
+
- **Web Components** — CSS custom properties pierce shadow DOM
|
|
59
|
+
- **Progressive enhancement** — layer interactivity onto existing HTML
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
package/cssState.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
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/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @uistate/css - Zero-build CSS-native state management
|
|
3
|
+
*
|
|
4
|
+
* Uses CSS custom properties and data attributes as the state transport layer.
|
|
5
|
+
* Works in any HTML page — no bundler, no framework required.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Primary: CSS State
|
|
9
|
+
export { createCssState } from './cssState.js';
|
|
10
|
+
|
|
11
|
+
// Serialization: CSS ↔ JS value transformation
|
|
12
|
+
export { default as stateSerializer, createSerializer, escapeCssValue, unescapeCssValue } from './stateSerializer.js';
|
|
13
|
+
|
|
14
|
+
// Templating: Component mounting and event delegation
|
|
15
|
+
export { createTemplateManager, TemplateManager } from './templateManager.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uistate/css",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-build CSS-native state management using CSS custom properties and data attributes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./cssState": "./cssState.js",
|
|
10
|
+
"./stateSerializer": "./stateSerializer.js",
|
|
11
|
+
"./templateManager": "./templateManager.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"cssState.js",
|
|
16
|
+
"stateSerializer.js",
|
|
17
|
+
"templateManager.js"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"css-state",
|
|
21
|
+
"css-custom-properties",
|
|
22
|
+
"css-variables",
|
|
23
|
+
"state-management",
|
|
24
|
+
"zero-build",
|
|
25
|
+
"no-framework",
|
|
26
|
+
"declarative",
|
|
27
|
+
"data-attributes",
|
|
28
|
+
"theming",
|
|
29
|
+
"wordpress",
|
|
30
|
+
"static-sites",
|
|
31
|
+
"progressive-enhancement"
|
|
32
|
+
],
|
|
33
|
+
"author": "Ajdin Imsirovic",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/ImsirovicAjdin/uistate-css"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
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 };
|
|
@@ -0,0 +1,216 @@
|
|
|
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 };
|