@uistate/core 3.0.0 → 3.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uistate/core",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "High-performance UI state management using CSS custom properties and ADSI (Attribute-Driven State Inheritance)",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
package/src/cssState.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * UIstate - CSS-based state management module with integrated serialization
3
3
  * Part of the UIstate declarative state management system
4
4
  * Uses CSS custom properties and data attributes for state representation
5
- * Includes built-in serialization for complex objects and special characters
5
+ * Features modular extension capabilities for DOM binding and events
6
6
  */
7
7
  import StateSerializer from './stateSerializer.js';
8
8
 
@@ -11,74 +11,60 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
11
11
  _sheet: null,
12
12
  _observers: new Map(),
13
13
  _serializer: serializer,
14
-
15
- init() {
14
+ _specialHandlers: {},
15
+ _eventHandlers: new Map(), // Store custom event binding handlers
16
+
17
+ init(serializerConfig) {
16
18
  if (!this._sheet) {
17
19
  const style = document.createElement('style');
18
20
  document.head.appendChild(style);
19
21
  this._sheet = style.sheet;
20
22
  this._addRule(':root {}');
21
23
  }
22
-
24
+
25
+ // Configure serializer if options provided
26
+ if (serializerConfig && typeof serializerConfig === 'object') {
27
+ this._serializer.configure(serializerConfig);
28
+ }
29
+
23
30
  // Initialize with any provided state
24
31
  if (initialState && typeof initialState === 'object') {
25
32
  Object.entries(initialState).forEach(([key, value]) => {
26
33
  this.setState(key, value);
27
34
  });
28
35
  }
29
-
36
+
30
37
  return this;
31
38
  },
32
-
39
+
33
40
  setState(key, value) {
34
41
  // Use serializer for CSS variables
35
42
  const cssValue = this._serializer.serialize(key, value);
36
43
  document.documentElement.style.setProperty(`--${key}`, cssValue);
37
-
38
- // For data attributes, handle objects specially
39
- if (value !== null && value !== undefined) {
40
- if (typeof value === 'object') {
41
- // Use the serializer for the data attribute value
42
- document.documentElement.setAttribute(`data-${key}`, this._serializer.serialize(key, value));
43
-
44
- // For objects, also set each property as a separate data attribute
45
- if (!Array.isArray(value)) {
46
- Object.entries(value).forEach(([propKey, propValue]) => {
47
- const attributeKey = `data-${key}-${propKey.toLowerCase()}`;
48
- if (propValue !== null && propValue !== undefined) {
49
- if (typeof propValue === 'object') {
50
- document.documentElement.setAttribute(
51
- attributeKey,
52
- this._serializer.serialize(`${key}.${propKey}`, propValue)
53
- );
54
- } else {
55
- document.documentElement.setAttribute(attributeKey, propValue);
56
- }
57
- } else {
58
- document.documentElement.removeAttribute(attributeKey);
59
- }
60
- });
61
- }
62
- } else {
63
- // For primitives, set directly
64
- document.documentElement.setAttribute(`data-${key}`, value);
65
- }
66
- } else {
67
- document.documentElement.removeAttribute(`data-${key}`);
68
- }
69
-
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
70
49
  this._notifyObservers(key, value);
71
50
  return value;
72
51
  },
73
-
52
+
53
+ setStates(stateObject) {
54
+ Object.entries(stateObject).forEach(([key, value]) => {
55
+ this.setState(key, value);
56
+ });
57
+ return this;
58
+ },
59
+
74
60
  getState(key) {
75
61
  const value = getComputedStyle(document.documentElement).getPropertyValue(`--${key}`).trim();
76
62
  if (!value) return '';
77
-
63
+
78
64
  // Use serializer for deserialization
79
65
  return this._serializer.deserialize(key, value);
80
66
  },
81
-
67
+
82
68
  observe(key, callback) {
83
69
  if (!this._observers.has(key)) {
84
70
  this._observers.set(key, new Set());
@@ -91,20 +77,115 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
91
77
  }
92
78
  };
93
79
  },
94
-
80
+
95
81
  _notifyObservers(key, value) {
96
82
  const observers = this._observers.get(key);
97
83
  if (observers) {
98
84
  observers.forEach(cb => cb(value));
99
85
  }
100
86
  },
101
-
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
+
102
183
  _addRule(rule) {
103
184
  if (this._sheet) {
104
185
  this._sheet.insertRule(rule, this._sheet.cssRules.length);
105
186
  }
106
187
  },
107
-
188
+
108
189
  // Add serializer configuration method
109
190
  configureSerializer(config) {
110
191
  if (this._serializer.configure) {
@@ -112,7 +193,7 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
112
193
  }
113
194
  return this;
114
195
  },
115
-
196
+
116
197
  // Clean up resources
117
198
  destroy() {
118
199
  this._observers.clear();
@@ -120,12 +201,12 @@ const createCssState = (initialState = {}, serializer = StateSerializer) => {
120
201
  // as removing it would affect the UI state
121
202
  }
122
203
  };
123
-
204
+
124
205
  return state.init();
125
206
  };
126
207
 
127
- // Legacy singleton for backward compatibility
128
- const CssState = createCssState();
208
+ // Create a singleton instance for easy usage
209
+ const UIstate = createCssState();
129
210
 
130
- export default createCssState;
131
- export { createCssState, CssState };
211
+ export { createCssState };
212
+ export default UIstate;
package/src/eventState.js CHANGED
@@ -23,12 +23,12 @@ const createEventState = (initial = {}) => {
23
23
  return path
24
24
  .split(".")
25
25
  .reduce(
26
- (obj, prop) =>
26
+ (obj, prop) =>
27
27
  obj && obj[prop] !== undefined ? obj[prop] : undefined,
28
28
  store
29
29
  );
30
30
  },
31
-
31
+
32
32
  // set a value in the store by path
33
33
  set: (path, value) => {
34
34
  if(!path) return;
@@ -84,10 +84,10 @@ const createEventState = (initial = {}) => {
84
84
 
85
85
  return () => bus.removeEventListener(path, handler);
86
86
  },
87
-
87
+
88
88
  // Optional method to clean up resources
89
89
  destroy: () => {
90
- if (bus.parentNode) {
90
+ if (bus.parentNode) {
91
91
  bus.parentNode.removeChild(bus);
92
92
  }
93
93
  },
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * UIstate - Declarative state management for the web
3
- *
3
+ *
4
4
  * A unified system that combines CSS and Event-based state management with templating
5
5
  * Integrates CSS variables, event-based state, and template management for optimal performance
6
6
  * Provides a simple, declarative API for state management
@@ -24,10 +24,10 @@ function convertPathToCssPath(path) {
24
24
  const createUnifiedState = (initialState = {}) => {
25
25
  // Create the CSS state manager with integrated serialization
26
26
  const cssState = createCssState(initialState);
27
-
27
+
28
28
  // Create the event state manager with the same initial state
29
29
  const eventState = createEventState(initialState);
30
-
30
+
31
31
  // Plugin system
32
32
  const plugins = new Map();
33
33
  const middlewares = [];
@@ -37,36 +37,36 @@ const createUnifiedState = (initialState = {}) => {
37
37
  onInit: [],
38
38
  onDestroy: []
39
39
  };
40
-
40
+
41
41
  // Create a unified API
42
42
  const unifiedState = {
43
43
  _isNotifying: false,
44
44
  _cssState: cssState, // Reference to cssState for direct access to advanced features
45
-
45
+
46
46
  // Get state with hierarchical path support
47
47
  getState(path) {
48
48
  // Always use eventState as the source of truth
49
49
  // This ensures consistency with the DOM state
50
50
  const value = eventState.get(path);
51
-
51
+
52
52
  // If path is undefined or value not found in eventState
53
53
  if (path && value === undefined) {
54
54
  // Fall back to CSS variable (for values set outside this API)
55
55
  const cssPath = convertPathToCssPath(path);
56
56
  return cssState.getState(cssPath);
57
57
  }
58
-
58
+
59
59
  return value;
60
60
  },
61
-
61
+
62
62
  // Set state with hierarchical path support
63
63
  setState(path, value) {
64
64
  if (!path) return this;
65
-
65
+
66
66
  // Run before state change middlewares
67
67
  let finalValue = value;
68
68
  let shouldContinue = true;
69
-
69
+
70
70
  // Apply middlewares
71
71
  for (const middleware of middlewares) {
72
72
  const result = middleware(path, finalValue, this.getState.bind(this));
@@ -77,49 +77,49 @@ const createUnifiedState = (initialState = {}) => {
77
77
  finalValue = result;
78
78
  }
79
79
  }
80
-
80
+
81
81
  // Run lifecycle hooks
82
82
  for (const hook of lifecycleHooks.beforeStateChange) {
83
83
  hook(path, finalValue, this.getState.bind(this));
84
84
  }
85
-
85
+
86
86
  if (!shouldContinue) return this;
87
-
87
+
88
88
  // Update event state (for JS access and pub/sub)
89
89
  eventState.set(path, finalValue);
90
-
90
+
91
91
  // Update CSS state (for styling and DOM representation)
92
92
  const cssPath = convertPathToCssPath(path);
93
93
  cssState.setState(cssPath, finalValue);
94
-
94
+
95
95
  // Run after state change hooks
96
96
  for (const hook of lifecycleHooks.afterStateChange) {
97
97
  hook(path, finalValue, this.getState.bind(this));
98
98
  }
99
-
99
+
100
100
  return this;
101
101
  },
102
-
102
+
103
103
  // Plugin system methods
104
104
  use(pluginName, plugin) {
105
105
  if (plugins.has(pluginName)) {
106
106
  console.warn(`Plugin '${pluginName}' is already registered. It will be replaced.`);
107
107
  }
108
-
108
+
109
109
  // Register the plugin
110
110
  plugins.set(pluginName, plugin);
111
-
111
+
112
112
  // Initialize the plugin
113
113
  if (typeof plugin.init === 'function') {
114
114
  // Pass the API to the plugin
115
115
  plugin.init(this);
116
116
  }
117
-
117
+
118
118
  // Register middlewares
119
119
  if (Array.isArray(plugin.middlewares)) {
120
120
  middlewares.push(...plugin.middlewares);
121
121
  }
122
-
122
+
123
123
  // Register lifecycle hooks
124
124
  if (plugin.hooks) {
125
125
  Object.entries(plugin.hooks).forEach(([hookName, hookFn]) => {
@@ -128,7 +128,7 @@ const createUnifiedState = (initialState = {}) => {
128
128
  }
129
129
  });
130
130
  }
131
-
131
+
132
132
  // Add plugin methods to the API
133
133
  if (plugin.methods) {
134
134
  Object.entries(plugin.methods).forEach(([methodName, method]) => {
@@ -137,34 +137,34 @@ const createUnifiedState = (initialState = {}) => {
137
137
  }
138
138
  });
139
139
  }
140
-
140
+
141
141
  return this;
142
142
  },
143
-
143
+
144
144
  getPlugin(pluginName) {
145
145
  return plugins.get(pluginName);
146
146
  },
147
-
147
+
148
148
  hasPlugin(pluginName) {
149
149
  return plugins.has(pluginName);
150
150
  },
151
-
151
+
152
152
  removePlugin(pluginName) {
153
153
  const plugin = plugins.get(pluginName);
154
154
  if (!plugin) return false;
155
-
155
+
156
156
  // Run cleanup if available
157
157
  if (typeof plugin.destroy === 'function') {
158
158
  plugin.destroy();
159
159
  }
160
-
160
+
161
161
  // Remove plugin methods
162
162
  if (plugin.methods) {
163
163
  Object.keys(plugin.methods).forEach(methodName => {
164
164
  delete this[methodName];
165
165
  });
166
166
  }
167
-
167
+
168
168
  // Remove middlewares
169
169
  if (Array.isArray(plugin.middlewares)) {
170
170
  plugin.middlewares.forEach(middleware => {
@@ -174,7 +174,7 @@ const createUnifiedState = (initialState = {}) => {
174
174
  }
175
175
  });
176
176
  }
177
-
177
+
178
178
  // Remove lifecycle hooks
179
179
  if (plugin.hooks) {
180
180
  Object.entries(plugin.hooks).forEach(([hookName, hookFn]) => {
@@ -186,12 +186,12 @@ const createUnifiedState = (initialState = {}) => {
186
186
  }
187
187
  });
188
188
  }
189
-
189
+
190
190
  // Remove the plugin
191
191
  plugins.delete(pluginName);
192
192
  return true;
193
193
  },
194
-
194
+
195
195
  // Add middleware
196
196
  addMiddleware(middleware) {
197
197
  if (typeof middleware === 'function') {
@@ -200,7 +200,7 @@ const createUnifiedState = (initialState = {}) => {
200
200
  }
201
201
  return false;
202
202
  },
203
-
203
+
204
204
  // Add lifecycle hook
205
205
  addHook(hookName, hookFn) {
206
206
  if (typeof hookFn === 'function' && Array.isArray(lifecycleHooks[hookName])) {
@@ -209,60 +209,60 @@ const createUnifiedState = (initialState = {}) => {
209
209
  }
210
210
  return false;
211
211
  },
212
-
212
+
213
213
  // Subscribe to state changes with support for wildcards
214
214
  subscribe(path, callback) {
215
215
  return eventState.subscribe(path, callback);
216
216
  },
217
-
217
+
218
218
  // Observe CSS state changes (simpler API, no wildcards)
219
219
  observe(key, callback) {
220
220
  return cssState.observe(key, callback);
221
221
  },
222
-
222
+
223
223
  // Configure serialization options
224
224
  configureSerializer(config) {
225
225
  cssState.configureSerializer(config);
226
226
  return this;
227
227
  },
228
-
228
+
229
229
  // Get current serializer configuration
230
230
  getSerializerConfig() {
231
231
  return cssState._serializer?.config || null;
232
232
  },
233
-
233
+
234
234
  // Set serialization mode ('hybrid', 'json', or 'escape')
235
235
  setSerializationMode(mode) {
236
236
  return this.configureSerializer({ mode });
237
237
  },
238
-
238
+
239
239
  // Clean up resources
240
240
  destroy() {
241
241
  // Run onDestroy hooks
242
242
  for (const hook of lifecycleHooks.onDestroy) {
243
243
  hook();
244
244
  }
245
-
245
+
246
246
  // Destroy all plugins
247
247
  for (const [pluginName, plugin] of plugins.entries()) {
248
248
  if (typeof plugin.destroy === 'function') {
249
249
  plugin.destroy();
250
250
  }
251
251
  }
252
-
252
+
253
253
  // Clear plugins and middlewares
254
254
  plugins.clear();
255
255
  middlewares.length = 0;
256
256
  Object.keys(lifecycleHooks).forEach(key => {
257
257
  lifecycleHooks[key].length = 0;
258
258
  });
259
-
259
+
260
260
  // Destroy core state management
261
261
  cssState.destroy();
262
262
  eventState.destroy();
263
263
  }
264
264
  };
265
-
265
+
266
266
  return unifiedState;
267
267
  };
268
268
 
@@ -273,47 +273,47 @@ const UnifiedState = createUnifiedState();
273
273
  const templateManagerPlugin = {
274
274
  name: 'templateManager',
275
275
  instance: null,
276
-
276
+
277
277
  init(api) {
278
278
  this.instance = createTemplateManager(api);
279
279
  return this;
280
280
  },
281
-
281
+
282
282
  destroy() {
283
283
  // Any cleanup needed for the template manager
284
284
  },
285
-
285
+
286
286
  // Plugin methods that will be added to the UIstate API
287
287
  methods: {
288
288
  onAction(action, handler) {
289
289
  return this.instance.onAction(action, handler);
290
290
  },
291
-
291
+
292
292
  registerActions(actionsMap) {
293
293
  return this.instance.registerActions(actionsMap);
294
294
  },
295
-
295
+
296
296
  attachDelegation(root = document.body) {
297
297
  return this.instance.attachDelegation(root);
298
298
  },
299
-
299
+
300
300
  mount(componentName, container) {
301
301
  return this.instance.mount(componentName, container);
302
302
  },
303
-
303
+
304
304
  renderTemplateFromCss(templateName, data) {
305
305
  return this.instance.renderTemplateFromCss(templateName, data);
306
306
  },
307
-
307
+
308
308
  createComponent(name, renderFn, stateKeys) {
309
309
  return this.instance.createComponent(name, renderFn, stateKeys);
310
310
  },
311
-
311
+
312
312
  applyClassesFromState(element, stateKey, options) {
313
313
  return this.instance.applyClassesFromState(element, stateKey, options);
314
314
  }
315
315
  },
316
-
316
+
317
317
  // Expose handlers property
318
318
  get handlers() {
319
319
  return this.instance.handlers;
@@ -323,30 +323,30 @@ const templateManagerPlugin = {
323
323
  // Create a combined API with plugin support
324
324
  const UIstate = {
325
325
  ...UnifiedState,
326
-
326
+
327
327
  // Initialize the system with plugins
328
328
  init() {
329
329
  // Register the template manager plugin
330
330
  this.use('templateManager', templateManagerPlugin);
331
-
331
+
332
332
  // Run onInit hooks
333
333
  for (const hook of this.getPlugin('templateManager').instance.lifecycleHooks?.onInit || []) {
334
334
  // for (const hook of lifecycleHooks.onInit) {
335
335
  hook();
336
336
  }
337
-
337
+
338
338
  // Attach event delegation to document body
339
339
  this.attachDelegation(document.body);
340
-
340
+
341
341
  return this;
342
342
  }
343
343
  };
344
344
 
345
345
  export default UIstate;
346
- export {
347
- createUnifiedState,
348
- createCssState,
349
- createEventState,
346
+ export {
347
+ createUnifiedState,
348
+ createCssState,
349
+ createEventState,
350
350
  createTemplateManager,
351
351
  convertPathToCssPath,
352
352
  templateManagerPlugin
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * StateInspector - Real-time state inspection panel for UIstate
3
- *
3
+ *
4
4
  * This module provides a configurable UI panel that displays and allows manipulation
5
5
  * of CSS variables and state values in UIstate applications.
6
- *
6
+ *
7
7
  * Features:
8
8
  * - Toggle visibility with a dedicated button
9
9
  * - Real-time updates of CSS variable values
@@ -28,10 +28,10 @@ function createStateInspector(config = {}) {
28
28
  stateManager: null, // Optional reference to UIstate or CssState instance
29
29
  theme: 'light'
30
30
  };
31
-
31
+
32
32
  // Merge provided config with defaults
33
33
  const options = { ...defaultConfig, ...config };
34
-
34
+
35
35
  // Private state
36
36
  let isVisible = options.initiallyVisible;
37
37
  let panel = null;
@@ -39,12 +39,12 @@ function createStateInspector(config = {}) {
39
39
  let filterInput = null;
40
40
  let stateContainer = null;
41
41
  let currentFilter = '';
42
-
42
+
43
43
  // StateInspector instance
44
44
  const inspector = {
45
45
  // Current configuration
46
46
  config: options,
47
-
47
+
48
48
  /**
49
49
  * Update configuration
50
50
  * @param {Object} newConfig - New configuration options
@@ -53,7 +53,7 @@ function createStateInspector(config = {}) {
53
53
  Object.assign(this.config, newConfig);
54
54
  this.refresh();
55
55
  },
56
-
56
+
57
57
  /**
58
58
  * Attach the inspector panel to the target element
59
59
  * @param {Element} element - Target element to attach to (defaults to config.target)
@@ -65,7 +65,7 @@ function createStateInspector(config = {}) {
65
65
  console.log('StateInspector: Panel would be attached to', element);
66
66
  return this;
67
67
  },
68
-
68
+
69
69
  /**
70
70
  * Show the inspector panel
71
71
  * @returns {Object} - The inspector instance for chaining
@@ -75,7 +75,7 @@ function createStateInspector(config = {}) {
75
75
  if (panel) panel.style.display = 'block';
76
76
  return this;
77
77
  },
78
-
78
+
79
79
  /**
80
80
  * Hide the inspector panel
81
81
  * @returns {Object} - The inspector instance for chaining
@@ -85,7 +85,7 @@ function createStateInspector(config = {}) {
85
85
  if (panel) panel.style.display = 'none';
86
86
  return this;
87
87
  },
88
-
88
+
89
89
  /**
90
90
  * Toggle the visibility of the inspector panel
91
91
  * @returns {Object} - The inspector instance for chaining
@@ -93,7 +93,7 @@ function createStateInspector(config = {}) {
93
93
  toggle() {
94
94
  return isVisible ? this.hide() : this.show();
95
95
  },
96
-
96
+
97
97
  /**
98
98
  * Refresh the state display
99
99
  * @returns {Object} - The inspector instance for chaining
@@ -104,7 +104,7 @@ function createStateInspector(config = {}) {
104
104
  console.log('StateInspector: State display would be refreshed');
105
105
  return this;
106
106
  },
107
-
107
+
108
108
  /**
109
109
  * Filter the displayed state variables
110
110
  * @param {string} filterText - Text to filter by
@@ -115,7 +115,7 @@ function createStateInspector(config = {}) {
115
115
  // Implementation will filter the displayed variables
116
116
  return this.refresh();
117
117
  },
118
-
118
+
119
119
  /**
120
120
  * Clean up resources used by the inspector
121
121
  */
@@ -129,7 +129,7 @@ function createStateInspector(config = {}) {
129
129
  stateContainer = null;
130
130
  }
131
131
  };
132
-
132
+
133
133
  return inspector;
134
134
  }
135
135
 
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * StateSerializer - Configurable serialization module for UIstate
3
3
  * Handles transformation between JavaScript values and CSS-compatible string values
4
- *
4
+ *
5
5
  * Supports multiple serialization strategies:
6
6
  * - 'escape': Uses custom escaping for all values (original UIstate approach)
7
7
  * - 'json': Uses JSON.stringify for complex objects, direct values for primitives
8
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
9
12
  */
10
13
 
11
14
  // Utility functions for CSS value escaping/unescaping
@@ -21,7 +24,7 @@ function unescapeCssValue(value) {
21
24
  if (typeof value !== 'string') return value;
22
25
  // Only perform unescaping if there are escape sequences
23
26
  if (!value.includes('\\')) return value;
24
-
27
+
25
28
  return value.replace(/\\([0-9a-f]{1,6})\s?/gi, function(match, hex) {
26
29
  return String.fromCharCode(parseInt(hex, 16));
27
30
  });
@@ -40,15 +43,15 @@ function createSerializer(config = {}) {
40
43
  complexThreshold: 3, // Object properties threshold for hybrid mode
41
44
  preserveTypes: true // Preserve type information in serialization
42
45
  };
43
-
46
+
44
47
  // Merge provided config with defaults
45
48
  const options = { ...defaultConfig, ...config };
46
-
49
+
47
50
  // Serializer instance
48
51
  const serializer = {
49
52
  // Current configuration
50
53
  config: options,
51
-
54
+
52
55
  /**
53
56
  * Update configuration
54
57
  * @param {Object} newConfig - New configuration options
@@ -59,7 +62,7 @@ function createSerializer(config = {}) {
59
62
  console.log('StateSerializer config updated:', this.config);
60
63
  }
61
64
  },
62
-
65
+
63
66
  /**
64
67
  * Serialize a value for storage in CSS variables
65
68
  * @param {string} key - The state key (for context-aware serialization)
@@ -71,14 +74,14 @@ function createSerializer(config = {}) {
71
74
  if (value === null || value === undefined) {
72
75
  return '';
73
76
  }
74
-
77
+
75
78
  const valueType = typeof value;
76
- const isComplex = valueType === 'object' &&
77
- (Array.isArray(value) ||
79
+ const isComplex = valueType === 'object' &&
80
+ (Array.isArray(value) ||
78
81
  (Object.keys(value).length >= this.config.complexThreshold));
79
-
82
+
80
83
  // Select serialization strategy based on configuration and value type
81
- if (this.config.mode === 'escape' ||
84
+ if (this.config.mode === 'escape' ||
82
85
  (this.config.mode === 'hybrid' && !isComplex)) {
83
86
  // Use escape strategy for primitives or when escape mode is forced
84
87
  if (valueType === 'string') {
@@ -96,7 +99,7 @@ function createSerializer(config = {}) {
96
99
  return JSON.stringify(value);
97
100
  }
98
101
  },
99
-
102
+
100
103
  /**
101
104
  * Deserialize a value from CSS variable storage
102
105
  * @param {string} key - The state key (for context-aware deserialization)
@@ -106,10 +109,10 @@ function createSerializer(config = {}) {
106
109
  deserialize(key, value) {
107
110
  // Handle empty values
108
111
  if (!value) return '';
109
-
112
+
110
113
  // Try JSON parse first for values that look like JSON
111
- if (this.config.mode !== 'escape' &&
112
- ((value.startsWith('{') && value.endsWith('}')) ||
114
+ if (this.config.mode !== 'escape' &&
115
+ ((value.startsWith('{') && value.endsWith('}')) ||
113
116
  (value.startsWith('[') && value.endsWith(']')))) {
114
117
  try {
115
118
  return JSON.parse(value);
@@ -120,13 +123,13 @@ function createSerializer(config = {}) {
120
123
  // Fall through to unescaping if JSON parse fails
121
124
  }
122
125
  }
123
-
126
+
124
127
  // For non-JSON or escape mode, try unescaping
125
128
  const unescaped = unescapeCssValue(value);
126
-
129
+
127
130
  // If unescaped looks like JSON (might have been double-escaped), try parsing it
128
131
  if (this.config.mode !== 'escape' &&
129
- ((unescaped.startsWith('{') && unescaped.endsWith('}')) ||
132
+ ((unescaped.startsWith('{') && unescaped.endsWith('}')) ||
130
133
  (unescaped.startsWith('[') && unescaped.endsWith(']')))) {
131
134
  try {
132
135
  return JSON.parse(unescaped);
@@ -134,10 +137,70 @@ function createSerializer(config = {}) {
134
137
  // Not valid JSON, return unescaped string
135
138
  }
136
139
  }
137
-
140
+
138
141
  return unescaped;
139
142
  },
140
-
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
+
141
204
  /**
142
205
  * Utility method to determine if a value needs complex serialization
143
206
  * @param {any} value - Value to check
@@ -146,7 +209,7 @@ function createSerializer(config = {}) {
146
209
  needsComplexSerialization(value) {
147
210
  return typeof value === 'object' && value !== null;
148
211
  },
149
-
212
+
150
213
  /**
151
214
  * Set state with proper serialization for CSS variables
152
215
  * @param {Object} uistate - UIstate instance
@@ -157,23 +220,23 @@ function createSerializer(config = {}) {
157
220
  setStateWithCss(uistate, path, value) {
158
221
  // Update UIstate
159
222
  uistate.setState(path, value);
160
-
223
+
161
224
  // Update CSS variable with properly serialized value
162
225
  const cssPath = path.replace(/\./g, '-');
163
226
  const serialized = this.serialize(path, value);
164
227
  document.documentElement.style.setProperty(`--${cssPath}`, serialized);
165
-
228
+
166
229
  // Update data attribute for root level state
167
230
  const segments = path.split('.');
168
231
  if (segments.length === 1) {
169
- document.documentElement.dataset[path] = typeof value === 'object'
170
- ? JSON.stringify(value)
232
+ document.documentElement.dataset[path] = typeof value === 'object'
233
+ ? JSON.stringify(value)
171
234
  : value;
172
235
  }
173
-
236
+
174
237
  return value;
175
238
  },
176
-
239
+
177
240
  /**
178
241
  * Get state with fallback to CSS variables
179
242
  * @param {Object} uistate - UIstate instance
@@ -184,16 +247,16 @@ function createSerializer(config = {}) {
184
247
  // First try UIstate
185
248
  const value = uistate.getState(path);
186
249
  if (value !== undefined) return value;
187
-
250
+
188
251
  // If not found, try CSS variable
189
252
  const cssPath = path.replace(/\./g, '-');
190
253
  const cssValue = getComputedStyle(document.documentElement)
191
254
  .getPropertyValue(`--${cssPath}`).trim();
192
-
255
+
193
256
  return cssValue ? this.deserialize(path, cssValue) : undefined;
194
257
  }
195
258
  };
196
-
259
+
197
260
  return serializer;
198
261
  }
199
262