@uistate/core 2.0.0 → 3.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/src/index.js CHANGED
@@ -3,58 +3,211 @@
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
+ * Provides a simple, declarative API for state management
7
+ * Uses the DOM as the source of truth for state
8
+ * Supports plugins through a clean composition system
6
9
  */
7
- import { createCssState } from './cssState.js';
10
+ import createCssState from './cssState.js';
8
11
  import { createEventState } from './eventState.js';
9
12
  import { createTemplateManager } from './templateManager.js';
10
13
 
14
+ /**
15
+ * Utility function to convert hierarchical path notation to CSS variable notation
16
+ * Converts dot notation (e.g., 'user.profile.name') to dash notation (e.g., 'user-profile-name')
17
+ * @param {string} path - Hierarchical path with dot notation
18
+ * @returns {string} - CSS-compatible path with dash notation
19
+ */
20
+ function convertPathToCssPath(path) {
21
+ return path.replace(/\./g, "-");
22
+ }
23
+
11
24
  const createUnifiedState = (initialState = {}) => {
12
- // Initialize state store
13
- const store = JSON.parse(JSON.stringify(initialState));
14
-
15
- // Create the CSS state manager
25
+ // Create the CSS state manager with integrated serialization
16
26
  const cssState = createCssState(initialState);
17
27
 
18
28
  // Create the event state manager with the same initial state
19
29
  const eventState = createEventState(initialState);
20
30
 
31
+ // Plugin system
32
+ const plugins = new Map();
33
+ const middlewares = [];
34
+ const lifecycleHooks = {
35
+ beforeStateChange: [],
36
+ afterStateChange: [],
37
+ onInit: [],
38
+ onDestroy: []
39
+ };
40
+
21
41
  // Create a unified API
22
42
  const unifiedState = {
23
- _isNotifying: false, // Flag to prevent recursive notifications
43
+ _isNotifying: false,
44
+ _cssState: cssState, // Reference to cssState for direct access to advanced features
24
45
 
25
46
  // Get state with hierarchical path support
26
47
  getState(path) {
27
- if (!path) return store;
28
-
29
- // Try to get from event state first (faster)
48
+ // Always use eventState as the source of truth
49
+ // This ensures consistency with the DOM state
30
50
  const value = eventState.get(path);
31
51
 
32
- if (value !== undefined) return value;
52
+ // If path is undefined or value not found in eventState
53
+ if (path && value === undefined) {
54
+ // Fall back to CSS variable (for values set outside this API)
55
+ const cssPath = convertPathToCssPath(path);
56
+ return cssState.getState(cssPath);
57
+ }
33
58
 
34
- // Fall back to CSS variable (for values set outside this API)
35
- const cssPath = path.replace(/\./g, "-");
36
- return cssState.getState(cssPath);
59
+ return value;
37
60
  },
38
61
 
39
62
  // Set state with hierarchical path support
40
63
  setState(path, value) {
41
- // Prevent recursive notifications
42
- if (this._isNotifying) return value;
64
+ if (!path) return this;
65
+
66
+ // Run before state change middlewares
67
+ let finalValue = value;
68
+ let shouldContinue = true;
69
+
70
+ // Apply middlewares
71
+ for (const middleware of middlewares) {
72
+ const result = middleware(path, finalValue, this.getState.bind(this));
73
+ if (result === false) {
74
+ shouldContinue = false;
75
+ break;
76
+ } else if (result !== undefined) {
77
+ finalValue = result;
78
+ }
79
+ }
80
+
81
+ // Run lifecycle hooks
82
+ for (const hook of lifecycleHooks.beforeStateChange) {
83
+ hook(path, finalValue, this.getState.bind(this));
84
+ }
85
+
86
+ if (!shouldContinue) return this;
87
+
88
+ // Update event state (for JS access and pub/sub)
89
+ eventState.set(path, finalValue);
90
+
91
+ // Update CSS state (for styling and DOM representation)
92
+ const cssPath = convertPathToCssPath(path);
93
+ cssState.setState(cssPath, finalValue);
94
+
95
+ // Run after state change hooks
96
+ for (const hook of lifecycleHooks.afterStateChange) {
97
+ hook(path, finalValue, this.getState.bind(this));
98
+ }
99
+
100
+ return this;
101
+ },
102
+
103
+ // Plugin system methods
104
+ use(pluginName, plugin) {
105
+ if (plugins.has(pluginName)) {
106
+ console.warn(`Plugin '${pluginName}' is already registered. It will be replaced.`);
107
+ }
108
+
109
+ // Register the plugin
110
+ plugins.set(pluginName, plugin);
111
+
112
+ // Initialize the plugin
113
+ if (typeof plugin.init === 'function') {
114
+ // Pass the API to the plugin
115
+ plugin.init(this);
116
+ }
117
+
118
+ // Register middlewares
119
+ if (Array.isArray(plugin.middlewares)) {
120
+ middlewares.push(...plugin.middlewares);
121
+ }
122
+
123
+ // Register lifecycle hooks
124
+ if (plugin.hooks) {
125
+ Object.entries(plugin.hooks).forEach(([hookName, hookFn]) => {
126
+ if (Array.isArray(lifecycleHooks[hookName])) {
127
+ lifecycleHooks[hookName].push(hookFn);
128
+ }
129
+ });
130
+ }
131
+
132
+ // Add plugin methods to the API
133
+ if (plugin.methods) {
134
+ Object.entries(plugin.methods).forEach(([methodName, method]) => {
135
+ if (typeof method === 'function') {
136
+ this[methodName] = method.bind(plugin);
137
+ }
138
+ });
139
+ }
140
+
141
+ return this;
142
+ },
143
+
144
+ getPlugin(pluginName) {
145
+ return plugins.get(pluginName);
146
+ },
147
+
148
+ hasPlugin(pluginName) {
149
+ return plugins.has(pluginName);
150
+ },
151
+
152
+ removePlugin(pluginName) {
153
+ const plugin = plugins.get(pluginName);
154
+ if (!plugin) return false;
155
+
156
+ // Run cleanup if available
157
+ if (typeof plugin.destroy === 'function') {
158
+ plugin.destroy();
159
+ }
160
+
161
+ // Remove plugin methods
162
+ if (plugin.methods) {
163
+ Object.keys(plugin.methods).forEach(methodName => {
164
+ delete this[methodName];
165
+ });
166
+ }
167
+
168
+ // Remove middlewares
169
+ if (Array.isArray(plugin.middlewares)) {
170
+ plugin.middlewares.forEach(middleware => {
171
+ const index = middlewares.indexOf(middleware);
172
+ if (index !== -1) {
173
+ middlewares.splice(index, 1);
174
+ }
175
+ });
176
+ }
43
177
 
44
- this._isNotifying = true;
178
+ // Remove lifecycle hooks
179
+ if (plugin.hooks) {
180
+ Object.entries(plugin.hooks).forEach(([hookName, hookFn]) => {
181
+ if (Array.isArray(lifecycleHooks[hookName])) {
182
+ const index = lifecycleHooks[hookName].indexOf(hookFn);
183
+ if (index !== -1) {
184
+ lifecycleHooks[hookName].splice(index, 1);
185
+ }
186
+ }
187
+ });
188
+ }
45
189
 
46
- try {
47
- // Update event state
48
- eventState.set(path, value);
49
-
50
- // Update CSS state (convert dots to dashes for CSS variables)
51
- const cssPath = path.replace(/\./g, "-");
52
- cssState.setState(cssPath, value);
53
-
54
- return value;
55
- } finally {
56
- this._isNotifying = false;
190
+ // Remove the plugin
191
+ plugins.delete(pluginName);
192
+ return true;
193
+ },
194
+
195
+ // Add middleware
196
+ addMiddleware(middleware) {
197
+ if (typeof middleware === 'function') {
198
+ middlewares.push(middleware);
199
+ return true;
57
200
  }
201
+ return false;
202
+ },
203
+
204
+ // Add lifecycle hook
205
+ addHook(hookName, hookFn) {
206
+ if (typeof hookFn === 'function' && Array.isArray(lifecycleHooks[hookName])) {
207
+ lifecycleHooks[hookName].push(hookFn);
208
+ return true;
209
+ }
210
+ return false;
58
211
  },
59
212
 
60
213
  // Subscribe to state changes with support for wildcards
@@ -67,8 +220,44 @@ const createUnifiedState = (initialState = {}) => {
67
220
  return cssState.observe(key, callback);
68
221
  },
69
222
 
223
+ // Configure serialization options
224
+ configureSerializer(config) {
225
+ cssState.configureSerializer(config);
226
+ return this;
227
+ },
228
+
229
+ // Get current serializer configuration
230
+ getSerializerConfig() {
231
+ return cssState._serializer?.config || null;
232
+ },
233
+
234
+ // Set serialization mode ('hybrid', 'json', or 'escape')
235
+ setSerializationMode(mode) {
236
+ return this.configureSerializer({ mode });
237
+ },
238
+
70
239
  // Clean up resources
71
240
  destroy() {
241
+ // Run onDestroy hooks
242
+ for (const hook of lifecycleHooks.onDestroy) {
243
+ hook();
244
+ }
245
+
246
+ // Destroy all plugins
247
+ for (const [pluginName, plugin] of plugins.entries()) {
248
+ if (typeof plugin.destroy === 'function') {
249
+ plugin.destroy();
250
+ }
251
+ }
252
+
253
+ // Clear plugins and middlewares
254
+ plugins.clear();
255
+ middlewares.length = 0;
256
+ Object.keys(lifecycleHooks).forEach(key => {
257
+ lifecycleHooks[key].length = 0;
258
+ });
259
+
260
+ // Destroy core state management
72
261
  cssState.destroy();
73
262
  eventState.destroy();
74
263
  }
@@ -80,23 +269,75 @@ const createUnifiedState = (initialState = {}) => {
80
269
  // Create a singleton instance of the unified state
81
270
  const UnifiedState = createUnifiedState();
82
271
 
83
- // Create a template manager connected to the unified state
84
- const TemplateManager = createTemplateManager(UnifiedState);
272
+ // Create a template manager plugin
273
+ const templateManagerPlugin = {
274
+ name: 'templateManager',
275
+ instance: null,
276
+
277
+ init(api) {
278
+ this.instance = createTemplateManager(api);
279
+ return this;
280
+ },
281
+
282
+ destroy() {
283
+ // Any cleanup needed for the template manager
284
+ },
285
+
286
+ // Plugin methods that will be added to the UIstate API
287
+ methods: {
288
+ onAction(action, handler) {
289
+ return this.instance.onAction(action, handler);
290
+ },
291
+
292
+ registerActions(actionsMap) {
293
+ return this.instance.registerActions(actionsMap);
294
+ },
295
+
296
+ attachDelegation(root = document.body) {
297
+ return this.instance.attachDelegation(root);
298
+ },
299
+
300
+ mount(componentName, container) {
301
+ return this.instance.mount(componentName, container);
302
+ },
303
+
304
+ renderTemplateFromCss(templateName, data) {
305
+ return this.instance.renderTemplateFromCss(templateName, data);
306
+ },
307
+
308
+ createComponent(name, renderFn, stateKeys) {
309
+ return this.instance.createComponent(name, renderFn, stateKeys);
310
+ },
311
+
312
+ applyClassesFromState(element, stateKey, options) {
313
+ return this.instance.applyClassesFromState(element, stateKey, options);
314
+ }
315
+ },
316
+
317
+ // Expose handlers property
318
+ get handlers() {
319
+ return this.instance.handlers;
320
+ }
321
+ };
85
322
 
86
- // Create a combined API for backward compatibility
323
+ // Create a combined API with plugin support
87
324
  const UIstate = {
88
325
  ...UnifiedState,
89
326
 
90
- // Add template manager methods
91
- handlers: TemplateManager.handlers,
92
- onAction: TemplateManager.onAction.bind(TemplateManager),
93
- attachDelegation: TemplateManager.attachDelegation.bind(TemplateManager),
94
- mount: TemplateManager.mount.bind(TemplateManager),
95
-
96
- // Initialize both systems
327
+ // Initialize the system with plugins
97
328
  init() {
329
+ // Register the template manager plugin
330
+ this.use('templateManager', templateManagerPlugin);
331
+
332
+ // Run onInit hooks
333
+ for (const hook of this.getPlugin('templateManager').instance.lifecycleHooks?.onInit || []) {
334
+ // for (const hook of lifecycleHooks.onInit) {
335
+ hook();
336
+ }
337
+
98
338
  // Attach event delegation to document body
99
339
  this.attachDelegation(document.body);
340
+
100
341
  return this;
101
342
  }
102
343
  };
@@ -104,8 +345,9 @@ const UIstate = {
104
345
  export default UIstate;
105
346
  export {
106
347
  createUnifiedState,
107
- UnifiedState,
348
+ createCssState,
349
+ createEventState,
108
350
  createTemplateManager,
109
- TemplateManager,
110
- UIstate
351
+ convertPathToCssPath,
352
+ templateManagerPlugin
111
353
  };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * StateInspector - Real-time state inspection panel for UIstate
3
+ *
4
+ * This module provides a configurable UI panel that displays and allows manipulation
5
+ * of CSS variables and state values in UIstate applications.
6
+ *
7
+ * Features:
8
+ * - Toggle visibility with a dedicated button
9
+ * - Real-time updates of CSS variable values
10
+ * - Organized display of state by categories
11
+ * - Ability to filter state variables
12
+ * - Direct manipulation of state values
13
+ */
14
+
15
+ /**
16
+ * Create a configured state inspector instance
17
+ * @param {Object} config - Configuration options
18
+ * @returns {Object} - StateInspector instance
19
+ */
20
+ function createStateInspector(config = {}) {
21
+ // Default configuration
22
+ const defaultConfig = {
23
+ target: document.body,
24
+ initiallyVisible: false,
25
+ categories: ['app', 'ui', 'data'],
26
+ position: 'bottom-right',
27
+ maxHeight: '300px',
28
+ stateManager: null, // Optional reference to UIstate or CssState instance
29
+ theme: 'light'
30
+ };
31
+
32
+ // Merge provided config with defaults
33
+ const options = { ...defaultConfig, ...config };
34
+
35
+ // Private state
36
+ let isVisible = options.initiallyVisible;
37
+ let panel = null;
38
+ let toggleButton = null;
39
+ let filterInput = null;
40
+ let stateContainer = null;
41
+ let currentFilter = '';
42
+
43
+ // StateInspector instance
44
+ const inspector = {
45
+ // Current configuration
46
+ config: options,
47
+
48
+ /**
49
+ * Update configuration
50
+ * @param {Object} newConfig - New configuration options
51
+ */
52
+ configure(newConfig) {
53
+ Object.assign(this.config, newConfig);
54
+ this.refresh();
55
+ },
56
+
57
+ /**
58
+ * Attach the inspector panel to the target element
59
+ * @param {Element} element - Target element to attach to (defaults to config.target)
60
+ * @returns {Object} - The inspector instance for chaining
61
+ */
62
+ attach(element = this.config.target) {
63
+ // Implementation will create and attach the panel
64
+ // For now, this is a placeholder
65
+ console.log('StateInspector: Panel would be attached to', element);
66
+ return this;
67
+ },
68
+
69
+ /**
70
+ * Show the inspector panel
71
+ * @returns {Object} - The inspector instance for chaining
72
+ */
73
+ show() {
74
+ isVisible = true;
75
+ if (panel) panel.style.display = 'block';
76
+ return this;
77
+ },
78
+
79
+ /**
80
+ * Hide the inspector panel
81
+ * @returns {Object} - The inspector instance for chaining
82
+ */
83
+ hide() {
84
+ isVisible = false;
85
+ if (panel) panel.style.display = 'none';
86
+ return this;
87
+ },
88
+
89
+ /**
90
+ * Toggle the visibility of the inspector panel
91
+ * @returns {Object} - The inspector instance for chaining
92
+ */
93
+ toggle() {
94
+ return isVisible ? this.hide() : this.show();
95
+ },
96
+
97
+ /**
98
+ * Refresh the state display
99
+ * @returns {Object} - The inspector instance for chaining
100
+ */
101
+ refresh() {
102
+ // Implementation will update the displayed state values
103
+ // For now, this is a placeholder
104
+ console.log('StateInspector: State display would be refreshed');
105
+ return this;
106
+ },
107
+
108
+ /**
109
+ * Filter the displayed state variables
110
+ * @param {string} filterText - Text to filter by
111
+ * @returns {Object} - The inspector instance for chaining
112
+ */
113
+ filter(filterText) {
114
+ currentFilter = filterText;
115
+ // Implementation will filter the displayed variables
116
+ return this.refresh();
117
+ },
118
+
119
+ /**
120
+ * Clean up resources used by the inspector
121
+ */
122
+ destroy() {
123
+ if (panel && panel.parentNode) {
124
+ panel.parentNode.removeChild(panel);
125
+ }
126
+ panel = null;
127
+ toggleButton = null;
128
+ filterInput = null;
129
+ stateContainer = null;
130
+ }
131
+ };
132
+
133
+ return inspector;
134
+ }
135
+
136
+ // Create a default instance
137
+ const StateInspector = createStateInspector();
138
+
139
+ export default StateInspector;
140
+ export { createStateInspector };