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,1410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View Engine class for managing view instances
|
|
3
|
+
* @param {Object} config - View configuration
|
|
4
|
+
*
|
|
5
|
+
* Refactored 2025-12-29: Now uses manager pattern for better maintainability
|
|
6
|
+
*/
|
|
7
|
+
import { __defineGetters, __defineMethods, __defineProperties, __defineProps, deleteProp, escapeString, hasData, uniqId } from '../helpers/utils.js';
|
|
8
|
+
import { ViewState } from './ViewState.js';
|
|
9
|
+
import logger from './services/LoggerService.js';
|
|
10
|
+
import { ATTR, FORBIDDEN_KEYS } from './ViewConfig.js';
|
|
11
|
+
import { LoopContext } from './LoopContext.js';
|
|
12
|
+
import { ResourceManager } from './managers/ResourceManager.js';
|
|
13
|
+
import { EventManager } from './managers/EventManager.js';
|
|
14
|
+
import { RenderEngine } from './managers/RenderEngine.js';
|
|
15
|
+
import { LifecycleManager } from './managers/LifecycleManager.js';
|
|
16
|
+
import { ReactiveManager } from './managers/ReactiveManager.js';
|
|
17
|
+
import { BindingManager } from './managers/BindingManager.js';
|
|
18
|
+
import { ViewTemplateManager } from './managers/ViewTemplateManager.js';
|
|
19
|
+
import { ConfigurationManager } from './managers/ConfigurationManager.js';
|
|
20
|
+
import { ViewHierarchyManager } from './managers/ViewHierarchyManager.js';
|
|
21
|
+
import { ChildrenRegistry } from './ChildrenRegistry.js';
|
|
22
|
+
import { OneMarkupModel } from './OneMarkup.js';
|
|
23
|
+
|
|
24
|
+
export class ViewController {
|
|
25
|
+
/**
|
|
26
|
+
*
|
|
27
|
+
* @param {string} path
|
|
28
|
+
* @param {View} view
|
|
29
|
+
*/
|
|
30
|
+
constructor(path, view, app) {
|
|
31
|
+
/**
|
|
32
|
+
* @type {View}
|
|
33
|
+
*/
|
|
34
|
+
this.view = view;
|
|
35
|
+
this.states = new ViewState(this);
|
|
36
|
+
// Private properties using naming convention for browser compatibility
|
|
37
|
+
/**
|
|
38
|
+
* @type {Application}
|
|
39
|
+
*/
|
|
40
|
+
this.App = app;
|
|
41
|
+
/**
|
|
42
|
+
* @type {boolean}
|
|
43
|
+
*/
|
|
44
|
+
this.isSuperView = false;
|
|
45
|
+
/**
|
|
46
|
+
* @type {function}
|
|
47
|
+
*/
|
|
48
|
+
this._emptyFn = () => { };
|
|
49
|
+
/**
|
|
50
|
+
* @type {string}
|
|
51
|
+
*/
|
|
52
|
+
this.id = null;
|
|
53
|
+
/**
|
|
54
|
+
* @type {function}
|
|
55
|
+
*/
|
|
56
|
+
this.init = this._emptyFn;
|
|
57
|
+
/**
|
|
58
|
+
* @type {ViewController}
|
|
59
|
+
*/
|
|
60
|
+
this.parent = null;
|
|
61
|
+
/**
|
|
62
|
+
* @type {Array<ViewController>}
|
|
63
|
+
*/
|
|
64
|
+
this.children = [];
|
|
65
|
+
/**
|
|
66
|
+
* @type {ViewController}
|
|
67
|
+
*/
|
|
68
|
+
this.superView = null;
|
|
69
|
+
/**
|
|
70
|
+
* @type {string}
|
|
71
|
+
*/
|
|
72
|
+
this.superViewPath = null;
|
|
73
|
+
/**
|
|
74
|
+
* @type {string}
|
|
75
|
+
*/
|
|
76
|
+
this.superViewId = null;
|
|
77
|
+
/**
|
|
78
|
+
* @type {boolean}
|
|
79
|
+
*/
|
|
80
|
+
this.hasSuperView = false;
|
|
81
|
+
/**
|
|
82
|
+
* @type {string}
|
|
83
|
+
*/
|
|
84
|
+
this.originalViewPath = null;
|
|
85
|
+
/**
|
|
86
|
+
* @type {string}
|
|
87
|
+
*/
|
|
88
|
+
this.originalViewId = null;
|
|
89
|
+
/**
|
|
90
|
+
* @type {ViewController}
|
|
91
|
+
*/
|
|
92
|
+
this.originalView = null;
|
|
93
|
+
/**
|
|
94
|
+
* @type {boolean}
|
|
95
|
+
*/
|
|
96
|
+
this.hasAwaitData = false;
|
|
97
|
+
/**
|
|
98
|
+
* @type {boolean}
|
|
99
|
+
*/
|
|
100
|
+
this.hasFetchData = false;
|
|
101
|
+
/**
|
|
102
|
+
* @type {Object}
|
|
103
|
+
*/
|
|
104
|
+
this.fetch = {};
|
|
105
|
+
/**
|
|
106
|
+
* @type {boolean}
|
|
107
|
+
*/
|
|
108
|
+
this.usesVars = false;
|
|
109
|
+
/**
|
|
110
|
+
* @type {boolean}
|
|
111
|
+
*/
|
|
112
|
+
this.hasSections = false;
|
|
113
|
+
/**
|
|
114
|
+
* @type {Array<string>}
|
|
115
|
+
*/
|
|
116
|
+
this.renderLongSections = [];
|
|
117
|
+
/**
|
|
118
|
+
* @type {Object}
|
|
119
|
+
*/
|
|
120
|
+
this.sections = {};
|
|
121
|
+
/**
|
|
122
|
+
* @type {function}
|
|
123
|
+
*/
|
|
124
|
+
this.addCSS = this._emptyFn;
|
|
125
|
+
this.removeCSS = this._emptyFn;
|
|
126
|
+
/**
|
|
127
|
+
* @type {Array<string>}
|
|
128
|
+
*/
|
|
129
|
+
this.resources = [];
|
|
130
|
+
/**
|
|
131
|
+
* @type {Array}
|
|
132
|
+
*/
|
|
133
|
+
this.scripts = [];
|
|
134
|
+
/**
|
|
135
|
+
* @type {Array}
|
|
136
|
+
*/
|
|
137
|
+
this.styles = [];
|
|
138
|
+
/**
|
|
139
|
+
* @type {Set<string>}
|
|
140
|
+
*/
|
|
141
|
+
this.insertedResourceKeys = new Set();
|
|
142
|
+
this.userDefined = {};
|
|
143
|
+
/**
|
|
144
|
+
* @type {Object}
|
|
145
|
+
*/
|
|
146
|
+
this.events = {};
|
|
147
|
+
this.eventIndex = 0;
|
|
148
|
+
/**
|
|
149
|
+
* @type {Object}
|
|
150
|
+
*/
|
|
151
|
+
this.data = {};
|
|
152
|
+
this.urlPath = '';
|
|
153
|
+
/**
|
|
154
|
+
* @type {boolean}
|
|
155
|
+
*/
|
|
156
|
+
this.isInitlized = false;
|
|
157
|
+
/**
|
|
158
|
+
* @type {TemplateEngine}
|
|
159
|
+
*/
|
|
160
|
+
this.templateEngine = null;
|
|
161
|
+
|
|
162
|
+
// Initialize
|
|
163
|
+
this.path = path;
|
|
164
|
+
this.viewType = 'view';
|
|
165
|
+
this.config = {};
|
|
166
|
+
|
|
167
|
+
this.subscribeStates = true;
|
|
168
|
+
|
|
169
|
+
this.isRendering = false;
|
|
170
|
+
this.isRendered = false;
|
|
171
|
+
|
|
172
|
+
this.isMounted = false;
|
|
173
|
+
this.isDestroyed = false;
|
|
174
|
+
this.isScanning = false;
|
|
175
|
+
this.isScanned = false;
|
|
176
|
+
|
|
177
|
+
this.isFirstClientRendering = true;
|
|
178
|
+
|
|
179
|
+
this.isReady = false;
|
|
180
|
+
|
|
181
|
+
this.isMarkupScanned = false;
|
|
182
|
+
/**
|
|
183
|
+
* @type {OneMarkupModel}
|
|
184
|
+
*/
|
|
185
|
+
this.markup = null;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @type {HTMLElement}
|
|
189
|
+
*/
|
|
190
|
+
this.rootElement = null;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @type {Array<HTMLElement>}
|
|
194
|
+
*/
|
|
195
|
+
this.refElements = [];
|
|
196
|
+
|
|
197
|
+
this.eventListenerStatus = false;
|
|
198
|
+
|
|
199
|
+
this.eventListeners = [];
|
|
200
|
+
|
|
201
|
+
this.isVirtualRendering = false;
|
|
202
|
+
this.renderedContent = null;
|
|
203
|
+
|
|
204
|
+
this.__scope = {};
|
|
205
|
+
this.isRefreshing = false;
|
|
206
|
+
|
|
207
|
+
this.changeStateQueueCount = 0;
|
|
208
|
+
this.changedStateKeys = new Set();
|
|
209
|
+
this._stateChangePending = false;
|
|
210
|
+
|
|
211
|
+
this.loopContext = null;
|
|
212
|
+
|
|
213
|
+
this.isCommitedConstructorData = false;
|
|
214
|
+
|
|
215
|
+
this.isReadyToStateChangeListen = false;
|
|
216
|
+
|
|
217
|
+
this.childrenNeedToRefreshID = null;
|
|
218
|
+
|
|
219
|
+
// Memoization cache for expensive operations
|
|
220
|
+
this._memoCache = {
|
|
221
|
+
isHtmlString: new Map(),
|
|
222
|
+
escapedStrings: new Map()
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Initialize managers for better code organization
|
|
226
|
+
/**
|
|
227
|
+
* @type {ResourceManager}
|
|
228
|
+
*/
|
|
229
|
+
this._resourceManager = new ResourceManager(this);
|
|
230
|
+
/**
|
|
231
|
+
* @type {EventManager}
|
|
232
|
+
*/
|
|
233
|
+
this._eventManager = new EventManager(this);
|
|
234
|
+
/**
|
|
235
|
+
* @type {RenderEngine}
|
|
236
|
+
*/
|
|
237
|
+
this._renderEngine = new RenderEngine(this);
|
|
238
|
+
/**
|
|
239
|
+
* @type {LifecycleManager}
|
|
240
|
+
*/
|
|
241
|
+
this._lifecycleManager = new LifecycleManager(this);
|
|
242
|
+
/**
|
|
243
|
+
* @type {ReactiveManager}
|
|
244
|
+
*/
|
|
245
|
+
this._reactiveManager = new ReactiveManager(this);
|
|
246
|
+
/**
|
|
247
|
+
* @type {BindingManager}
|
|
248
|
+
*/
|
|
249
|
+
this._bindingManager = new BindingManager(this);
|
|
250
|
+
/**
|
|
251
|
+
* @type {ViewTemplateManager}
|
|
252
|
+
*/
|
|
253
|
+
this._templateManager = new ViewTemplateManager(this);
|
|
254
|
+
/**
|
|
255
|
+
* @type {ConfigurationManager}
|
|
256
|
+
*/
|
|
257
|
+
this._configManager = new ConfigurationManager(this);
|
|
258
|
+
/**
|
|
259
|
+
* @type {ViewHierarchyManager}
|
|
260
|
+
*/
|
|
261
|
+
this._hierarchyManager = new ViewHierarchyManager(this);
|
|
262
|
+
/**
|
|
263
|
+
* @type {ChildrenRegistry}
|
|
264
|
+
*/
|
|
265
|
+
this._childrenRegistry = new ChildrenRegistry(this);
|
|
266
|
+
|
|
267
|
+
this.renuewnChildrenIDs = [];
|
|
268
|
+
|
|
269
|
+
// Performance tracking
|
|
270
|
+
this._perfMarks = new Map();
|
|
271
|
+
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Mark performance checkpoint
|
|
276
|
+
* @param {string} label - Performance marker label
|
|
277
|
+
*/
|
|
278
|
+
_perfMark(label) {
|
|
279
|
+
if (this.App?.env?.debug) {
|
|
280
|
+
this._perfMarks.set(label, performance.now());
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Measure performance between two marks
|
|
286
|
+
* @param {string} startLabel - Start marker
|
|
287
|
+
* @param {string} endLabel - End marker
|
|
288
|
+
* @returns {number} Duration in ms
|
|
289
|
+
*/
|
|
290
|
+
_perfMeasure(startLabel, endLabel) {
|
|
291
|
+
if (!this.App?.env?.debug) {
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
const start = this._perfMarks.get(startLabel);
|
|
295
|
+
const end = this._perfMarks.get(endLabel);
|
|
296
|
+
if (start && end) {
|
|
297
|
+
const duration = end - start;
|
|
298
|
+
logger.info(`[Perf] ${this.path} ${startLabel}->${endLabel}: ${duration.toFixed(2)}ms`);
|
|
299
|
+
return duration;
|
|
300
|
+
}
|
|
301
|
+
return 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Destroy view controller and cleanup all resources
|
|
306
|
+
* Prevents memory leaks by properly cleaning up all references
|
|
307
|
+
*/
|
|
308
|
+
destroy() {
|
|
309
|
+
if (this.isDestroyed) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this._perfMark('destroy-start');
|
|
314
|
+
|
|
315
|
+
// Call lifecycle beforeDestroy
|
|
316
|
+
this._lifecycleManager?.beforeDestroy();
|
|
317
|
+
|
|
318
|
+
// Destroy all managers
|
|
319
|
+
this._reactiveManager?.destroy();
|
|
320
|
+
this._eventManager?.destroy();
|
|
321
|
+
this._resourceManager?.removeResources();
|
|
322
|
+
this._bindingManager?.destroy();
|
|
323
|
+
|
|
324
|
+
// Destroy state manager
|
|
325
|
+
this.states?.__?.destroy();
|
|
326
|
+
|
|
327
|
+
// Clear all references
|
|
328
|
+
this.children = [];
|
|
329
|
+
this.parent = null;
|
|
330
|
+
this.superView = null;
|
|
331
|
+
this.originalView = null;
|
|
332
|
+
this.eventListeners = [];
|
|
333
|
+
this.refElements = [];
|
|
334
|
+
this.loopContext = null;
|
|
335
|
+
|
|
336
|
+
// Clear data
|
|
337
|
+
this.data = {};
|
|
338
|
+
this.userDefined = {};
|
|
339
|
+
this.events = {};
|
|
340
|
+
this.sections = {};
|
|
341
|
+
this.__scope = {};
|
|
342
|
+
|
|
343
|
+
// Mark as destroyed
|
|
344
|
+
this.isDestroyed = true;
|
|
345
|
+
|
|
346
|
+
// Call lifecycle destroyed
|
|
347
|
+
this._lifecycleManager?.destroyed();
|
|
348
|
+
|
|
349
|
+
this._perfMark('destroy-end');
|
|
350
|
+
this._perfMeasure('destroy-start', 'destroy-end');
|
|
351
|
+
|
|
352
|
+
// Clear perf marks
|
|
353
|
+
this._perfMarks.clear();
|
|
354
|
+
|
|
355
|
+
logger.info(`[ViewController] Destroyed: ${this.path}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Setup the view engine with configuration
|
|
361
|
+
* @param {string} path - View path
|
|
362
|
+
* @param {Object} config - View configuration
|
|
363
|
+
*/
|
|
364
|
+
setup(path, config) {
|
|
365
|
+
if (this.isInitlized) {
|
|
366
|
+
return this;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this._perfMark('setup-start');
|
|
370
|
+
|
|
371
|
+
// Set config and path
|
|
372
|
+
this.path = path;
|
|
373
|
+
this.config = config || {};
|
|
374
|
+
|
|
375
|
+
// Call _initialize to do the actual setup
|
|
376
|
+
this.initialize();
|
|
377
|
+
|
|
378
|
+
this._perfMark('setup-end');
|
|
379
|
+
this._perfMeasure('setup-start', 'setup-end');
|
|
380
|
+
|
|
381
|
+
return this;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Initialize the view engine with configuration
|
|
387
|
+
* @private
|
|
388
|
+
*/
|
|
389
|
+
initialize() {
|
|
390
|
+
if (this.isInitlized) {
|
|
391
|
+
if (typeof this.view.initialize === 'function') {
|
|
392
|
+
return this.view.initialize.apply(this.view, arguments);
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this.isInitlized = true;
|
|
398
|
+
const config = this.config;
|
|
399
|
+
|
|
400
|
+
// Set basic properties (giữ nguyên tên từ code gốc)
|
|
401
|
+
this.id = config.viewId || uniqId();
|
|
402
|
+
deleteProp(config, 'viewId');
|
|
403
|
+
this.init = config.init || this._emptyFn;
|
|
404
|
+
deleteProp(config, 'init');
|
|
405
|
+
this.addCSS = config.addCSS || this._emptyFn;
|
|
406
|
+
deleteProp(config, 'addCSS');
|
|
407
|
+
this.removeCSS = config.removeCSS || this._emptyFn;
|
|
408
|
+
deleteProp(config, 'removeCSS');
|
|
409
|
+
this.superViewPath = config.superViewPath || config.superView;
|
|
410
|
+
deleteProp(config, 'superViewPath');
|
|
411
|
+
deleteProp(config, 'superView');
|
|
412
|
+
this.hasSuperView = config.hasSuperView;
|
|
413
|
+
deleteProp(config, 'hasSuperView');
|
|
414
|
+
this.hasAwaitData = config.hasAwaitData;
|
|
415
|
+
deleteProp(config, 'hasAwaitData');
|
|
416
|
+
this.hasFetchData = config.hasFetchData;
|
|
417
|
+
deleteProp(config, 'hasFetchData');
|
|
418
|
+
this.fetch = config.fetch;
|
|
419
|
+
deleteProp(config, 'fetch');
|
|
420
|
+
this.usesVars = config.usesVars;
|
|
421
|
+
deleteProp(config, 'usesVars');
|
|
422
|
+
this.hasSections = config.hasSections;
|
|
423
|
+
deleteProp(config, 'hasSections');
|
|
424
|
+
this.hasSectionPreload = config.hasSectionPreload;
|
|
425
|
+
deleteProp(config, 'hasSectionPreload');
|
|
426
|
+
this.renderLongSections = config.renderLongSections || [];
|
|
427
|
+
deleteProp(config, 'renderLongSections');
|
|
428
|
+
this.hasPrerender = config.hasPrerender;
|
|
429
|
+
deleteProp(config, 'hasPrerender');
|
|
430
|
+
this.sections = config.sections;
|
|
431
|
+
deleteProp(config, 'sections');
|
|
432
|
+
this._templateManager.initializeWrapperConfig(config.wrapperConfig);
|
|
433
|
+
deleteProp(config, 'wrapperConfig');
|
|
434
|
+
this.resources = config.resources || [];
|
|
435
|
+
deleteProp(config, 'resources');
|
|
436
|
+
this.scripts = config.scripts || [];
|
|
437
|
+
deleteProp(config, 'scripts');
|
|
438
|
+
this.styles = config.styles || [];
|
|
439
|
+
deleteProp(config, 'styles');
|
|
440
|
+
this.events = {};
|
|
441
|
+
|
|
442
|
+
this.subscribeStates = false;
|
|
443
|
+
this._lifecycleManager.beforeCreate();
|
|
444
|
+
// Process defined properties and methods (giữ nguyên logic từ code gốc)
|
|
445
|
+
this.processDefinedProperties(config);
|
|
446
|
+
|
|
447
|
+
if (config && config.data && typeof config.data === 'object' && config.data.__SSR_VIEW_ID__) {
|
|
448
|
+
config.data.__SSR_VIEW_ID__ = null;
|
|
449
|
+
deleteProp(config.data, '__SSR_VIEW_ID__');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Merge data
|
|
453
|
+
this.data = { ...(this.data || {}), ...(config.data || {}) };
|
|
454
|
+
|
|
455
|
+
this._eventManager.updateController(this);
|
|
456
|
+
this._resourceManager.updateController(this);
|
|
457
|
+
|
|
458
|
+
this.commitConstructorData();
|
|
459
|
+
|
|
460
|
+
// Call lifecycle hooks
|
|
461
|
+
this._lifecycleManager.created(); // ✅ Insert styles here
|
|
462
|
+
|
|
463
|
+
this._lifecycleManager.beforeInit();
|
|
464
|
+
this._lifecycleManager.init();
|
|
465
|
+
this._lifecycleManager.afterInit();
|
|
466
|
+
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Insert styles into DOM
|
|
473
|
+
* Styles should be inserted before render (in created hook)
|
|
474
|
+
*/
|
|
475
|
+
/**
|
|
476
|
+
* Insert styles into DOM with reference counting
|
|
477
|
+
* Delegated to ResourceManager for better code organization
|
|
478
|
+
*/
|
|
479
|
+
insertStyles() {
|
|
480
|
+
return this._resourceManager.insertStyles();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Insert scripts into DOM
|
|
485
|
+
* Scripts should be inserted after DOM ready (in mounted hook)
|
|
486
|
+
*/
|
|
487
|
+
/**
|
|
488
|
+
* Insert scripts into DOM
|
|
489
|
+
* Scripts should be inserted after DOM ready (in mounted hook)
|
|
490
|
+
* Delegated to ResourceManager for better code organization
|
|
491
|
+
*/
|
|
492
|
+
insertScripts() {
|
|
493
|
+
return this._resourceManager.insertScripts();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Remove styles from registry (but not from DOM if other views use them)
|
|
498
|
+
*/
|
|
499
|
+
/**
|
|
500
|
+
* Remove styles from registry (but not from DOM if other views use them)
|
|
501
|
+
* Delegated to ResourceManager for better code organization
|
|
502
|
+
*/
|
|
503
|
+
removeStyles() {
|
|
504
|
+
return this._resourceManager.removeStyles();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Remove all styles from DOM that belong to this view path
|
|
510
|
+
* Used as fallback when registry-based removal fails
|
|
511
|
+
* @param {string} viewPath - Optional view path, defaults to this.path
|
|
512
|
+
*/
|
|
513
|
+
/**
|
|
514
|
+
* Remove all styles from DOM that belong to this view path
|
|
515
|
+
* Used as fallback when registry-based removal fails
|
|
516
|
+
* @param {string} viewPath - Optional view path, defaults to this.path
|
|
517
|
+
* Delegated to ResourceManager for better code organization
|
|
518
|
+
*/
|
|
519
|
+
removeStylesByViewPath(viewPath = null) {
|
|
520
|
+
return this._resourceManager.removeStylesByViewPath(viewPath);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Remove scripts from registry (but not from DOM if other views use them)
|
|
526
|
+
*/
|
|
527
|
+
/**
|
|
528
|
+
* Remove scripts from registry (but not from DOM if other views use them)
|
|
529
|
+
* Delegated to ResourceManager for better code organization
|
|
530
|
+
*/
|
|
531
|
+
removeScripts() {
|
|
532
|
+
return this._resourceManager.removeScripts();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Process defined properties and methods from config
|
|
538
|
+
* @private
|
|
539
|
+
*/
|
|
540
|
+
/**
|
|
541
|
+
* Process defined properties and methods from config
|
|
542
|
+
* Delegated to ConfigurationManager
|
|
543
|
+
*/
|
|
544
|
+
processDefinedProperties(config) {
|
|
545
|
+
return this._configManager.processDefinedProperties(config);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Set scope for the view
|
|
552
|
+
* Delegated to ConfigurationManager
|
|
553
|
+
*/
|
|
554
|
+
setScope(scope) {
|
|
555
|
+
return this._configManager.setScope(scope);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Commit constructor data
|
|
561
|
+
* Delegated to ConfigurationManager
|
|
562
|
+
*/
|
|
563
|
+
commitConstructorData() {
|
|
564
|
+
return this._configManager.commitConstructorData();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Add event configuration for view engine
|
|
569
|
+
* @param {string} eventType - Type of event
|
|
570
|
+
* @param {Array} handlers - Array of handler objects
|
|
571
|
+
* @returns {string} Event attribute string
|
|
572
|
+
* @example
|
|
573
|
+
* <AppViewEngine>.addEventConfig('click', [{handler: 'handleClick', params: [1, 2, 3]}]);
|
|
574
|
+
*/
|
|
575
|
+
addEventConfig(eventType, handlers) {
|
|
576
|
+
if (typeof eventType !== 'string' || eventType === '') {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (typeof handlers !== 'object' || handlers === null) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
return this.addEventStack(eventType, handlers);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Render the view with data
|
|
589
|
+
* @param {Object} _data - Additional data to merge
|
|
590
|
+
* @returns {string|Object} Rendered content
|
|
591
|
+
*/
|
|
592
|
+
/**
|
|
593
|
+
* Render the view
|
|
594
|
+
* Delegated to RenderEngine for better code organization
|
|
595
|
+
*/
|
|
596
|
+
render() {
|
|
597
|
+
return this._renderEngine.render();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Virtual render the view with data (Scan version)
|
|
602
|
+
* @param {Object} _data - Additional data to merge
|
|
603
|
+
* @returns {string|Object} Virtual rendered content
|
|
604
|
+
*/
|
|
605
|
+
virtualRender() {
|
|
606
|
+
return this._renderEngine.virtualRender();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Prerender the view with data
|
|
614
|
+
* @param {Object} _data - Additional data to merge
|
|
615
|
+
* @returns {string|Object} Prerendered content
|
|
616
|
+
*/
|
|
617
|
+
prerender(_data = {}) {
|
|
618
|
+
return this._renderEngine.prerender(_data);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Virtual prerender the view with data (Scan version)
|
|
623
|
+
* @param {Object} _data - Additional data to merge
|
|
624
|
+
* @returns {string|Object} Virtual prerendered content
|
|
625
|
+
*/
|
|
626
|
+
virtualPrerender(_data = {}) {
|
|
627
|
+
return this._renderEngine.virtualPrerender(_data);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Replace view content with new HTML
|
|
635
|
+
* @param {string} htmlString - New HTML content
|
|
636
|
+
* @returns {boolean} True if replaced successfully
|
|
637
|
+
*/
|
|
638
|
+
replaceView(htmlString) {
|
|
639
|
+
if (this.isHtmlString(htmlString)) {
|
|
640
|
+
const container = document.createElement('div');
|
|
641
|
+
container.innerHTML = htmlString;
|
|
642
|
+
const frag = container.content;
|
|
643
|
+
const oldElements = document.querySelectorAll(`[x-ref-view="${this.id}"]`);
|
|
644
|
+
const elemtntCount = oldElements.length;
|
|
645
|
+
for (let i = elemtntCount - 1; i > 0; i--) {
|
|
646
|
+
const oldElement = oldElements[i];
|
|
647
|
+
const newElement = frag.childNodes[i];
|
|
648
|
+
oldElement.parentNode.replaceChild(newElement, oldElement);
|
|
649
|
+
}
|
|
650
|
+
const oldElement = oldElements[0];
|
|
651
|
+
oldElement.parentNode.replaceChild(frag, oldElement);
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
refresh(data = null) {
|
|
658
|
+
return this._renderEngine.refresh(data);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
onChildrenRefresh(childName, childIndex) {
|
|
663
|
+
return this._lifecycleManager.onChildrenRefresh(childName, childIndex);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Lifecycle: Called when super view is mounted (giữ nguyên tên từ code gốc)
|
|
667
|
+
*/
|
|
668
|
+
onSuperViewMounted() {
|
|
669
|
+
this._lifecycleManager.mounted();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Lifecycle: Called when super view is unmounted (giữ nguyên tên từ code gốc)
|
|
674
|
+
*/
|
|
675
|
+
onSuperViewUnmounted() {
|
|
676
|
+
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Lifecycle: Called when parent view is mounted (giữ nguyên tên từ code gốc)
|
|
681
|
+
*/
|
|
682
|
+
onParentMounted() {
|
|
683
|
+
this._lifecycleManager.mounted();
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Lifecycle: Called when parent view is unmounted (giữ nguyên tên từ code gốc)
|
|
687
|
+
*/
|
|
688
|
+
onParentUnmounted() {
|
|
689
|
+
// Placeholder for when parent view is unmounted
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Hàm parse string HTML thành DOM, thêm thuộc tính x-ref-view cho các phần tử con level 0, trả về string HTML mới.
|
|
696
|
+
* @param {string} htmlString - Chuỗi HTML đầu vào
|
|
697
|
+
* @param {string|number} id - Giá trị cho thuộc tính x-ref-view
|
|
698
|
+
* @returns {string} Chuỗi HTML đã thêm thuộc tính x-ref-view cho các phần tử level 0
|
|
699
|
+
*/
|
|
700
|
+
addXRefViewToRootElements(htmlString) {
|
|
701
|
+
// Tạo một container ảo để parse HTML
|
|
702
|
+
const container = document.createElement('div');
|
|
703
|
+
container.innerHTML = htmlString;
|
|
704
|
+
|
|
705
|
+
// Lặp qua các phần tử con trực tiếp (level 0)
|
|
706
|
+
Array.from(container.children).forEach(child => {
|
|
707
|
+
child.setAttribute('x-ref-view', this.id);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// Trả về HTML đã được thêm thuộc tính
|
|
711
|
+
return container.innerHTML;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Check if string is HTML
|
|
716
|
+
* @param {string} str - String to check
|
|
717
|
+
* @returns {boolean} True if HTML string
|
|
718
|
+
* @private
|
|
719
|
+
*/
|
|
720
|
+
isHtmlString(str) {
|
|
721
|
+
return /<[a-z][\s\S]*>/i.test(str);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
renderPlaceholder() {
|
|
727
|
+
return `<div class="${ATTR.className('placeholder')}" ${ATTR.KEYS.VIEW_ID}="${this.id}"></div>`;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
showError(error) {
|
|
731
|
+
if (this.isSuperView) {
|
|
732
|
+
return `<div class="${ATTR.className('ERROR_VIEW')}">${error}</div>`;
|
|
733
|
+
}
|
|
734
|
+
else if (this.hasSuperView) {
|
|
735
|
+
if (this.renderLongSections.length > 0) {
|
|
736
|
+
return this.renderLongSections.map(section => {
|
|
737
|
+
return this.App.View.section(section, `<div class="${ATTR.className('section-error')}" ${ATTR.KEYS.VIEW_SECTION_REF}="${this.id}">${error}</div>`, 'html');
|
|
738
|
+
}).join('');
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
return `<div class="${ATTR.className('ERROR_VIEW')}" ${ATTR.KEYS.VIEW_ID}="${this.id}">${error}</div>`;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
if (this.renderLongSections.length > 0) {
|
|
746
|
+
return this.renderLongSections.map(section => {
|
|
747
|
+
return this.App.View.section(section, `<div class="${ATTR.className('section-error')}" ${ATTR.KEYS.VIEW_SECTION_REF}="${this.id}">${error}</div>`, 'html');
|
|
748
|
+
}).join('');
|
|
749
|
+
}
|
|
750
|
+
return `<div class="${ATTR.className('ERROR_VIEW')}" ${ATTR.KEYS.VIEW_ID}="${this.id}">${error}</div>`;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
showErrorScan(error) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Reset original view
|
|
761
|
+
* Delegated to ViewHierarchyManager
|
|
762
|
+
*/
|
|
763
|
+
resetOriginalView() {
|
|
764
|
+
return this._hierarchyManager.resetOriginalView();
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Eject original view
|
|
769
|
+
* Delegated to ViewHierarchyManager
|
|
770
|
+
*/
|
|
771
|
+
ejectOriginalView() {
|
|
772
|
+
return this._hierarchyManager.ejectOriginalView();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Remove this view and all children
|
|
777
|
+
* Delegated to ViewHierarchyManager
|
|
778
|
+
*/
|
|
779
|
+
remove() {
|
|
780
|
+
return this._hierarchyManager.remove();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Insert resources into DOM
|
|
785
|
+
*/
|
|
786
|
+
insertResources() {
|
|
787
|
+
this.resources.forEach(resource => {
|
|
788
|
+
if (document.querySelector(`[data-resource-uuid="${resource.uuid}"]`)) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const element = document.createElement(resource.tag);
|
|
792
|
+
Object.entries(resource.attrs).forEach(([key, value]) => {
|
|
793
|
+
element.setAttribute(key, value);
|
|
794
|
+
});
|
|
795
|
+
element.setAttribute('data-resource-uuid', resource.uuid);
|
|
796
|
+
if (resource.tag === 'script') {
|
|
797
|
+
document.body.appendChild(element);
|
|
798
|
+
}
|
|
799
|
+
else if (resource.tag === 'link') {
|
|
800
|
+
document.head.appendChild(element);
|
|
801
|
+
}
|
|
802
|
+
else if (resource.tag === 'style') {
|
|
803
|
+
document.head.appendChild(element);
|
|
804
|
+
}
|
|
805
|
+
document.head.appendChild(element);
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Remove resources from DOM
|
|
811
|
+
*/
|
|
812
|
+
removeResources() {
|
|
813
|
+
this.resources.forEach(resource => {
|
|
814
|
+
const element = document.querySelector(`[data-resource-uuid="${resource.uuid}"]`);
|
|
815
|
+
if (element) {
|
|
816
|
+
element.remove();
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
__showError(error) {
|
|
823
|
+
logger.error(`ViewEngine Error [${this.path}]: ${error}`);
|
|
824
|
+
return this.showError(error);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
__section(name, content, type) {
|
|
828
|
+
return this._templateManager.section(name, content, type);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
__yield(name, defaultValue = '') {
|
|
832
|
+
return this._templateManager.yieldSection(name, defaultValue);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
__yieldContent(name, defaultValue = '') {
|
|
836
|
+
return this._templateManager.yieldContent(name, defaultValue);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
__execute(...args) {
|
|
842
|
+
return this.App.View.execute(...args);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
__subscribe(...args) {
|
|
846
|
+
// return this.subscribe(...args);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
__text(...args) {
|
|
851
|
+
return this.App.View.text(...args);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
__follow(stateKeys = [], renderBlock = () => '') {
|
|
856
|
+
// @follow directive has been removed - use __watch instead
|
|
857
|
+
console.warn('@follow directive is deprecated. Use __watch() instead.');
|
|
858
|
+
return this.__watch(uniqId(), stateKeys, renderBlock);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
__attr(attrs = {}) {
|
|
862
|
+
if (typeof attrs !== 'object' || attrs === null) {
|
|
863
|
+
return '';
|
|
864
|
+
}
|
|
865
|
+
let result = this._reactiveManager.renderBindingAttribute(attrs);
|
|
866
|
+
return result;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
__watch(watchID, watchKeys = [], callback = () => { }) {
|
|
870
|
+
return this._reactiveManager.renderWatchComponent(watchID, watchKeys, callback);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
__output(subscribeKeys = [], renderBlock = () => '') {
|
|
874
|
+
if (this.isVirtualRendering) {
|
|
875
|
+
return this._reactiveManager.renderOutputComponentScan(subscribeKeys, renderBlock);
|
|
876
|
+
}
|
|
877
|
+
return this._reactiveManager.renderOutputComponent(subscribeKeys, renderBlock);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
__outputEscaped(subscribeKeys = [], renderBlock = () => '') {
|
|
881
|
+
return this._reactiveManager.renderOutputEscapedComponent(subscribeKeys, renderBlock);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Unified reactive component method
|
|
886
|
+
* @param {string} reactiveID - Component ID
|
|
887
|
+
* @param {Array<string>} stateKeys - State keys to watch
|
|
888
|
+
* @param {Function} renderBlock - Render function
|
|
889
|
+
* @param {Object} options - Component options
|
|
890
|
+
* @returns {string} Rendered component
|
|
891
|
+
*/
|
|
892
|
+
__reactive(reactiveID, stateKeys = [], renderBlock = () => '', options = {}) {
|
|
893
|
+
const {
|
|
894
|
+
type = 'output',
|
|
895
|
+
escapeHTML = false
|
|
896
|
+
} = options;
|
|
897
|
+
|
|
898
|
+
if (type === 'watch') {
|
|
899
|
+
return this._reactiveManager.renderWatchComponent(reactiveID, stateKeys, renderBlock);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (escapeHTML) {
|
|
903
|
+
return this._reactiveManager.renderOutputEscapedComponent(stateKeys, renderBlock);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return this._reactiveManager.renderOutputComponent(stateKeys, renderBlock);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
__checked(condition) {
|
|
910
|
+
return condition ? 'checked' : '';
|
|
911
|
+
}
|
|
912
|
+
__classBinding(bindings = []) {
|
|
913
|
+
if (!Array.isArray(bindings)) {
|
|
914
|
+
return '';
|
|
915
|
+
}
|
|
916
|
+
return this._reactiveManager.renderClassBinding(bindings);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
__styleBinding(watchKeys = [], styles = []) {
|
|
920
|
+
// Generate reactive style binding
|
|
921
|
+
// styles = [['color', 'red'], ['font-size', '14px']]
|
|
922
|
+
// Returns: "color: red; font-size: 14px;"
|
|
923
|
+
|
|
924
|
+
const generateStyle = () => {
|
|
925
|
+
if (!Array.isArray(styles)) return '';
|
|
926
|
+
|
|
927
|
+
return styles
|
|
928
|
+
.map(([prop, value]) => {
|
|
929
|
+
if (prop && value !== null && value !== undefined) {
|
|
930
|
+
return `${prop}: ${value}`;
|
|
931
|
+
}
|
|
932
|
+
return '';
|
|
933
|
+
})
|
|
934
|
+
.filter(s => s)
|
|
935
|
+
.join('; ');
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
if (watchKeys && watchKeys.length > 0) {
|
|
939
|
+
// Reactive - watch state changes
|
|
940
|
+
return this._reactiveManager.renderWatchComponent(watchKeys, generateStyle);
|
|
941
|
+
} else {
|
|
942
|
+
// Static - generate once
|
|
943
|
+
return generateStyle();
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
__showBinding(watchKeys = [], condition) {
|
|
948
|
+
// Generate reactive visibility binding
|
|
949
|
+
// Similar to v-show in Vue - toggles display property
|
|
950
|
+
// Returns: "display: none;" or "" based on condition
|
|
951
|
+
|
|
952
|
+
const generateDisplay = () => {
|
|
953
|
+
return condition ? '' : 'display: none;';
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
if (watchKeys && watchKeys.length > 0) {
|
|
957
|
+
// Reactive - watch state changes
|
|
958
|
+
return this._reactiveManager.renderWatchComponent(watchKeys, generateDisplay);
|
|
959
|
+
} else {
|
|
960
|
+
// Static - generate once
|
|
961
|
+
return generateDisplay();
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
__block(name, attributes = {}, content) {
|
|
968
|
+
return this._templateManager.addBlock(name, attributes, content);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
__useBlock(name, defaultValue = '') {
|
|
972
|
+
return this._templateManager.useBlock(name, defaultValue);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
__mountBlock(name, defaultValue = '') {
|
|
976
|
+
return this._templateManager.mountBlock(name, defaultValue);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
__subscribeBlock(name, defaultValue = '') {
|
|
980
|
+
return this._templateManager.subscribeBlock(name, defaultValue);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
__include(path, data = {}) {
|
|
984
|
+
if (this.isVirtualRendering) {
|
|
985
|
+
const childParams = this._templateManager.childrenConfig[this._templateManager.childrenIndex];
|
|
986
|
+
if (!(childParams && childParams.name === path)) {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
this._templateManager.childrenIndex++;
|
|
990
|
+
const childConfig = this.App.View.ssrViewManager.getInstance(childParams.name, childParams.id);
|
|
991
|
+
if (!childConfig) {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
const childData = { ...data, ...childConfig.data, __SSR_VIEW_ID__: childParams.id };
|
|
995
|
+
const child = this.$include(childParams.name, childData);
|
|
996
|
+
if (!child) {
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
child.__.__scan(childConfig);
|
|
1000
|
+
return this.App.View.renderView(child, null, true);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (this.isRefreshing) {
|
|
1004
|
+
let childCtrl = this.children.find(c => c.__scope.name === path && c.__scope.index === this._templateManager.childrenIndex);
|
|
1005
|
+
if (childCtrl) {
|
|
1006
|
+
childCtrl.isFirstClientRendering = false;
|
|
1007
|
+
childCtrl._templateManager.wrapperConfig.enable = true;
|
|
1008
|
+
this._templateManager.childrenIndex++;
|
|
1009
|
+
const result = this.childrenNeedToRefreshID === childCtrl.id ? childCtrl.rrend() : null;
|
|
1010
|
+
|
|
1011
|
+
return childCtrl.render();
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return this.$include(path, data);
|
|
1015
|
+
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
__includeif(path, data = {}) {
|
|
1019
|
+
if (!this.App.View.exists(path)) {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
return this.__include(path, data);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
__includewhen(condition, path, data = {}) {
|
|
1026
|
+
if (!condition) {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
return this.__include(path, data);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
__extends(name, data = {}) {
|
|
1034
|
+
if (this.isVirtualRendering) {
|
|
1035
|
+
if (!this.App.View.exists(name)) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
let superViewConfig = null;
|
|
1039
|
+
let superViewOfChildren = this._templateManager.childrenConfig.find((child, index) => child.name === name && index == this._templateManager.childrenConfig.length - 1);
|
|
1040
|
+
if (superViewOfChildren) {
|
|
1041
|
+
superViewConfig = this.App.View.ssrViewManager.getInstance(superViewOfChildren.name, superViewOfChildren.id);
|
|
1042
|
+
} else {
|
|
1043
|
+
superViewConfig = this.App.View.ssrViewManager.scan(name);
|
|
1044
|
+
}
|
|
1045
|
+
if (!superViewConfig) {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
const superViewData = { ...data, ...superViewConfig.data, __SSR_VIEW_ID__: superViewConfig.viewId };
|
|
1049
|
+
const superView = this.$extends(name, superViewData);
|
|
1050
|
+
if (!superView) {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
superView.__.__scan(superViewConfig);
|
|
1054
|
+
return superView;
|
|
1055
|
+
}
|
|
1056
|
+
return this.$extends(name, data);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
__setLoopContext(length) {
|
|
1061
|
+
let parent = this.loopContext;
|
|
1062
|
+
this.loopContext = new LoopContext(parent);
|
|
1063
|
+
this.loopContext.setCount(length);
|
|
1064
|
+
return this.loopContext;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
__resetLoopContext() {
|
|
1068
|
+
if (this.loopContext && this.loopContext.parent) {
|
|
1069
|
+
let parent = this.loopContext.parent;
|
|
1070
|
+
this.loopContext.parent = null;
|
|
1071
|
+
Object.freeze(this.loopContext);
|
|
1072
|
+
this.loopContext = parent;
|
|
1073
|
+
} else if (this.loopContext) {
|
|
1074
|
+
Object.freeze(this.loopContext);
|
|
1075
|
+
this.loopContext = null;
|
|
1076
|
+
} else {
|
|
1077
|
+
this.loopContext = null;
|
|
1078
|
+
}
|
|
1079
|
+
return this.loopContext;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Lặp qua danh sách hoặc đối tượng và gọi callback cho mỗi phần tử hoặc key
|
|
1084
|
+
* @param {Array|Object} list danh sách cần lặp hoặc đối tượng cần lặp
|
|
1085
|
+
* @param {function(item: any, defaultKeyName: string, index: number, loopContext: LoopContext): string} callback hàm callback để xử lý mỗi phần tử hoặc key của đối tượng
|
|
1086
|
+
* @returns {string} kết quả của việc lặp
|
|
1087
|
+
* @example
|
|
1088
|
+
* <AppViewEngine>.foreach([1, 2, 3], (item, defaultKeyName, index, loopContext) => {
|
|
1089
|
+
* return `<div>${defaultKeyName}: ${item}</div>`;
|
|
1090
|
+
* });
|
|
1091
|
+
* // returns '<div>1</div><div>2</div><div>3</div>'
|
|
1092
|
+
* <AppViewEngine>.foreach({a: 1, b: 2, c: 3}, (value, key, index, loopContext) => {
|
|
1093
|
+
* return `<div>${key}: ${value}</div>`;
|
|
1094
|
+
* });
|
|
1095
|
+
*/
|
|
1096
|
+
__foreach(list, callback) {
|
|
1097
|
+
if (!list || (typeof list !== 'object')) {
|
|
1098
|
+
return '';
|
|
1099
|
+
}
|
|
1100
|
+
let result = '';
|
|
1101
|
+
if (Array.isArray(list)) {
|
|
1102
|
+
let loopContext = this.__setLoopContext(list);
|
|
1103
|
+
loopContext.setType('increment');
|
|
1104
|
+
list.forEach((item, index) => {
|
|
1105
|
+
loopContext.setCurrentTimes(index);
|
|
1106
|
+
result += callback(item, index, index, loopContext);
|
|
1107
|
+
});
|
|
1108
|
+
this.__resetLoopContext();
|
|
1109
|
+
} else {
|
|
1110
|
+
let count = Object.keys(list).length;
|
|
1111
|
+
let loopContext = this.__setLoopContext(count);
|
|
1112
|
+
loopContext.setType('increment');
|
|
1113
|
+
let index = 0;
|
|
1114
|
+
Object.entries(list).forEach(([key, value]) => {
|
|
1115
|
+
loopContext.setCurrentTimes(index);
|
|
1116
|
+
result += callback(value, key, index, loopContext);
|
|
1117
|
+
index = index + 1;
|
|
1118
|
+
});
|
|
1119
|
+
this.__resetLoopContext();
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
return result;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
__for(loopType = 'increment', start = 0, end = 0, execute = (loop) => '') {
|
|
1126
|
+
const LoopContext = this.__setLoopContext(end);
|
|
1127
|
+
LoopContext.setType(loopType);
|
|
1128
|
+
const result = typeof execute === 'function' ? execute(LoopContext) : '';
|
|
1129
|
+
this.__resetLoopContext();
|
|
1130
|
+
return result;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Scan and hydrate server-rendered view
|
|
1136
|
+
* This method:
|
|
1137
|
+
* 1. Finds DOM elements for this view
|
|
1138
|
+
* 2. Attaches event handlers from server data
|
|
1139
|
+
* 3. Sets up state subscriptions
|
|
1140
|
+
* 4. Stores children and following block references
|
|
1141
|
+
*
|
|
1142
|
+
* @param {Object} config - Server-side view configuration
|
|
1143
|
+
* @param {string} config.viewId - View instance ID
|
|
1144
|
+
* @param {Object} config.data - View data from server
|
|
1145
|
+
* @param {Object} config.events - Event handlers to attach
|
|
1146
|
+
* @param {Array} config.following - Following blocks to setup
|
|
1147
|
+
* @param {Array} config.children - Child views to scan
|
|
1148
|
+
* @param {Object} config.parent - Parent view reference
|
|
1149
|
+
*/
|
|
1150
|
+
/**
|
|
1151
|
+
* Scan view configuration and setup reactive components
|
|
1152
|
+
* Delegated to RenderEngine for better code organization
|
|
1153
|
+
*/
|
|
1154
|
+
__scan(config) {
|
|
1155
|
+
return this._renderEngine.scan(config);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Find and store DOM elements for this view
|
|
1160
|
+
* @private
|
|
1161
|
+
* @param {string} viewId - View instance ID
|
|
1162
|
+
*/
|
|
1163
|
+
__scanDOMElements(viewId) {
|
|
1164
|
+
return this._renderEngine.scanDOMElements(viewId);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Store children view references for hydration
|
|
1169
|
+
* @private
|
|
1170
|
+
* @param {Array} children - Child view configurations
|
|
1171
|
+
*/
|
|
1172
|
+
__storeChildrenReferences(children) {
|
|
1173
|
+
this._templateManager.storeChildrenReferences(children);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
$extends(path, data = {}) {
|
|
1179
|
+
return this._hierarchyManager.createSuperView(path, data);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
$include(path, data = {}) {
|
|
1183
|
+
return this._hierarchyManager.createChildView(path, data);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
addEventStack(eventType, handlers) {
|
|
1188
|
+
if (typeof handlers !== 'object' || handlers === null) {
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
let eventIndex = this.eventIndex++;
|
|
1192
|
+
let eventID = this.id + '-' + eventType + '-' + eventIndex;
|
|
1193
|
+
if (typeof eventID !== 'string' || eventID === '') {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
if (typeof this.events[eventType] === 'undefined') {
|
|
1197
|
+
this.events[eventType] = {};
|
|
1198
|
+
}
|
|
1199
|
+
if (typeof this.events[eventType][eventID] === 'undefined') {
|
|
1200
|
+
this.events[eventType][eventID] = []
|
|
1201
|
+
}
|
|
1202
|
+
this.events[eventType][eventID].push(...handlers);
|
|
1203
|
+
|
|
1204
|
+
return ` data-${eventType}-id="${eventID}"`;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
__addEventConfig(eventType, handlers) {
|
|
1209
|
+
return this.addEventConfig(eventType, handlers);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Set event configuration for view engine
|
|
1214
|
+
* @param {Object} events - Event configuration object
|
|
1215
|
+
*/
|
|
1216
|
+
setEventConfig(events) {
|
|
1217
|
+
if (typeof events !== 'object' || events === null) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
Object.entries(events).forEach((eventType, eventObjectList) => {
|
|
1221
|
+
if (typeof eventObjectList !== 'object' || eventObjectList === null) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
Object.entries(eventObjectList).forEach((eventID, handlers) => {
|
|
1225
|
+
this.addEventStack(eventType, eventID, handlers);
|
|
1226
|
+
});
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
wrapattr() {
|
|
1233
|
+
return this._templateManager.wrapperAttribute();
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
startWrapper(tag = null, attributes = {}) {
|
|
1237
|
+
return this._templateManager.startWrapper(tag, attributes);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
endWrapper() {
|
|
1241
|
+
return this._templateManager.endWrapper();
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
onStateDataChanges() {
|
|
1248
|
+
const data = this.stateChangeData;
|
|
1249
|
+
this.stateChangeData = null;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* Set App instance
|
|
1254
|
+
* @param {Object} app - App instance
|
|
1255
|
+
* @returns {ViewController} This instance for chaining
|
|
1256
|
+
*/
|
|
1257
|
+
setApp(app) {
|
|
1258
|
+
this.App = app;
|
|
1259
|
+
return this;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
setUrlPath(urlPath) {
|
|
1263
|
+
this.urlPath = urlPath;
|
|
1264
|
+
return this;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Set super view
|
|
1268
|
+
* Delegated to ViewHierarchyManager
|
|
1269
|
+
*/
|
|
1270
|
+
setSuperView(superView) {
|
|
1271
|
+
return this._hierarchyManager.setSuperView(superView);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Set parent view
|
|
1276
|
+
* Delegated to ViewHierarchyManager
|
|
1277
|
+
*/
|
|
1278
|
+
setParent(parent) {
|
|
1279
|
+
return this._hierarchyManager.setParent(parent);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* Set original view
|
|
1284
|
+
* Delegated to ViewHierarchyManager
|
|
1285
|
+
*/
|
|
1286
|
+
setOriginalView(originalView) {
|
|
1287
|
+
return this._hierarchyManager.setOriginalView(originalView);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Add child view
|
|
1292
|
+
* Delegated to ViewHierarchyManager
|
|
1293
|
+
*/
|
|
1294
|
+
addChild(child, data = {}) {
|
|
1295
|
+
return this._hierarchyManager.addChild(child, data);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Remove child view
|
|
1300
|
+
* Delegated to ViewHierarchyManager
|
|
1301
|
+
*/
|
|
1302
|
+
removeChild(child) {
|
|
1303
|
+
return this._hierarchyManager.removeChild(child);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Update data
|
|
1308
|
+
* Delegated to ConfigurationManager
|
|
1309
|
+
*/
|
|
1310
|
+
updateData(__data = {}) {
|
|
1311
|
+
return this._configManager.updateData(__data);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Update variable data
|
|
1316
|
+
* Delegated to ConfigurationManager
|
|
1317
|
+
*/
|
|
1318
|
+
updateVariableData(data = {}) {
|
|
1319
|
+
return this._configManager.updateVariableData(data);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* Update variable item
|
|
1324
|
+
* Delegated to ConfigurationManager
|
|
1325
|
+
*/
|
|
1326
|
+
updateVariableItem(key, value) {
|
|
1327
|
+
return this._configManager.updateVariableItem(key, value);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Set isSuperView flag
|
|
1332
|
+
* @param {boolean} isSuperView - Super view flag
|
|
1333
|
+
* @returns {AppViewEngine} This instance for chaining
|
|
1334
|
+
*/
|
|
1335
|
+
setIsSuperView(isSuperView) {
|
|
1336
|
+
this.isSuperView = isSuperView;
|
|
1337
|
+
return this;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** Events */
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
reset() {
|
|
1344
|
+
this._templateManager.resetChildrenIndex();
|
|
1345
|
+
this._reactiveManager.resetScanIndex();
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
clear() {
|
|
1349
|
+
// Use registry for proper cleanup if available
|
|
1350
|
+
if (this._childrenRegistry) {
|
|
1351
|
+
this._childrenRegistry.clear();
|
|
1352
|
+
} else {
|
|
1353
|
+
// Fallback to manual cleanup
|
|
1354
|
+
this.children.forEach(childCtrl => {
|
|
1355
|
+
if (childCtrl && childCtrl instanceof ViewController) {
|
|
1356
|
+
childCtrl.destroy();
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
this.children = [];
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
this._reactiveManager.clearForRefresh();
|
|
1363
|
+
this._bindingManager.reset();
|
|
1364
|
+
this.refElements = {};
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
query(selector) {
|
|
1368
|
+
for (const el of this.refElements) {
|
|
1369
|
+
// 1. chính element
|
|
1370
|
+
if (el.matches?.(selector)) {
|
|
1371
|
+
return el;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// 2. con bên trong
|
|
1375
|
+
const found = el.querySelector?.(selector);
|
|
1376
|
+
if (found) {
|
|
1377
|
+
return found;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
queryAll(selector) {
|
|
1384
|
+
const results = [];
|
|
1385
|
+
|
|
1386
|
+
for (const el of this.refElements) {
|
|
1387
|
+
// chính element
|
|
1388
|
+
if (el.matches?.(selector)) {
|
|
1389
|
+
results.push(el);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// con bên trong
|
|
1393
|
+
const found = el.querySelectorAll?.(selector);
|
|
1394
|
+
if (found?.length) {
|
|
1395
|
+
results.push(...found);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
return results;
|
|
1400
|
+
}
|
|
1401
|
+
// accessors
|
|
1402
|
+
|
|
1403
|
+
get type() {
|
|
1404
|
+
return this.viewType;
|
|
1405
|
+
}
|
|
1406
|
+
set type(value) {
|
|
1407
|
+
this.viewType = value;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
}
|