@uistate/core 4.1.2 → 5.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/LICENSE-eventTest.md +26 -0
- package/README.md +409 -42
- package/cssState.js +32 -1
- package/eventState.js +58 -49
- package/eventTest.js +196 -0
- package/examples/001-counter/README.md +44 -0
- package/examples/001-counter/eventState.js +86 -0
- package/examples/001-counter/index.html +30 -0
- package/examples/002-counter-improved/README.md +44 -0
- package/examples/002-counter-improved/eventState.js +86 -0
- package/examples/002-counter-improved/index.html +44 -0
- package/examples/003-input-reactive/README.md +44 -0
- package/examples/003-input-reactive/eventState.js +86 -0
- package/examples/003-input-reactive/index.html +30 -0
- package/examples/004-computed-state/README.md +45 -0
- package/examples/004-computed-state/eventState.js +86 -0
- package/examples/004-computed-state/index.html +62 -0
- package/examples/005-conditional-rendering/README.md +42 -0
- package/examples/005-conditional-rendering/eventState.js +86 -0
- package/examples/005-conditional-rendering/index.html +36 -0
- package/examples/006-list-rendering/README.md +49 -0
- package/examples/006-list-rendering/eventState.js +86 -0
- package/examples/006-list-rendering/index.html +60 -0
- package/examples/007-form-validation/README.md +52 -0
- package/examples/007-form-validation/eventState.js +86 -0
- package/examples/007-form-validation/index.html +99 -0
- package/examples/008-undo-redo/README.md +70 -0
- package/examples/008-undo-redo/eventState.js +86 -0
- package/examples/008-undo-redo/index.html +105 -0
- package/examples/009-localStorage-side-effects/README.md +72 -0
- package/examples/009-localStorage-side-effects/eventState.js +86 -0
- package/examples/009-localStorage-side-effects/index.html +54 -0
- package/examples/010-decoupled-components/README.md +74 -0
- package/examples/010-decoupled-components/eventState.js +86 -0
- package/examples/010-decoupled-components/index.html +90 -0
- package/examples/011-async-patterns/README.md +98 -0
- package/examples/011-async-patterns/eventState.js +86 -0
- package/examples/011-async-patterns/index.html +129 -0
- package/examples/028-counter-improved-eventTest/LICENSE +55 -0
- package/examples/028-counter-improved-eventTest/README.md +131 -0
- package/examples/028-counter-improved-eventTest/app/store.js +9 -0
- package/examples/028-counter-improved-eventTest/index.html +49 -0
- package/examples/028-counter-improved-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/examples/028-counter-improved-eventTest/runtime/core/eventState.js +100 -0
- package/examples/028-counter-improved-eventTest/runtime/core/helpers.js +212 -0
- package/examples/028-counter-improved-eventTest/runtime/core/router.js +271 -0
- package/examples/028-counter-improved-eventTest/store.d.ts +8 -0
- package/examples/028-counter-improved-eventTest/style.css +170 -0
- package/examples/028-counter-improved-eventTest/tests/README.md +208 -0
- package/examples/028-counter-improved-eventTest/tests/counter.test.js +116 -0
- package/examples/028-counter-improved-eventTest/tests/eventTest.js +176 -0
- package/examples/028-counter-improved-eventTest/tests/generateTypes.js +168 -0
- package/examples/028-counter-improved-eventTest/tests/run.js +20 -0
- package/examples/030-todo-app-with-eventTest/LICENSE +55 -0
- package/examples/030-todo-app-with-eventTest/README.md +121 -0
- package/examples/030-todo-app-with-eventTest/app/router.js +25 -0
- package/examples/030-todo-app-with-eventTest/app/store.js +16 -0
- package/examples/030-todo-app-with-eventTest/app/views/home.js +11 -0
- package/examples/030-todo-app-with-eventTest/app/views/todoDemo.js +88 -0
- package/examples/030-todo-app-with-eventTest/index.html +65 -0
- package/examples/030-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/examples/030-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/examples/030-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/examples/030-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/examples/030-todo-app-with-eventTest/store.d.ts +18 -0
- package/examples/030-todo-app-with-eventTest/style.css +170 -0
- package/examples/030-todo-app-with-eventTest/tests/README.md +208 -0
- package/examples/030-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/examples/030-todo-app-with-eventTest/tests/generateTypes.js +189 -0
- package/examples/030-todo-app-with-eventTest/tests/run.js +20 -0
- package/examples/030-todo-app-with-eventTest/tests/todos.test.js +167 -0
- package/examples/031-todo-app-with-eventTest/LICENSE +55 -0
- package/examples/031-todo-app-with-eventTest/README.md +54 -0
- package/examples/031-todo-app-with-eventTest/TUTORIAL.md +390 -0
- package/examples/031-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
- package/examples/031-todo-app-with-eventTest/app/bridges.js +113 -0
- package/examples/031-todo-app-with-eventTest/app/router.js +26 -0
- package/examples/031-todo-app-with-eventTest/app/store.js +15 -0
- package/examples/031-todo-app-with-eventTest/app/views/home.js +46 -0
- package/examples/031-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
- package/examples/031-todo-app-with-eventTest/devtools/dock.js +41 -0
- package/examples/031-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
- package/examples/031-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
- package/examples/031-todo-app-with-eventTest/devtools/telemetry.js +104 -0
- package/examples/031-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
- package/examples/031-todo-app-with-eventTest/index.html +103 -0
- package/examples/031-todo-app-with-eventTest/package-lock.json +2184 -0
- package/examples/031-todo-app-with-eventTest/package.json +24 -0
- package/examples/031-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/examples/031-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/examples/031-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/examples/031-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
- package/examples/031-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
- package/examples/031-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
- package/examples/031-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
- package/examples/031-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
- package/examples/031-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
- package/examples/031-todo-app-with-eventTest/store.d.ts +23 -0
- package/examples/031-todo-app-with-eventTest/style.css +170 -0
- package/examples/031-todo-app-with-eventTest/tests/README.md +208 -0
- package/examples/031-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/examples/031-todo-app-with-eventTest/tests/generateTypes.js +191 -0
- package/examples/031-todo-app-with-eventTest/tests/run.js +20 -0
- package/examples/031-todo-app-with-eventTest/tests/todos.test.js +192 -0
- package/examples/032-todo-app-with-eventTest/LICENSE +55 -0
- package/examples/032-todo-app-with-eventTest/README.md +54 -0
- package/examples/032-todo-app-with-eventTest/TUTORIAL.md +390 -0
- package/examples/032-todo-app-with-eventTest/WHY_EVENTSTATE.md +777 -0
- package/examples/032-todo-app-with-eventTest/app/actions/index.js +153 -0
- package/examples/032-todo-app-with-eventTest/app/bridges.js +113 -0
- package/examples/032-todo-app-with-eventTest/app/router.js +26 -0
- package/examples/032-todo-app-with-eventTest/app/store.js +15 -0
- package/examples/032-todo-app-with-eventTest/app/views/home.js +46 -0
- package/examples/032-todo-app-with-eventTest/app/views/todoDemo.js +69 -0
- package/examples/032-todo-app-with-eventTest/devtools/dock.js +41 -0
- package/examples/032-todo-app-with-eventTest/devtools/stateTracker.dock.js +10 -0
- package/examples/032-todo-app-with-eventTest/devtools/stateTracker.js +246 -0
- package/examples/032-todo-app-with-eventTest/devtools/telemetry.js +104 -0
- package/examples/032-todo-app-with-eventTest/devtools/typeGenerator.js +339 -0
- package/examples/032-todo-app-with-eventTest/index.html +87 -0
- package/examples/032-todo-app-with-eventTest/package-lock.json +2184 -0
- package/examples/032-todo-app-with-eventTest/package.json +24 -0
- package/examples/032-todo-app-with-eventTest/runtime/core/behaviors.runtime.js +282 -0
- package/examples/032-todo-app-with-eventTest/runtime/core/eventState.js +100 -0
- package/examples/032-todo-app-with-eventTest/runtime/core/helpers.js +212 -0
- package/examples/032-todo-app-with-eventTest/runtime/core/router.js +271 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/boundary.js +36 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/converge.js +63 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/eventState.plus.js +210 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/hydrate.js +157 -0
- package/examples/032-todo-app-with-eventTest/runtime/extensions/queryBinding.js +69 -0
- package/examples/032-todo-app-with-eventTest/runtime/forms/computed.js +78 -0
- package/examples/032-todo-app-with-eventTest/runtime/forms/meta.js +51 -0
- package/examples/032-todo-app-with-eventTest/runtime/forms/submitWithBoundary.js +28 -0
- package/examples/032-todo-app-with-eventTest/runtime/forms/validators.js +55 -0
- package/examples/032-todo-app-with-eventTest/store.d.ts +23 -0
- package/examples/032-todo-app-with-eventTest/style.css +170 -0
- package/examples/032-todo-app-with-eventTest/tests/README.md +208 -0
- package/examples/032-todo-app-with-eventTest/tests/eventTest.js +176 -0
- package/examples/032-todo-app-with-eventTest/tests/generateTypes.js +191 -0
- package/examples/032-todo-app-with-eventTest/tests/run.js +20 -0
- package/examples/032-todo-app-with-eventTest/tests/todos.test.js +192 -0
- package/index.js +14 -3
- package/package.json +16 -7
- package/stateSerializer.js +99 -4
- package/templateManager.js +50 -2
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* todos.test.js - Event-sequence tests for todo functionality
|
|
3
|
+
*
|
|
4
|
+
* These tests define the behavior AND types of the todo domain
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createEventTest, test, runTests } from './eventTest.js';
|
|
8
|
+
|
|
9
|
+
// Helper to set up todo bridges on a test store
|
|
10
|
+
function setupTodoBridges(store) {
|
|
11
|
+
let nextId = 1;
|
|
12
|
+
|
|
13
|
+
// Add todo
|
|
14
|
+
store.subscribe('intent.todo.add', (detail) => {
|
|
15
|
+
const { text } = detail;
|
|
16
|
+
const items = store.get('domain.todos.items') || [];
|
|
17
|
+
const todo = { id: nextId++, text: String(text || '').trim(), done: false };
|
|
18
|
+
if (!todo.text) return;
|
|
19
|
+
store.set('domain.todos.items', [...items, todo]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Toggle todo
|
|
23
|
+
store.subscribe('intent.todo.toggle', (detail) => {
|
|
24
|
+
const { id } = detail;
|
|
25
|
+
const items = store.get('domain.todos.items') || [];
|
|
26
|
+
const out = items.map(t => (String(t?.id) === String(id)) ? { ...t, done: !t.done } : t);
|
|
27
|
+
store.set('domain.todos.items', out);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Clear completed
|
|
31
|
+
store.subscribe('intent.todo.clearCompleted', () => {
|
|
32
|
+
const items = store.get('domain.todos.items') || [];
|
|
33
|
+
store.set('domain.todos.items', items.filter(t => !t.done));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// UI filter
|
|
37
|
+
store.subscribe('intent.ui.filter', (detail) => {
|
|
38
|
+
const { filter } = detail;
|
|
39
|
+
const f = (filter === 'active' || filter === 'completed') ? filter : 'all';
|
|
40
|
+
store.set('ui.todos.filter', f);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Test suite
|
|
45
|
+
const tests = {
|
|
46
|
+
'add todo creates correct structure': () => {
|
|
47
|
+
const t = createEventTest({
|
|
48
|
+
domain: { todos: { items: [] } }
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Set up bridges for this test
|
|
52
|
+
setupTodoBridges(t.store);
|
|
53
|
+
|
|
54
|
+
// Trigger intent
|
|
55
|
+
t.trigger('intent.todo.add', { text: 'Buy milk' });
|
|
56
|
+
|
|
57
|
+
// Assert array structure and element types
|
|
58
|
+
t.assertArrayOf('domain.todos.items', {
|
|
59
|
+
id: 'number',
|
|
60
|
+
text: 'string',
|
|
61
|
+
done: 'boolean'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Assert array length
|
|
65
|
+
t.assertArrayLength('domain.todos.items', 1);
|
|
66
|
+
|
|
67
|
+
// Assert specific values
|
|
68
|
+
const items = t.store.get('domain.todos.items');
|
|
69
|
+
if (items[0].text !== 'Buy milk') {
|
|
70
|
+
throw new Error('Expected first todo text to be "Buy milk"');
|
|
71
|
+
}
|
|
72
|
+
if (items[0].done !== false) {
|
|
73
|
+
throw new Error('Expected first todo to not be done');
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
'toggle todo changes done state': () => {
|
|
78
|
+
const t = createEventTest({
|
|
79
|
+
domain: { todos: { items: [{ id: 1, text: 'Buy milk', done: false }] } }
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Set up bridges for this test
|
|
83
|
+
setupTodoBridges(t.store);
|
|
84
|
+
|
|
85
|
+
// Trigger toggle
|
|
86
|
+
t.trigger('intent.todo.toggle', { id: 1 });
|
|
87
|
+
|
|
88
|
+
// Assert structure
|
|
89
|
+
t.assertArrayOf('domain.todos.items', {
|
|
90
|
+
id: 'number',
|
|
91
|
+
text: 'string',
|
|
92
|
+
done: 'boolean'
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Assert done is now true
|
|
96
|
+
const items = t.store.get('domain.todos.items');
|
|
97
|
+
if (items[0].done !== true) {
|
|
98
|
+
throw new Error('Expected todo to be done');
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
'clear completed removes done todos': () => {
|
|
103
|
+
const t = createEventTest({
|
|
104
|
+
domain: {
|
|
105
|
+
todos: {
|
|
106
|
+
items: [
|
|
107
|
+
{ id: 1, text: 'Buy milk', done: true },
|
|
108
|
+
{ id: 2, text: 'Walk dog', done: false }
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Set up bridges for this test
|
|
115
|
+
setupTodoBridges(t.store);
|
|
116
|
+
|
|
117
|
+
// Trigger clear
|
|
118
|
+
t.trigger('intent.todo.clearCompleted');
|
|
119
|
+
|
|
120
|
+
// Assert only active todo remains
|
|
121
|
+
t.assertArrayLength('domain.todos.items', 1);
|
|
122
|
+
|
|
123
|
+
const items = t.store.get('domain.todos.items');
|
|
124
|
+
if (items[0].text !== 'Walk dog') {
|
|
125
|
+
throw new Error('Expected only "Walk dog" to remain');
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
'filter intent updates filter state': () => {
|
|
130
|
+
const t = createEventTest({
|
|
131
|
+
ui: { todos: { filter: 'all' } }
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Set up bridges for this test
|
|
135
|
+
setupTodoBridges(t.store);
|
|
136
|
+
|
|
137
|
+
// Trigger filter change
|
|
138
|
+
t.trigger('intent.ui.filter', { filter: 'active' });
|
|
139
|
+
|
|
140
|
+
// Assert type
|
|
141
|
+
t.assertType('ui.todos.filter', 'string');
|
|
142
|
+
|
|
143
|
+
// Assert value
|
|
144
|
+
t.assertPath('ui.todos.filter', 'active');
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
'intent.todo.add has correct shape': () => {
|
|
148
|
+
const t = createEventTest({});
|
|
149
|
+
|
|
150
|
+
// Trigger intent
|
|
151
|
+
t.trigger('intent.todo.add', { text: 'Buy milk' });
|
|
152
|
+
|
|
153
|
+
// Assert intent shape (for type generation)
|
|
154
|
+
t.assertShape('intent.todo.add', {
|
|
155
|
+
text: 'string'
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
'intent.todo.toggle has correct shape': () => {
|
|
160
|
+
const t = createEventTest({});
|
|
161
|
+
|
|
162
|
+
// Trigger intent
|
|
163
|
+
t.trigger('intent.todo.toggle', { id: 1 });
|
|
164
|
+
|
|
165
|
+
// Assert intent shape
|
|
166
|
+
t.assertShape('intent.todo.toggle', {
|
|
167
|
+
id: 'number'
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
'empty todos array is valid': () => {
|
|
172
|
+
const t = createEventTest({
|
|
173
|
+
domain: { todos: { items: [] } }
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Assert empty array is still typed correctly
|
|
177
|
+
t.assertArrayOf('domain.todos.items', {
|
|
178
|
+
id: 'number',
|
|
179
|
+
text: 'string',
|
|
180
|
+
done: 'boolean'
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
t.assertArrayLength('domain.todos.items', 0);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Run tests if executed directly
|
|
188
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
189
|
+
runTests(tests);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default tests;
|
package/index.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* UIstate v5 - Core barrel exports
|
|
3
|
+
*
|
|
4
|
+
* EventState is now the primary export for application state management.
|
|
5
|
+
* cssState remains available for CSS variable/theme management use cases.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Primary: EventState (recommended for application state)
|
|
2
9
|
export { createEventState } from './eventState.js';
|
|
10
|
+
export { createEventState as default } from './eventState.js';
|
|
11
|
+
|
|
12
|
+
// Specialized: CSS State (for CSS variables and theme management)
|
|
13
|
+
export { createCssState } from './cssState.js';
|
|
14
|
+
|
|
15
|
+
// Utilities
|
|
3
16
|
export { default as stateSerializer } from './stateSerializer.js';
|
|
4
17
|
export { createTemplateManager } from './templateManager.js';
|
|
5
|
-
|
|
6
|
-
export { createCssState as default } from './cssState.js';
|
package/package.json
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uistate/core",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "Lightweight event-driven state management with slot orchestration and experimental event-sequence testing (eventTest.js available under dual license)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.js",
|
|
9
|
-
"./cssState": "./cssState.js",
|
|
10
9
|
"./eventState": "./eventState.js",
|
|
10
|
+
"./cssState": "./cssState.js",
|
|
11
11
|
"./stateSerializer": "./stateSerializer.js",
|
|
12
12
|
"./templateManager": "./templateManager.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"index.js",
|
|
16
|
+
"eventState.js",
|
|
16
17
|
"cssState.js",
|
|
17
|
-
"eventState.js",
|
|
18
18
|
"stateSerializer.js",
|
|
19
|
-
"templateManager.js"
|
|
19
|
+
"templateManager.js",
|
|
20
|
+
"eventTest.js",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"LICENSE-eventTest.md",
|
|
23
|
+
"examples/"
|
|
20
24
|
],
|
|
21
25
|
"keywords": [
|
|
22
26
|
"state-management",
|
|
23
|
-
"
|
|
27
|
+
"event-driven",
|
|
28
|
+
"reactive",
|
|
24
29
|
"dom-events",
|
|
30
|
+
"slot-orchestration",
|
|
25
31
|
"zero-dependency",
|
|
26
|
-
"
|
|
32
|
+
"framework-free",
|
|
33
|
+
"micro-framework",
|
|
34
|
+
"testing",
|
|
35
|
+
"event-testing"
|
|
27
36
|
],
|
|
28
37
|
"author": "Ajdin Imsirovic",
|
|
29
38
|
"license": "MIT",
|
package/stateSerializer.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
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
|
|
1
15
|
function escapeCssValue(value) {
|
|
2
16
|
if (typeof value !== 'string') return value;
|
|
3
17
|
return value.replace(/[^\x20-\x7E]|[!;{}:()[\]/@,'"]/g, function(char) {
|
|
@@ -8,6 +22,7 @@ function escapeCssValue(value) {
|
|
|
8
22
|
|
|
9
23
|
function unescapeCssValue(value) {
|
|
10
24
|
if (typeof value !== 'string') return value;
|
|
25
|
+
// Only perform unescaping if there are escape sequences
|
|
11
26
|
if (!value.includes('\\')) return value;
|
|
12
27
|
|
|
13
28
|
return value.replace(/\\([0-9a-f]{1,6})\s?/gi, function(match, hex) {
|
|
@@ -15,19 +30,32 @@ function unescapeCssValue(value) {
|
|
|
15
30
|
});
|
|
16
31
|
}
|
|
17
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Create a configured serializer instance
|
|
35
|
+
* @param {Object} config - Configuration options
|
|
36
|
+
* @returns {Object} - Serializer instance
|
|
37
|
+
*/
|
|
18
38
|
function createSerializer(config = {}) {
|
|
39
|
+
// Default configuration
|
|
19
40
|
const defaultConfig = {
|
|
20
|
-
mode: 'hybrid',
|
|
21
|
-
debug: false,
|
|
22
|
-
complexThreshold: 3,
|
|
23
|
-
preserveTypes: true
|
|
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
|
|
24
45
|
};
|
|
25
46
|
|
|
47
|
+
// Merge provided config with defaults
|
|
26
48
|
const options = { ...defaultConfig, ...config };
|
|
27
49
|
|
|
50
|
+
// Serializer instance
|
|
28
51
|
const serializer = {
|
|
52
|
+
// Current configuration
|
|
29
53
|
config: options,
|
|
30
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Update configuration
|
|
57
|
+
* @param {Object} newConfig - New configuration options
|
|
58
|
+
*/
|
|
31
59
|
configure(newConfig) {
|
|
32
60
|
Object.assign(this.config, newConfig);
|
|
33
61
|
if (this.config.debug) {
|
|
@@ -35,7 +63,14 @@ function createSerializer(config = {}) {
|
|
|
35
63
|
}
|
|
36
64
|
},
|
|
37
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
|
+
*/
|
|
38
72
|
serialize(key, value) {
|
|
73
|
+
// Handle null/undefined
|
|
39
74
|
if (value === null || value === undefined) {
|
|
40
75
|
return '';
|
|
41
76
|
}
|
|
@@ -45,24 +80,37 @@ function createSerializer(config = {}) {
|
|
|
45
80
|
(Array.isArray(value) ||
|
|
46
81
|
(Object.keys(value).length >= this.config.complexThreshold));
|
|
47
82
|
|
|
83
|
+
// Select serialization strategy based on configuration and value type
|
|
48
84
|
if (this.config.mode === 'escape' ||
|
|
49
85
|
(this.config.mode === 'hybrid' && !isComplex)) {
|
|
86
|
+
// Use escape strategy for primitives or when escape mode is forced
|
|
50
87
|
if (valueType === 'string') {
|
|
51
88
|
return escapeCssValue(value);
|
|
52
89
|
} else if (valueType === 'object') {
|
|
90
|
+
// For simple objects in escape mode, still use JSON but with escaping
|
|
53
91
|
const jsonStr = JSON.stringify(value);
|
|
54
92
|
return escapeCssValue(jsonStr);
|
|
55
93
|
} else {
|
|
94
|
+
// For other primitives, convert to string
|
|
56
95
|
return String(value);
|
|
57
96
|
}
|
|
58
97
|
} else {
|
|
98
|
+
// Use JSON strategy for complex objects or when JSON mode is forced
|
|
59
99
|
return JSON.stringify(value);
|
|
60
100
|
}
|
|
61
101
|
},
|
|
62
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
|
+
*/
|
|
63
109
|
deserialize(key, value) {
|
|
110
|
+
// Handle empty values
|
|
64
111
|
if (!value) return '';
|
|
65
112
|
|
|
113
|
+
// Try JSON parse first for values that look like JSON
|
|
66
114
|
if (this.config.mode !== 'escape' &&
|
|
67
115
|
((value.startsWith('{') && value.endsWith('}')) ||
|
|
68
116
|
(value.startsWith('[') && value.endsWith(']')))) {
|
|
@@ -72,42 +120,64 @@ function createSerializer(config = {}) {
|
|
|
72
120
|
if (this.config.debug) {
|
|
73
121
|
console.warn(`Failed to parse JSON for key "${key}":`, value);
|
|
74
122
|
}
|
|
123
|
+
// Fall through to unescaping if JSON parse fails
|
|
75
124
|
}
|
|
76
125
|
}
|
|
77
126
|
|
|
127
|
+
// For non-JSON or escape mode, try unescaping
|
|
78
128
|
const unescaped = unescapeCssValue(value);
|
|
79
129
|
|
|
130
|
+
// If unescaped looks like JSON (might have been double-escaped), try parsing it
|
|
80
131
|
if (this.config.mode !== 'escape' &&
|
|
81
132
|
((unescaped.startsWith('{') && unescaped.endsWith('}')) ||
|
|
82
133
|
(unescaped.startsWith('[') && unescaped.endsWith(']')))) {
|
|
83
134
|
try {
|
|
84
135
|
return JSON.parse(unescaped);
|
|
85
136
|
} catch (e) {
|
|
137
|
+
// Not valid JSON, return unescaped string
|
|
86
138
|
}
|
|
87
139
|
}
|
|
88
140
|
|
|
89
141
|
return unescaped;
|
|
90
142
|
},
|
|
91
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
|
+
*/
|
|
92
150
|
serializeForAttribute(key, value) {
|
|
93
151
|
if (value === null || value === undefined) return null;
|
|
94
152
|
|
|
153
|
+
// For objects, use the standard serializer
|
|
95
154
|
if (typeof value === 'object') {
|
|
96
155
|
return this.serialize(key, value);
|
|
97
156
|
}
|
|
98
157
|
|
|
158
|
+
// For primitive values, use direct string conversion
|
|
99
159
|
return String(value);
|
|
100
160
|
},
|
|
101
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
|
+
*/
|
|
102
168
|
applyToAttributes(key, value, element = document.documentElement) {
|
|
169
|
+
// Skip null/undefined values
|
|
103
170
|
if (value === null || value === undefined) {
|
|
104
171
|
element.removeAttribute(`data-${key}`);
|
|
105
172
|
return;
|
|
106
173
|
}
|
|
107
174
|
|
|
175
|
+
// Handle objects specially
|
|
108
176
|
if (typeof value === 'object') {
|
|
177
|
+
// Set the main attribute with serialized value
|
|
109
178
|
element.setAttribute(`data-${key}`, this.serialize(key, value));
|
|
110
179
|
|
|
180
|
+
// For non-array objects, set individual property attributes
|
|
111
181
|
if (!Array.isArray(value)) {
|
|
112
182
|
Object.entries(value).forEach(([propKey, propValue]) => {
|
|
113
183
|
const attributeKey = `data-${key}-${propKey.toLowerCase()}`;
|
|
@@ -126,21 +196,37 @@ function createSerializer(config = {}) {
|
|
|
126
196
|
});
|
|
127
197
|
}
|
|
128
198
|
} else {
|
|
199
|
+
// For primitives, set directly
|
|
129
200
|
element.setAttribute(`data-${key}`, value);
|
|
130
201
|
}
|
|
131
202
|
},
|
|
132
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
|
+
*/
|
|
133
209
|
needsComplexSerialization(value) {
|
|
134
210
|
return typeof value === 'object' && value !== null;
|
|
135
211
|
},
|
|
136
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
|
+
*/
|
|
137
220
|
setStateWithCss(uistate, path, value) {
|
|
221
|
+
// Update UIstate
|
|
138
222
|
uistate.setState(path, value);
|
|
139
223
|
|
|
224
|
+
// Update CSS variable with properly serialized value
|
|
140
225
|
const cssPath = path.replace(/\./g, '-');
|
|
141
226
|
const serialized = this.serialize(path, value);
|
|
142
227
|
document.documentElement.style.setProperty(`--${cssPath}`, serialized);
|
|
143
228
|
|
|
229
|
+
// Update data attribute for root level state
|
|
144
230
|
const segments = path.split('.');
|
|
145
231
|
if (segments.length === 1) {
|
|
146
232
|
document.documentElement.dataset[path] = typeof value === 'object'
|
|
@@ -151,10 +237,18 @@ function createSerializer(config = {}) {
|
|
|
151
237
|
return value;
|
|
152
238
|
},
|
|
153
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
|
+
*/
|
|
154
246
|
getStateFromCss(uistate, path) {
|
|
247
|
+
// First try UIstate
|
|
155
248
|
const value = uistate.getState(path);
|
|
156
249
|
if (value !== undefined) return value;
|
|
157
250
|
|
|
251
|
+
// If not found, try CSS variable
|
|
158
252
|
const cssPath = path.replace(/\./g, '-');
|
|
159
253
|
const cssValue = getComputedStyle(document.documentElement)
|
|
160
254
|
.getPropertyValue(`--${cssPath}`).trim();
|
|
@@ -166,6 +260,7 @@ function createSerializer(config = {}) {
|
|
|
166
260
|
return serializer;
|
|
167
261
|
}
|
|
168
262
|
|
|
263
|
+
// Create a default instance with hybrid mode
|
|
169
264
|
const StateSerializer = createSerializer();
|
|
170
265
|
|
|
171
266
|
export default StateSerializer;
|
package/templateManager.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TemplateManager - Component mounting and event delegation
|
|
3
|
+
* Handles HTML templating, component mounting, and event delegation
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
const createTemplateManager = (stateManager) => {
|
|
2
7
|
const manager = {
|
|
3
8
|
handlers: {},
|
|
@@ -7,11 +12,25 @@ const createTemplateManager = (stateManager) => {
|
|
|
7
12
|
return this;
|
|
8
13
|
},
|
|
9
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
|
+
*/
|
|
10
27
|
registerActions(actionsMap) {
|
|
11
28
|
Object.entries(actionsMap).forEach(([action, handler]) => {
|
|
12
29
|
if (typeof handler === 'function') {
|
|
30
|
+
// Simple function handler
|
|
13
31
|
this.onAction(action, handler);
|
|
14
32
|
} else if (typeof handler === 'object' && handler !== null) {
|
|
33
|
+
// Handler with configuration
|
|
15
34
|
const { fn, extractId = true, idAttribute = 'id' } = handler;
|
|
16
35
|
|
|
17
36
|
if (typeof fn !== 'function') {
|
|
@@ -21,6 +40,7 @@ const createTemplateManager = (stateManager) => {
|
|
|
21
40
|
this.onAction(action, (e) => {
|
|
22
41
|
if (extractId) {
|
|
23
42
|
const target = e.target.closest('[data-action]');
|
|
43
|
+
// Look for common ID attributes in order of preference
|
|
24
44
|
const id = target.dataset[idAttribute] ||
|
|
25
45
|
target.dataset.actionId ||
|
|
26
46
|
target.dataset.cardId ||
|
|
@@ -50,30 +70,40 @@ const createTemplateManager = (stateManager) => {
|
|
|
50
70
|
if (typeof handler === 'function') {
|
|
51
71
|
handler(e);
|
|
52
72
|
} else if (target.dataset.value !== undefined && stateManager) {
|
|
73
|
+
// If we have a state manager, use it to update state
|
|
53
74
|
stateManager.setState(action, target.dataset.value);
|
|
54
75
|
}
|
|
55
76
|
});
|
|
56
77
|
return this;
|
|
57
78
|
},
|
|
58
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
|
+
*/
|
|
59
86
|
renderTemplateFromCss(templateName, data = {}) {
|
|
60
87
|
const cssTemplate = getComputedStyle(document.documentElement)
|
|
61
88
|
.getPropertyValue(`--template-${templateName}`)
|
|
62
89
|
.trim()
|
|
63
|
-
.replace(/^['"]|['"]$/g, '');
|
|
90
|
+
.replace(/^['"]|['"]$/g, ''); // Remove surrounding quotes
|
|
64
91
|
|
|
65
92
|
if (!cssTemplate) throw new Error(`Template not found in CSS: --template-${templateName}`);
|
|
66
93
|
|
|
67
94
|
let html = cssTemplate;
|
|
68
95
|
|
|
96
|
+
// Replace all placeholders with actual data
|
|
69
97
|
Object.entries(data).forEach(([key, value]) => {
|
|
70
98
|
const regex = new RegExp(`{{${key}}}`, 'g');
|
|
71
99
|
html = html.replace(regex, value);
|
|
72
100
|
});
|
|
73
101
|
|
|
102
|
+
// Create a temporary container
|
|
74
103
|
const temp = document.createElement('div');
|
|
75
104
|
temp.innerHTML = html;
|
|
76
105
|
|
|
106
|
+
// Return the first child (the rendered template)
|
|
77
107
|
return temp.firstElementChild;
|
|
78
108
|
},
|
|
79
109
|
|
|
@@ -101,11 +131,13 @@ const createTemplateManager = (stateManager) => {
|
|
|
101
131
|
return clone.firstElementChild;
|
|
102
132
|
},
|
|
103
133
|
|
|
134
|
+
// Helper to create a reactive component with automatic updates
|
|
104
135
|
createComponent(name, renderFn, stateKeys = []) {
|
|
105
136
|
if (!stateManager) {
|
|
106
137
|
throw new Error('State manager is required for reactive components');
|
|
107
138
|
}
|
|
108
139
|
|
|
140
|
+
// Create template element if it doesn't exist
|
|
109
141
|
let tpl = document.getElementById(`${name}-template`);
|
|
110
142
|
if (!tpl) {
|
|
111
143
|
tpl = document.createElement('template');
|
|
@@ -113,8 +145,10 @@ const createTemplateManager = (stateManager) => {
|
|
|
113
145
|
document.body.appendChild(tpl);
|
|
114
146
|
}
|
|
115
147
|
|
|
148
|
+
// Initial render
|
|
116
149
|
tpl.innerHTML = renderFn(stateManager);
|
|
117
150
|
|
|
151
|
+
// Set up observers for reactive updates
|
|
118
152
|
if (stateKeys.length > 0) {
|
|
119
153
|
stateKeys.forEach(key => {
|
|
120
154
|
stateManager.observe(key, () => {
|
|
@@ -128,6 +162,17 @@ const createTemplateManager = (stateManager) => {
|
|
|
128
162
|
};
|
|
129
163
|
},
|
|
130
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
|
+
*/
|
|
131
176
|
applyClassesFromState(element, stateKey, options = {}) {
|
|
132
177
|
if (!element) return element;
|
|
133
178
|
|
|
@@ -146,22 +191,25 @@ const createTemplateManager = (stateManager) => {
|
|
|
146
191
|
.replace(/^['"]|['"]$/g, '');
|
|
147
192
|
|
|
148
193
|
if (classString) {
|
|
194
|
+
// Clear existing classes if specified
|
|
149
195
|
if (clearExisting) {
|
|
150
196
|
element.className = '';
|
|
151
197
|
}
|
|
152
198
|
|
|
199
|
+
// Add new classes
|
|
153
200
|
classString.split(' ').forEach(cls => {
|
|
154
201
|
if (cls) element.classList.add(cls);
|
|
155
202
|
});
|
|
156
203
|
}
|
|
157
204
|
|
|
158
|
-
return element;
|
|
205
|
+
return element; // For chaining
|
|
159
206
|
}
|
|
160
207
|
};
|
|
161
208
|
|
|
162
209
|
return manager;
|
|
163
210
|
};
|
|
164
211
|
|
|
212
|
+
// Create a standalone instance that doesn't depend on any state manager
|
|
165
213
|
const TemplateManager = createTemplateManager();
|
|
166
214
|
|
|
167
215
|
export default createTemplateManager;
|