onelaraveljs 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 +87 -0
- package/docs/integration_analysis.md +116 -0
- package/docs/onejs_analysis.md +108 -0
- package/docs/optimization_implementation_group2.md +458 -0
- package/docs/optimization_plan.md +130 -0
- package/index.js +16 -0
- package/package.json +13 -0
- package/src/app.js +61 -0
- package/src/core/API.js +72 -0
- package/src/core/ChildrenRegistry.js +410 -0
- package/src/core/DOMBatcher.js +207 -0
- package/src/core/ErrorBoundary.js +226 -0
- package/src/core/EventDelegator.js +416 -0
- package/src/core/Helper.js +817 -0
- package/src/core/LoopContext.js +97 -0
- package/src/core/OneDOM.js +246 -0
- package/src/core/OneMarkup.js +444 -0
- package/src/core/Router.js +996 -0
- package/src/core/SEOConfig.js +321 -0
- package/src/core/SectionEngine.js +75 -0
- package/src/core/TemplateEngine.js +83 -0
- package/src/core/View.js +273 -0
- package/src/core/ViewConfig.js +229 -0
- package/src/core/ViewController.js +1410 -0
- package/src/core/ViewControllerOptimized.js +164 -0
- package/src/core/ViewIdentifier.js +361 -0
- package/src/core/ViewLoader.js +272 -0
- package/src/core/ViewManager.js +1962 -0
- package/src/core/ViewState.js +761 -0
- package/src/core/ViewSystem.js +301 -0
- package/src/core/ViewTemplate.js +4 -0
- package/src/core/helpers/BindingHelper.js +239 -0
- package/src/core/helpers/ConfigHelper.js +37 -0
- package/src/core/helpers/EventHelper.js +172 -0
- package/src/core/helpers/LifecycleHelper.js +17 -0
- package/src/core/helpers/ReactiveHelper.js +169 -0
- package/src/core/helpers/RenderHelper.js +15 -0
- package/src/core/helpers/ResourceHelper.js +89 -0
- package/src/core/helpers/TemplateHelper.js +11 -0
- package/src/core/managers/BindingManager.js +671 -0
- package/src/core/managers/ConfigurationManager.js +136 -0
- package/src/core/managers/EventManager.js +309 -0
- package/src/core/managers/LifecycleManager.js +356 -0
- package/src/core/managers/ReactiveManager.js +334 -0
- package/src/core/managers/RenderEngine.js +292 -0
- package/src/core/managers/ResourceManager.js +441 -0
- package/src/core/managers/ViewHierarchyManager.js +258 -0
- package/src/core/managers/ViewTemplateManager.js +127 -0
- package/src/core/reactive/ReactiveComponent.js +592 -0
- package/src/core/services/EventService.js +418 -0
- package/src/core/services/HttpService.js +106 -0
- package/src/core/services/LoggerService.js +57 -0
- package/src/core/services/StateService.js +512 -0
- package/src/core/services/StorageService.js +856 -0
- package/src/core/services/StoreService.js +258 -0
- package/src/core/services/TemplateDetectorService.js +361 -0
- package/src/core/services/Test.js +18 -0
- package/src/helpers/devWarnings.js +205 -0
- package/src/helpers/performance.js +226 -0
- package/src/helpers/utils.js +287 -0
- package/src/init.js +343 -0
- package/src/plugins/auto-plugin.js +34 -0
- package/src/services/Test.js +18 -0
- package/src/types/index.js +193 -0
- package/src/utils/date-helper.js +51 -0
- package/src/utils/helpers.js +39 -0
- package/src/utils/validation.js +32 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced View System with Server-Side Rendering Support
|
|
3
|
+
* Handles view identification, hydration, and DOM mapping
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ViewIdentifier } from './ViewIdentifier.js';
|
|
7
|
+
|
|
8
|
+
export class ViewSystem {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.identifier = new ViewIdentifier();
|
|
11
|
+
this.hydratedViews = new Set();
|
|
12
|
+
this.viewInstances = new Map();
|
|
13
|
+
this.isServerRendered = false;
|
|
14
|
+
this.hydrationComplete = false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the view system
|
|
19
|
+
*/
|
|
20
|
+
init() {
|
|
21
|
+
this.detectServerRendering();
|
|
22
|
+
this.identifier.init();
|
|
23
|
+
|
|
24
|
+
if (this.isServerRendered) {
|
|
25
|
+
this.hydrateServerRenderedViews();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.setupGlobalEventListeners();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect if page was server-rendered
|
|
33
|
+
*/
|
|
34
|
+
detectServerRendering() {
|
|
35
|
+
const container = document.querySelector('#app-root, #app, [data-server-rendered]');
|
|
36
|
+
this.isServerRendered = container && container.getAttribute('data-server-rendered') === 'true';
|
|
37
|
+
|
|
38
|
+
console.log(`🔍 Server rendering detected: ${this.isServerRendered}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Hydrate server-rendered views
|
|
43
|
+
*/
|
|
44
|
+
hydrateServerRenderedViews() {
|
|
45
|
+
console.log('🚀 Starting hydration of server-rendered views...');
|
|
46
|
+
|
|
47
|
+
// Get all view elements
|
|
48
|
+
const viewElements = this.identifier.getAllViewElements();
|
|
49
|
+
|
|
50
|
+
viewElements.forEach(element => {
|
|
51
|
+
const viewInfo = this.identifier.extractViewInfo(element);
|
|
52
|
+
if (viewInfo) {
|
|
53
|
+
this.hydrateView(element, viewInfo);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Mark hydration as complete
|
|
58
|
+
this.hydrationComplete = true;
|
|
59
|
+
this.emitHydrationComplete();
|
|
60
|
+
|
|
61
|
+
console.log('✅ Hydration complete');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Hydrate a specific view
|
|
66
|
+
*/
|
|
67
|
+
hydrateView(element, viewInfo) {
|
|
68
|
+
try {
|
|
69
|
+
// Create view instance if not exists
|
|
70
|
+
if (!this.viewInstances.has(viewInfo.id)) {
|
|
71
|
+
const viewInstance = this.createViewInstance(viewInfo);
|
|
72
|
+
this.viewInstances.set(viewInfo.id, viewInstance);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const viewInstance = this.viewInstances.get(viewInfo.id);
|
|
76
|
+
|
|
77
|
+
// Attach view instance to element
|
|
78
|
+
element._spaViewInstance = viewInstance;
|
|
79
|
+
|
|
80
|
+
// Mark as hydrated
|
|
81
|
+
this.hydratedViews.add(viewInfo.id);
|
|
82
|
+
|
|
83
|
+
// Trigger view lifecycle
|
|
84
|
+
this.triggerViewLifecycle(viewInstance, 'mounted');
|
|
85
|
+
|
|
86
|
+
console.log(`✅ Hydrated view: ${viewInfo.name} (${viewInfo.id})`);
|
|
87
|
+
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(`❌ Failed to hydrate view ${viewInfo.name}:`, error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create view instance
|
|
95
|
+
*/
|
|
96
|
+
createViewInstance(viewInfo) {
|
|
97
|
+
return {
|
|
98
|
+
id: viewInfo.id,
|
|
99
|
+
name: viewInfo.name,
|
|
100
|
+
path: viewInfo.path,
|
|
101
|
+
type: viewInfo.type,
|
|
102
|
+
element: viewInfo.element,
|
|
103
|
+
isHydrated: false,
|
|
104
|
+
lifecycle: {
|
|
105
|
+
mounted: false,
|
|
106
|
+
updated: false,
|
|
107
|
+
destroyed: false
|
|
108
|
+
},
|
|
109
|
+
data: {},
|
|
110
|
+
methods: {}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Trigger view lifecycle events
|
|
116
|
+
*/
|
|
117
|
+
triggerViewLifecycle(viewInstance, event) {
|
|
118
|
+
const element = viewInstance.element;
|
|
119
|
+
|
|
120
|
+
// Dispatch custom events
|
|
121
|
+
const customEvent = new CustomEvent(`spa:view:${event}`, {
|
|
122
|
+
detail: {
|
|
123
|
+
view: viewInstance,
|
|
124
|
+
timestamp: Date.now()
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
element.dispatchEvent(customEvent);
|
|
128
|
+
|
|
129
|
+
// Update lifecycle state
|
|
130
|
+
viewInstance.lifecycle[event] = true;
|
|
131
|
+
|
|
132
|
+
// Call view methods if available
|
|
133
|
+
if (viewInstance.methods[event]) {
|
|
134
|
+
try {
|
|
135
|
+
viewInstance.methods[event].call(viewInstance);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(`Error in view ${event} method:`, error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Setup global event listeners
|
|
144
|
+
*/
|
|
145
|
+
setupGlobalEventListeners() {
|
|
146
|
+
// Listen for view changes
|
|
147
|
+
document.addEventListener('spa:view:changed', (event) => {
|
|
148
|
+
this.onViewChanged(event.detail.view);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Listen for navigation events
|
|
152
|
+
document.addEventListener('spa:navigation:start', () => {
|
|
153
|
+
this.pauseViewUpdates();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
document.addEventListener('spa:navigation:complete', () => {
|
|
157
|
+
this.resumeViewUpdates();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Handle view change
|
|
163
|
+
*/
|
|
164
|
+
onViewChanged(viewData) {
|
|
165
|
+
console.log(`🔄 View changed to: ${viewData.info.name}`);
|
|
166
|
+
|
|
167
|
+
// Update current view tracking
|
|
168
|
+
this.identifier.setCurrentView(viewData.info.id);
|
|
169
|
+
|
|
170
|
+
// Trigger view enter event
|
|
171
|
+
this.triggerViewLifecycle(viewData, 'enter');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Pause view updates during navigation
|
|
176
|
+
*/
|
|
177
|
+
pauseViewUpdates() {
|
|
178
|
+
this.viewInstances.forEach(viewInstance => {
|
|
179
|
+
viewInstance.isPaused = true;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resume view updates after navigation
|
|
185
|
+
*/
|
|
186
|
+
resumeViewUpdates() {
|
|
187
|
+
this.viewInstances.forEach(viewInstance => {
|
|
188
|
+
viewInstance.isPaused = false;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get view by ID
|
|
194
|
+
*/
|
|
195
|
+
getView(viewId) {
|
|
196
|
+
return this.viewInstances.get(viewId);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get all hydrated views
|
|
201
|
+
*/
|
|
202
|
+
getHydratedViews() {
|
|
203
|
+
return Array.from(this.hydratedViews).map(id => this.viewInstances.get(id));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get views by type
|
|
208
|
+
*/
|
|
209
|
+
getViewsByType(type) {
|
|
210
|
+
const views = [];
|
|
211
|
+
this.viewInstances.forEach(viewInstance => {
|
|
212
|
+
if (viewInstance.type === type) {
|
|
213
|
+
views.push(viewInstance);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
return views;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Update view data
|
|
221
|
+
*/
|
|
222
|
+
updateViewData(viewId, data) {
|
|
223
|
+
const viewInstance = this.viewInstances.get(viewId);
|
|
224
|
+
if (viewInstance) {
|
|
225
|
+
viewInstance.data = { ...viewInstance.data, ...data };
|
|
226
|
+
this.triggerViewLifecycle(viewInstance, 'updated');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Destroy view
|
|
232
|
+
*/
|
|
233
|
+
destroyView(viewId) {
|
|
234
|
+
const viewInstance = this.viewInstances.get(viewId);
|
|
235
|
+
if (viewInstance) {
|
|
236
|
+
this.triggerViewLifecycle(viewInstance, 'destroyed');
|
|
237
|
+
this.hydratedViews.delete(viewId);
|
|
238
|
+
this.viewInstances.delete(viewId);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Emit hydration complete event
|
|
244
|
+
*/
|
|
245
|
+
emitHydrationComplete() {
|
|
246
|
+
const event = new CustomEvent('spa:hydration:complete', {
|
|
247
|
+
detail: {
|
|
248
|
+
hydratedViews: this.hydratedViews.size,
|
|
249
|
+
totalViews: this.viewInstances.size,
|
|
250
|
+
timestamp: Date.now()
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
document.dispatchEvent(event);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get system status
|
|
258
|
+
*/
|
|
259
|
+
getStatus() {
|
|
260
|
+
return {
|
|
261
|
+
isServerRendered: this.isServerRendered,
|
|
262
|
+
hydrationComplete: this.hydrationComplete,
|
|
263
|
+
totalViews: this.viewInstances.size,
|
|
264
|
+
hydratedViews: this.hydratedViews.size,
|
|
265
|
+
identifier: this.identifier.getDebugInfo()
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Enable debug mode
|
|
271
|
+
*/
|
|
272
|
+
enableDebugMode() {
|
|
273
|
+
this.identifier.enableDebugMode();
|
|
274
|
+
console.log('🔧 Debug mode enabled');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Disable debug mode
|
|
279
|
+
*/
|
|
280
|
+
disableDebugMode() {
|
|
281
|
+
this.identifier.disableDebugMode();
|
|
282
|
+
console.log('🔧 Debug mode disabled');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Export system data for debugging
|
|
287
|
+
*/
|
|
288
|
+
exportSystemData() {
|
|
289
|
+
return {
|
|
290
|
+
status: this.getStatus(),
|
|
291
|
+
views: this.identifier.exportViewData(),
|
|
292
|
+
instances: Array.from(this.viewInstances.entries()).map(([id, instance]) => ({
|
|
293
|
+
id,
|
|
294
|
+
name: instance.name,
|
|
295
|
+
type: instance.type,
|
|
296
|
+
isHydrated: this.hydratedViews.has(id),
|
|
297
|
+
lifecycle: instance.lifecycle
|
|
298
|
+
}))
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import OneDOM from '../OneDOM.js';
|
|
2
|
+
import DOMBatcher from '../DOMBatcher.js';
|
|
3
|
+
import logger from '../services/LoggerService.js';
|
|
4
|
+
|
|
5
|
+
export default class BindingHelper {
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Start Two-Way Binding Listeners
|
|
9
|
+
*/
|
|
10
|
+
static startBindingEventListener(controller) {
|
|
11
|
+
const state = controller._internal.binding;
|
|
12
|
+
const selector = `[data-binding][data-view-id="${controller.id}"]`;
|
|
13
|
+
const inputs = document.querySelectorAll(selector);
|
|
14
|
+
|
|
15
|
+
if (inputs.length === 0) return;
|
|
16
|
+
|
|
17
|
+
inputs.forEach(input => {
|
|
18
|
+
if (!input.isConnected) return;
|
|
19
|
+
|
|
20
|
+
// Check WeakMap to see if already attached
|
|
21
|
+
if (state.elementMap.has(input)) return;
|
|
22
|
+
|
|
23
|
+
const stateKey = input.getAttribute('data-binding');
|
|
24
|
+
if (!stateKey) return;
|
|
25
|
+
|
|
26
|
+
// Sync initial state (State -> Element)
|
|
27
|
+
BindingHelper.syncStateToElement(controller, input, stateKey);
|
|
28
|
+
|
|
29
|
+
// Create Handler (Element -> State)
|
|
30
|
+
const handler = (event) => BindingHelper.pushElementToState(controller, input, stateKey);
|
|
31
|
+
|
|
32
|
+
const tag = input.tagName.toLowerCase();
|
|
33
|
+
const eventType = tag === 'select' ? 'change' : 'input';
|
|
34
|
+
|
|
35
|
+
input.addEventListener(eventType, handler);
|
|
36
|
+
|
|
37
|
+
// Store in WeakMap for efficient lookup/garbage collection prevention
|
|
38
|
+
state.elementMap.set(input, { eventType, handler, stateKey });
|
|
39
|
+
|
|
40
|
+
// Store in array for manual cleanup in destroy()
|
|
41
|
+
state.listeners.push({ element: input, eventType, handler });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
state.isStarted = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Start Class Binding Listeners
|
|
49
|
+
*/
|
|
50
|
+
static startClassBindingEventListener(controller) {
|
|
51
|
+
const state = controller._internal.binding;
|
|
52
|
+
|
|
53
|
+
if (state.isClassReady || !state.classConfigs || state.classConfigs.length === 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const removeClassBindingIDs = [];
|
|
58
|
+
|
|
59
|
+
state.classConfigs.forEach(binding => {
|
|
60
|
+
const { id, config, states, initialiClass = '' } = binding;
|
|
61
|
+
// logic to find element...
|
|
62
|
+
const selector = `[data-one-class-id="${id}"]`;
|
|
63
|
+
const element = document.querySelector(selector);
|
|
64
|
+
|
|
65
|
+
if (!element) {
|
|
66
|
+
removeClassBindingIDs.push(id);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
binding.element = element;
|
|
70
|
+
|
|
71
|
+
state.classListeners.push({ id, states, config, element, initialiClass });
|
|
72
|
+
|
|
73
|
+
// Initial class application
|
|
74
|
+
const classOperations = [];
|
|
75
|
+
|
|
76
|
+
// Initial classes
|
|
77
|
+
String(initialiClass).split(' ').forEach(cls => {
|
|
78
|
+
if (cls.trim()) classOperations.push({ element, action: 'add', className: cls });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Config classes
|
|
82
|
+
Object.entries(config).forEach(([className, classConfig]) => {
|
|
83
|
+
const { checker } = classConfig;
|
|
84
|
+
if (typeof checker === 'function') {
|
|
85
|
+
const shouldAdd = checker.call(controller.view); // Bind to view
|
|
86
|
+
classOperations.push({ element, action: shouldAdd ? 'add' : 'remove', className });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (classOperations.length > 0) {
|
|
91
|
+
DOMBatcher.write(() => {
|
|
92
|
+
classOperations.forEach(({ element, action, className }) => {
|
|
93
|
+
element.classList[action](className);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
state.isClassReady = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Push Element value to State (Input -> State)
|
|
104
|
+
*/
|
|
105
|
+
static pushElementToState(controller, element, stateKey) {
|
|
106
|
+
const state = controller._internal.binding;
|
|
107
|
+
|
|
108
|
+
// Get or Create flags from WeakMap
|
|
109
|
+
let flags = state.elementFlags.get(element);
|
|
110
|
+
if (!flags) {
|
|
111
|
+
flags = { pushing: false, syncing: false };
|
|
112
|
+
state.elementFlags.set(element, flags);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (flags.syncing || flags.pushing) return;
|
|
116
|
+
flags.pushing = true;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const value = OneDOM.getInputValue(element);
|
|
120
|
+
// Update state on controller
|
|
121
|
+
controller.states.__.updateStateAddressKey(stateKey, value);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
logger.error(`Error pushing element state ${stateKey}`, e);
|
|
124
|
+
} finally {
|
|
125
|
+
// Clear flag asynchronously
|
|
126
|
+
Promise.resolve().then(() => {
|
|
127
|
+
const currentFlags = state.elementFlags.get(element);
|
|
128
|
+
if (currentFlags) currentFlags.pushing = false;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Sync State value to Element (State -> Element)
|
|
135
|
+
*/
|
|
136
|
+
static syncStateToElement(controller, element, stateKey) {
|
|
137
|
+
const state = controller._internal.binding;
|
|
138
|
+
|
|
139
|
+
let flags = state.elementFlags.get(element);
|
|
140
|
+
if (!flags) {
|
|
141
|
+
flags = { pushing: false, syncing: false };
|
|
142
|
+
state.elementFlags.set(element, flags);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (flags.pushing || flags.syncing) return;
|
|
146
|
+
|
|
147
|
+
const stateValue = controller.states.__.getStateByAddressKey(stateKey);
|
|
148
|
+
const currentValue = OneDOM.getInputValue(element);
|
|
149
|
+
|
|
150
|
+
if (currentValue == stateValue) return; // Loose equality check
|
|
151
|
+
|
|
152
|
+
flags.syncing = true;
|
|
153
|
+
try {
|
|
154
|
+
OneDOM.setInputValue(element, stateValue);
|
|
155
|
+
} finally {
|
|
156
|
+
flags.syncing = false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Notify state changes to all bindings
|
|
162
|
+
* @param {Object} controller
|
|
163
|
+
* @param {Array} changedKeys
|
|
164
|
+
*/
|
|
165
|
+
static notifyStateChanges(controller, changedKeys) {
|
|
166
|
+
if (!controller.isReadyToStateChangeListen) return;
|
|
167
|
+
|
|
168
|
+
// Handle explicit subscribeStates method on controller if exists
|
|
169
|
+
// ... (legacy logic support if needed) ...
|
|
170
|
+
|
|
171
|
+
const state = controller._internal.binding;
|
|
172
|
+
|
|
173
|
+
// 1. Notify Input Bindings
|
|
174
|
+
if (state.listeners.length > 0) {
|
|
175
|
+
state.listeners.forEach(({ element, stateKey }) => {
|
|
176
|
+
// Check if stateKey or its parent is in changedKeys
|
|
177
|
+
// Simplification: exact match or simple includes
|
|
178
|
+
// In real impl, need robust key matching (e.g. 'user.name' matches 'user')
|
|
179
|
+
const rootKey = stateKey.split('.')[0];
|
|
180
|
+
if (changedKeys.includes(rootKey) || changedKeys.includes(stateKey)) {
|
|
181
|
+
BindingHelper.syncStateToElement(controller, element, stateKey);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 2. Notify Class Bindings
|
|
187
|
+
if (state.isClassReady && state.classListeners.length > 0) {
|
|
188
|
+
const classOperations = [];
|
|
189
|
+
state.classListeners.forEach(listener => {
|
|
190
|
+
const { states, config, element } = listener;
|
|
191
|
+
if (!element.isConnected) return;
|
|
192
|
+
|
|
193
|
+
const shouldUpdate = states.some(key => changedKeys.includes(key));
|
|
194
|
+
if (!shouldUpdate) return;
|
|
195
|
+
|
|
196
|
+
Object.entries(config).forEach(([className, classConfig]) => {
|
|
197
|
+
const { states: classStates, checker } = classConfig;
|
|
198
|
+
if (classStates.some(k => changedKeys.includes(k))) {
|
|
199
|
+
const shouldAdd = checker.call(controller.view);
|
|
200
|
+
classOperations.push({ element, action: shouldAdd ? 'add' : 'remove', className });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (classOperations.length > 0) {
|
|
206
|
+
DOMBatcher.write(() => {
|
|
207
|
+
classOperations.forEach(({ element, action, className }) => {
|
|
208
|
+
element.classList[action](className);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 3. Notify Attribute Bindings (omitted for brevity, similar logic)
|
|
215
|
+
// ...
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Destroy binding state
|
|
220
|
+
*/
|
|
221
|
+
static destroy(controller) {
|
|
222
|
+
const state = controller._internal.binding;
|
|
223
|
+
|
|
224
|
+
// Remove listeners
|
|
225
|
+
state.listeners.forEach(({ element, eventType, handler }) => {
|
|
226
|
+
try {
|
|
227
|
+
if (element.isConnected) element.removeEventListener(eventType, handler);
|
|
228
|
+
} catch (e) { }
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
state.listeners = [];
|
|
232
|
+
state.classListeners = [];
|
|
233
|
+
state.attrListeners = [];
|
|
234
|
+
state.isStarted = false;
|
|
235
|
+
state.isClassReady = false;
|
|
236
|
+
|
|
237
|
+
// WeakMaps (elementFlags, elementMap) will be GC'd automatically when controller._internal dies
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigHelper
|
|
3
|
+
* Handles configuration processing
|
|
4
|
+
*/
|
|
5
|
+
import { __defineProps, __defineMethods } from '../../helpers/utils.js';
|
|
6
|
+
|
|
7
|
+
export default class ConfigHelper {
|
|
8
|
+
static processDefinedProperties(controller, config) {
|
|
9
|
+
let definedProps = {};
|
|
10
|
+
let definedMethods = {};
|
|
11
|
+
|
|
12
|
+
if (config.__props__ && config.__props__.length > 0) {
|
|
13
|
+
config.__props__.forEach(prop => {
|
|
14
|
+
if (typeof config[prop] === 'function') {
|
|
15
|
+
definedMethods[prop] = config[prop].bind(controller);
|
|
16
|
+
}
|
|
17
|
+
else if (typeof config[prop] !== 'undefined') {
|
|
18
|
+
definedProps[prop] = config[prop];
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
__defineProps(controller, definedProps, {
|
|
24
|
+
writable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
enumerable: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
__defineMethods(controller, definedMethods);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static commitConstructorData(controller) {
|
|
33
|
+
if (typeof controller.config.commitConstructorData === 'function') {
|
|
34
|
+
controller.config.commitConstructorData.call(controller.view);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|