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,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LifecycleManager - Quản lý các lifecycle hooks của view
|
|
3
|
+
* Xử lý created, mounted, updated, destroyed và các hooks liên quan
|
|
4
|
+
*
|
|
5
|
+
* Tách từ ViewController.js để cải thiện khả năng bảo trì
|
|
6
|
+
* @module core/managers/LifecycleManager
|
|
7
|
+
* @author OneLaravel Team
|
|
8
|
+
* @since 2025-12-29
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import logger from '../services/LoggerService.js';
|
|
12
|
+
import { devLog } from '../../helpers/devWarnings.js';
|
|
13
|
+
|
|
14
|
+
export class LifecycleManager {
|
|
15
|
+
/**
|
|
16
|
+
* @param {ViewController} controller - Parent controller instance
|
|
17
|
+
*/
|
|
18
|
+
constructor(controller) {
|
|
19
|
+
this.controller = controller;
|
|
20
|
+
this.view = controller.view;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
beforeCreate() {
|
|
24
|
+
// logger.log(`🔵 beforeCreate: ${this.controller.path}`);
|
|
25
|
+
// Lifecycle: Được gọi trước khi tạo view
|
|
26
|
+
if (typeof this.view.beforeCreate === 'function') {
|
|
27
|
+
this.view.beforeCreate();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Lifecycle: Được gọi khi view được tạo (trước lần render đầu tiên)
|
|
33
|
+
*/
|
|
34
|
+
created() {
|
|
35
|
+
// logger.log(`🟢 created: ${this.controller.path}`);
|
|
36
|
+
if (typeof this.view.created === 'function') {
|
|
37
|
+
this.view.created();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Chèn styles trước khi render
|
|
41
|
+
this.controller.insertStyles();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Lifecycle: Được gọi trước khi view được cập nhật
|
|
46
|
+
*/
|
|
47
|
+
beforeUpdate() {
|
|
48
|
+
logger.log(`🟡 beforeUpdate: ${this.controller.path}`);
|
|
49
|
+
if (typeof this.view.beforeUpdate === 'function') {
|
|
50
|
+
this.view.beforeUpdate();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Lifecycle: Được gọi sau khi view được cập nhật
|
|
56
|
+
*/
|
|
57
|
+
updated() {
|
|
58
|
+
// logger.log(`🟠 updated: ${this.controller.path}`);
|
|
59
|
+
if (typeof this.view.updated === 'function') {
|
|
60
|
+
this.view.updated();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Lifecycle: Được gọi trước khi khởi tạo
|
|
66
|
+
*/
|
|
67
|
+
beforeInit() {
|
|
68
|
+
// logger.log(`🔷 beforeInit: ${this.controller.path}`);
|
|
69
|
+
if (typeof this.view.beforeInit === 'function') {
|
|
70
|
+
this.view.beforeInit();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Lifecycle: Được gọi trong quá trình khởi tạo
|
|
76
|
+
*/
|
|
77
|
+
init() {
|
|
78
|
+
// logger.log(`🔶 init: ${this.controller.path}`);
|
|
79
|
+
if (typeof this.view.init === 'function') {
|
|
80
|
+
this.view.init();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Lifecycle: Được gọi sau khi khởi tạo
|
|
86
|
+
*/
|
|
87
|
+
afterInit() {
|
|
88
|
+
// logger.log(`🔸 afterInit: ${this.controller.path}`);
|
|
89
|
+
if (typeof this.view.afterInit === 'function') {
|
|
90
|
+
this.view.afterInit();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Lifecycle: Được gọi trước khi view bị hủy
|
|
96
|
+
*/
|
|
97
|
+
beforeDestroy() {
|
|
98
|
+
// logger.log(`🔴 beforeDestroy: ${this.controller.path}`);
|
|
99
|
+
if (typeof this.view.beforeDestroy === 'function') {
|
|
100
|
+
this.view.beforeDestroy();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Lifecycle: Được gọi trong quá trình hủy
|
|
106
|
+
*/
|
|
107
|
+
destroying() {
|
|
108
|
+
// logger.log(`🟥 destroying: ${this.controller.path}`);
|
|
109
|
+
if (typeof this.view.destroying === 'function') {
|
|
110
|
+
this.view.destroying();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Lifecycle: Được gọi sau khi view bị hủy
|
|
116
|
+
*/
|
|
117
|
+
destroyed() {
|
|
118
|
+
// logger.log(`⬛ destroyed: ${this.controller.path}`);
|
|
119
|
+
if (typeof this.view.destroyed === 'function') {
|
|
120
|
+
this.view.destroyed();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Lifecycle: Được gọi trước khi view được mount
|
|
126
|
+
*/
|
|
127
|
+
beforeMount() {
|
|
128
|
+
// logger.log(`🟦 beforeMount: ${this.controller.path}`);
|
|
129
|
+
if (typeof this.view.beforeMount === 'function') {
|
|
130
|
+
this.view.beforeMount();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Lifecycle: Được gọi trong quá trình mounting
|
|
136
|
+
*/
|
|
137
|
+
mounting() {
|
|
138
|
+
// logger.log(`🟪 mounting: ${this.controller.path}`);
|
|
139
|
+
if (typeof this.view.mounting === 'function') {
|
|
140
|
+
this.view.mounting();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Lifecycle: Được gọi khi view được mount (sau khi DOM sẵn sàng)
|
|
146
|
+
* Đây là nơi scripts được chèn và event listeners được khởi động
|
|
147
|
+
*/
|
|
148
|
+
mounted() {
|
|
149
|
+
const ctrl = this.controller;
|
|
150
|
+
// logger.log(`🟩 mounted START: ${ctrl.path}`);
|
|
151
|
+
ctrl.isDestroyed = false;
|
|
152
|
+
|
|
153
|
+
if (!ctrl.isMarkupScanned) {
|
|
154
|
+
ctrl.__scanDOMElements(ctrl.id);
|
|
155
|
+
ctrl.isMarkupScanned = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!ctrl.isMounted) {
|
|
159
|
+
this.beforeMount();
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
this.mounting();
|
|
163
|
+
|
|
164
|
+
// Chèn scripts sau khi DOM sẵn sàng
|
|
165
|
+
ctrl.insertScripts();
|
|
166
|
+
|
|
167
|
+
// Thông báo super view và children
|
|
168
|
+
if (ctrl.originalView && ctrl.originalView instanceof this.App.View.Controller) {
|
|
169
|
+
ctrl.originalView.onSuperViewMounted();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Thông báo children (controller.children được duy trì bởi ChildrenRegistry)
|
|
173
|
+
if (ctrl.children && ctrl.children.length > 0) {
|
|
174
|
+
ctrl.children.forEach(childCtrl => {
|
|
175
|
+
if (childCtrl && childCtrl instanceof this.App.View.Controller) {
|
|
176
|
+
childCtrl.onParentMounted();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Mount ReactiveComponents (output & watch thống nhất)
|
|
182
|
+
if (ctrl._reactiveManager.reactiveComponents && ctrl._reactiveManager.reactiveComponents.size > 0) {
|
|
183
|
+
ctrl._reactiveManager.reactiveComponents.forEach(component => {
|
|
184
|
+
component.mounted();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Khởi động event listeners
|
|
189
|
+
ctrl._eventManager.startEventListener();
|
|
190
|
+
ctrl._bindingManager.startBindingEventListener();
|
|
191
|
+
ctrl._bindingManager.startClassBindingEventListener();
|
|
192
|
+
|
|
193
|
+
ctrl.isMounted = true;
|
|
194
|
+
ctrl.isReady = true;
|
|
195
|
+
ctrl.isRendered = true;
|
|
196
|
+
|
|
197
|
+
if (typeof ctrl.view.mounted === 'function') {
|
|
198
|
+
ctrl.view.mounted();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// logger.log(`✅ mounted COMPLETE: ${ctrl.path}`);
|
|
202
|
+
|
|
203
|
+
} catch (error) {
|
|
204
|
+
logger.warn('Error in mounted lifecycle hook:', error);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
ctrl.states.__.readyToCommit = true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
ctrl.isReadyToStateChangeListen = true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Lifecycle: Called before view is unmounted
|
|
215
|
+
*/
|
|
216
|
+
beforeUnmount() {
|
|
217
|
+
// logger.log(`🟨 beforeUnmount: ${this.controller.path}`);
|
|
218
|
+
if (typeof this.view.beforeUnmount === 'function') {
|
|
219
|
+
this.view.beforeUnmount();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Lifecycle: Called during unmounting
|
|
225
|
+
*/
|
|
226
|
+
unmounting() {
|
|
227
|
+
// logger.log(`🟧 unmounting: ${this.controller.path}`);
|
|
228
|
+
if (typeof this.view.unmounting === 'function') {
|
|
229
|
+
this.view.unmounting();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Lifecycle: Called when view is unmounted
|
|
235
|
+
* This is where scripts are removed and event listeners are stopped
|
|
236
|
+
*/
|
|
237
|
+
unmounted() {
|
|
238
|
+
const ctrl = this.controller;
|
|
239
|
+
// logger.log(`🔻 unmounted START: ${ctrl.path}`);
|
|
240
|
+
|
|
241
|
+
if (ctrl.isMounted) {
|
|
242
|
+
ctrl.isReadyToStateChangeListen = false;
|
|
243
|
+
ctrl.states.__.readyToCommit = false;
|
|
244
|
+
|
|
245
|
+
this.beforeUnmount();
|
|
246
|
+
this.unmounting();
|
|
247
|
+
|
|
248
|
+
// Remove scripts
|
|
249
|
+
ctrl.removeScripts();
|
|
250
|
+
|
|
251
|
+
// Stop event listeners
|
|
252
|
+
ctrl._eventManager.stopEventListener();
|
|
253
|
+
ctrl._bindingManager.stopBindingEventListener();
|
|
254
|
+
ctrl._bindingManager.stopClassBindingEventListener();
|
|
255
|
+
|
|
256
|
+
ctrl.isMounted = false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Notify super view and children
|
|
260
|
+
if (ctrl.originalView && ctrl.originalView instanceof this.App.View.Controller) {
|
|
261
|
+
ctrl.originalView.onSuperViewUnmounted();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (ctrl.children && ctrl.children.length > 0) {
|
|
265
|
+
ctrl.children.forEach(childCtrl => {
|
|
266
|
+
if (childCtrl && childCtrl instanceof this.App.View.Controller) {
|
|
267
|
+
childCtrl.onParentUnmounted();
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Unmount ReactiveComponents (unified output & watch)
|
|
273
|
+
if (ctrl._reactiveManager.reactiveComponents && ctrl._reactiveManager.reactiveComponents.size > 0) {
|
|
274
|
+
ctrl._reactiveManager.reactiveComponents.forEach(component => {
|
|
275
|
+
component.unmounted();
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (typeof ctrl.view.unmounted === 'function') {
|
|
280
|
+
ctrl.view.unmounted();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// logger.log(`✅ unmounted COMPLETE: ${ctrl.path}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Destroy view and cleanup resources
|
|
288
|
+
*/
|
|
289
|
+
destroy() {
|
|
290
|
+
const ctrl = this.controller;
|
|
291
|
+
// logger.log(`💀 destroy START: ${ctrl.path}`);
|
|
292
|
+
|
|
293
|
+
// Mark as destroyed to prevent processing after destroy
|
|
294
|
+
ctrl.isDestroyed = true;
|
|
295
|
+
this.beforeDestroy();
|
|
296
|
+
this.destroying();
|
|
297
|
+
|
|
298
|
+
// Save view path before cleanup (needed for removing styles from DOM)
|
|
299
|
+
const viewPath = ctrl.path;
|
|
300
|
+
|
|
301
|
+
// Reset pending flag to prevent processing after destroy
|
|
302
|
+
ctrl._stateChangePending = false;
|
|
303
|
+
|
|
304
|
+
// Clear state change collections
|
|
305
|
+
if (ctrl.changedStateKeys) {
|
|
306
|
+
ctrl.changedStateKeys.clear();
|
|
307
|
+
}
|
|
308
|
+
ctrl.changeStateQueueCount = 0;
|
|
309
|
+
|
|
310
|
+
this.unmounted(); // Will call removeScripts()
|
|
311
|
+
|
|
312
|
+
// Remove styles (will use fallback if styles array is empty)
|
|
313
|
+
ctrl.removeStyles();
|
|
314
|
+
|
|
315
|
+
// Final cleanup: Remove all styles with this view path from DOM
|
|
316
|
+
// This ensures CSS is removed even if registry is out of sync
|
|
317
|
+
if (viewPath) {
|
|
318
|
+
ctrl.removeStylesByViewPath(viewPath);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (ctrl.originalView && ctrl.originalView instanceof this.App.View.Controller) {
|
|
322
|
+
ctrl.originalView._lifecycleManager.destroy();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Destroy all children using ChildrenRegistry for proper cleanup
|
|
326
|
+
if (ctrl._childrenRegistry) {
|
|
327
|
+
ctrl._childrenRegistry.clear();
|
|
328
|
+
} else {
|
|
329
|
+
// Fallback: manual destroy if registry not available
|
|
330
|
+
if (ctrl.children && ctrl.children.length > 0) {
|
|
331
|
+
ctrl.children.forEach(childCtrl => {
|
|
332
|
+
if (childCtrl && childCtrl instanceof this.App.View.Controller) {
|
|
333
|
+
childCtrl._lifecycleManager.destroy();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
ctrl._reactiveManager.destroy();
|
|
340
|
+
if (ctrl.refElements && ctrl.refElements.length > 0) {
|
|
341
|
+
ctrl.refElements.forEach(element => element.parentNode && element.parentNode.removeChild(element));
|
|
342
|
+
ctrl.refElements = [];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
this.destroyed();
|
|
346
|
+
// logger.log(`☠️ destroy COMPLETE: ${ctrl.path}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
get App() {
|
|
350
|
+
return this.controller.App;
|
|
351
|
+
}
|
|
352
|
+
set App(value) {
|
|
353
|
+
devLog('LifecycleManager.App is read-only.');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReactiveManager - Quản lý các reactive component
|
|
3
|
+
* Xử lý ReactiveComponents và reactive bindings
|
|
4
|
+
* Sử dụng ReactiveComponent thống nhất cho mọi reactive rendering
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ReactiveComponent } from '../reactive/ReactiveComponent.js';
|
|
8
|
+
import { escapeString, hasData, uniqId } from '../../helpers/utils.js';
|
|
9
|
+
import logger from '../services/LoggerService.js';
|
|
10
|
+
|
|
11
|
+
export class ReactiveManager {
|
|
12
|
+
constructor(controller) {
|
|
13
|
+
this.controller = controller;
|
|
14
|
+
|
|
15
|
+
// Reactive components (thống nhất output & watch)
|
|
16
|
+
this.reactiveComponents = new Map();
|
|
17
|
+
this.reactiveComponentConfig = [];
|
|
18
|
+
this.reactiveComponentIndex = 0;
|
|
19
|
+
this.reactiveComponentScanIndex = 0;
|
|
20
|
+
this.reactiveComponentIDs = [];
|
|
21
|
+
this.reactiveComponentPrerenderIDs = [];
|
|
22
|
+
this.reactiveComponentRenderIDs = [];
|
|
23
|
+
|
|
24
|
+
// Following IDs - theo dõi reactive IDs trong các giai đoạn render
|
|
25
|
+
this.followingIDs = [];
|
|
26
|
+
this.followingRenderIDs = [];
|
|
27
|
+
this.followingPrerenderIDs = [];
|
|
28
|
+
|
|
29
|
+
// Hỗ trợ legacy - map tới reactive components
|
|
30
|
+
this.outputComponents = this.reactiveComponents;
|
|
31
|
+
this.outputComponentIDs = this.reactiveComponentIDs;
|
|
32
|
+
this.watchComponents = this.reactiveComponents;
|
|
33
|
+
this.watchComponentIDs = this.reactiveComponentIDs;
|
|
34
|
+
|
|
35
|
+
this.parentWatchComponent = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
destroy() {
|
|
39
|
+
if (this.reactiveComponents && this.reactiveComponents.size > 0) {
|
|
40
|
+
this.reactiveComponents.forEach(component => component.destroy());
|
|
41
|
+
this.reactiveComponents.clear();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
resetScanIndex() {
|
|
46
|
+
this.reactiveComponentScanIndex = 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
clearForRefresh() {
|
|
50
|
+
if (this.reactiveComponents && this.reactiveComponents.size > 0) {
|
|
51
|
+
this.reactiveComponents.forEach(component => {
|
|
52
|
+
component.destroy();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
this.reactiveComponentIDs = [];
|
|
56
|
+
this.reactiveComponentScanIndex = 0;
|
|
57
|
+
|
|
58
|
+
// Reset các mảng following
|
|
59
|
+
this.followingIDs = [];
|
|
60
|
+
this.followingRenderIDs = [];
|
|
61
|
+
this.followingPrerenderIDs = [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
renderOutputComponent(stateKeys = [], renderBlock = () => '') {
|
|
65
|
+
if (!Array.isArray(stateKeys) || stateKeys.length === 0) {
|
|
66
|
+
return typeof renderBlock === 'function' ? renderBlock() : '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof renderBlock !== 'function') {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.controller.isVirtualRendering) {
|
|
74
|
+
this.controller.isVirtualRendering = false;
|
|
75
|
+
let result = this.renderOutputComponentScan(stateKeys, renderBlock);
|
|
76
|
+
this.controller.isVirtualRendering = true;
|
|
77
|
+
if (result !== false) {
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const rc = new ReactiveComponent({
|
|
83
|
+
stateKeys,
|
|
84
|
+
renderBlock,
|
|
85
|
+
controller: this.controller,
|
|
86
|
+
App: this.controller.App,
|
|
87
|
+
type: 'output',
|
|
88
|
+
escapeHTML: false
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
this.reactiveComponentIDs.push(rc.id);
|
|
92
|
+
this.reactiveComponents.set(rc.id, rc);
|
|
93
|
+
return rc.render();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
renderOutputEscapedComponent(stateKeys = [], renderBlock = () => '') {
|
|
97
|
+
if (!Array.isArray(stateKeys) || stateKeys.length === 0) {
|
|
98
|
+
return escapeString(typeof renderBlock === 'function' ? renderBlock() : '');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof renderBlock !== 'function') {
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this.controller.isVirtualRendering) {
|
|
106
|
+
this.controller.isVirtualRendering = false;
|
|
107
|
+
let result = this.renderOutputComponentScan(stateKeys, renderBlock, true);
|
|
108
|
+
this.controller.isVirtualRendering = true;
|
|
109
|
+
if (result !== false) {
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const rc = new ReactiveComponent({
|
|
115
|
+
stateKeys,
|
|
116
|
+
renderBlock,
|
|
117
|
+
controller: this.controller,
|
|
118
|
+
App: this.controller.App,
|
|
119
|
+
type: 'output',
|
|
120
|
+
escapeHTML: true
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.reactiveComponentIDs.push(rc.id);
|
|
124
|
+
this.reactiveComponents.set(rc.id, rc);
|
|
125
|
+
return rc.render();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
renderOutputComponentScan(stateKeys = [], renderBlock = () => '', escapeHTML = false) {
|
|
129
|
+
let reactiveComponentIndex = this.reactiveComponentScanIndex;
|
|
130
|
+
let reactiveComponentConfig = this.reactiveComponentConfig[reactiveComponentIndex];
|
|
131
|
+
|
|
132
|
+
if (!reactiveComponentConfig || !reactiveComponentConfig.stateKeys ||
|
|
133
|
+
reactiveComponentConfig.stateKeys.length != stateKeys.length ||
|
|
134
|
+
!reactiveComponentConfig.stateKeys.every(value => stateKeys.includes(value))) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { id: renderID } = reactiveComponentConfig;
|
|
139
|
+
const rc = new ReactiveComponent({
|
|
140
|
+
renderID,
|
|
141
|
+
stateKeys,
|
|
142
|
+
renderBlock,
|
|
143
|
+
controller: this.controller,
|
|
144
|
+
App: this.controller.App,
|
|
145
|
+
type: 'output',
|
|
146
|
+
escapeHTML
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
this.reactiveComponentIDs.push(rc.id);
|
|
150
|
+
this.reactiveComponents.set(rc.id, rc);
|
|
151
|
+
this.reactiveComponentScanIndex++;
|
|
152
|
+
return rc.render();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
renderWatchComponent(watchID, stateKeys = [], renderBlock = () => '') {
|
|
156
|
+
if (!Array.isArray(stateKeys) || stateKeys.length === 0) {
|
|
157
|
+
return typeof renderBlock === 'function' ? renderBlock() : '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof renderBlock !== 'function') {
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (this.reactiveComponents.has(watchID)) {
|
|
165
|
+
this.reactiveComponents.get(watchID).renderBlock = renderBlock;
|
|
166
|
+
return this.reactiveComponents.get(watchID).render();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const parentWatchComponent = this.parentWatchComponent;
|
|
170
|
+
const originChildrenIDs = this.controller._hierarchyManager.scanChildrenIDs;
|
|
171
|
+
this.controller._hierarchyManager.scanChildrenIDs = [];
|
|
172
|
+
|
|
173
|
+
const rc = new ReactiveComponent({
|
|
174
|
+
stateKeys,
|
|
175
|
+
renderBlock,
|
|
176
|
+
controller: this.controller,
|
|
177
|
+
App: this.controller.App,
|
|
178
|
+
parentWatchComponent,
|
|
179
|
+
renderID: watchID,
|
|
180
|
+
type: 'watch',
|
|
181
|
+
escapeHTML: false
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
this.parentWatchComponent = rc;
|
|
185
|
+
|
|
186
|
+
if (!this.reactiveComponentIDs.includes(rc.id)) {
|
|
187
|
+
this.reactiveComponentIDs.push(rc.id);
|
|
188
|
+
}
|
|
189
|
+
this.reactiveComponents.set(rc.id, rc);
|
|
190
|
+
|
|
191
|
+
let result = rc.render();
|
|
192
|
+
const newChildrenIDs = this.controller._hierarchyManager.scanChildrenIDs;
|
|
193
|
+
this.controller._hierarchyManager.scanChildrenIDs = originChildrenIDs;
|
|
194
|
+
rc.childrenIDs = newChildrenIDs;
|
|
195
|
+
this.parentWatchComponent = parentWatchComponent;
|
|
196
|
+
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
renderBindingAttribute(attrs = {}) {
|
|
201
|
+
if (typeof attrs !== 'object' || attrs === null) {
|
|
202
|
+
return '';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let bindingID = null;
|
|
206
|
+
const bindingManager = this.controller._bindingManager;
|
|
207
|
+
|
|
208
|
+
if (this.controller.isVirtualRendering) {
|
|
209
|
+
const attributeIndex = bindingManager.attributeIndex++;
|
|
210
|
+
const attributeConfig = bindingManager.attributeConfigs[attributeIndex];
|
|
211
|
+
if (!attributeConfig) {
|
|
212
|
+
return '';
|
|
213
|
+
}
|
|
214
|
+
const { id, config } = attributeConfig;
|
|
215
|
+
bindingID = id;
|
|
216
|
+
} else {
|
|
217
|
+
bindingID = uniqId();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const id = bindingID;
|
|
221
|
+
const stateKeys = [];
|
|
222
|
+
const element = null;
|
|
223
|
+
const newAttrs = {};
|
|
224
|
+
let attrStrValues = '';
|
|
225
|
+
const specialKeys = ['#text', '#html', '#children', '#content'];
|
|
226
|
+
|
|
227
|
+
Object.entries(attrs).forEach(([attrKey, attrConfig]) => {
|
|
228
|
+
const { states: attrStateKeys, render } = attrConfig;
|
|
229
|
+
if (!Array.isArray(attrStateKeys) || attrStateKeys.length === 0 || typeof render !== 'function') {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
attrStateKeys.forEach(stateKey => {
|
|
234
|
+
if (!stateKeys.includes(stateKey)) {
|
|
235
|
+
stateKeys.push(stateKey);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
newAttrs[attrKey] = { states: attrStateKeys, render };
|
|
240
|
+
|
|
241
|
+
if (specialKeys.includes(attrKey)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const renderResult = render();
|
|
247
|
+
if (renderResult === null || renderResult === undefined) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if(['checked', 'selected', 'disabled', 'readonly', 'multiple'].includes(attrKey)) {
|
|
251
|
+
if (renderResult && !['false', '0', 'undefined'].includes(String(renderResult).toLowerCase())) {
|
|
252
|
+
attrStrValues += ` ${attrKey}="${attrKey}"`;
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
attrStrValues += ` ${attrKey}="${escapeString(renderResult)}"`;
|
|
257
|
+
} catch (err) {
|
|
258
|
+
logger.error(`❌ ReactiveManager.renderBindingAttribute: Error rendering attribute ${attrKey}`, err);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!hasData(newAttrs)) {
|
|
263
|
+
return '';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
bindingManager.attributeListeners.push({ id, stateKeys, attrs: newAttrs, element });
|
|
267
|
+
return ` data-one-attribute-id="${id}"` + attrStrValues;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
renderClassBinding(classes = {}) {
|
|
271
|
+
if (typeof classes !== 'object' || classes === null) {
|
|
272
|
+
return '';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const id = uniqId();
|
|
276
|
+
const stateKeys = [];
|
|
277
|
+
const element = null;
|
|
278
|
+
const config = {};
|
|
279
|
+
let classStrValues = '';
|
|
280
|
+
|
|
281
|
+
classes.forEach((classConfig) => {
|
|
282
|
+
const { type = 'static', value, states: classStateKeys = [], checker } = classConfig;
|
|
283
|
+
|
|
284
|
+
if (type === 'static') {
|
|
285
|
+
classStrValues += ` ${value}`;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!Array.isArray(classStateKeys) || classStateKeys.length === 0 || typeof checker !== 'function') {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (checker()) {
|
|
294
|
+
classStrValues += ` ${value}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
classStateKeys.forEach(stateKey => {
|
|
298
|
+
if (!stateKeys.includes(stateKey)) {
|
|
299
|
+
stateKeys.push(stateKey);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
config[value] = { states: classStateKeys, checker };
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
let initialiClass = classStrValues.trim();
|
|
307
|
+
if (initialiClass === '') {
|
|
308
|
+
initialiClass = null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this.controller._bindingManager.classBindingConfigs.push({ id, states: stateKeys, config, initialiClass, element });
|
|
312
|
+
return ` data-one-class-id="${id}" class="${classStrValues.trim()}"`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
onWatchComponentUpdating() {
|
|
316
|
+
this.controller._eventManager.stopEventListener();
|
|
317
|
+
this.controller._bindingManager.stopBindingEventListener();
|
|
318
|
+
setTimeout(() => {
|
|
319
|
+
if (!this.controller.eventListenerStatus) {
|
|
320
|
+
this.controller._eventManager.startEventListener();
|
|
321
|
+
}
|
|
322
|
+
// Khởi động lại binding cho các DOM element mới
|
|
323
|
+
this.controller._bindingManager.startBindingEventListener();
|
|
324
|
+
}, 10);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
onWatchComponentUpdated() {
|
|
328
|
+
if (!this.controller.eventListenerStatus) {
|
|
329
|
+
this.controller._eventManager.startEventListener();
|
|
330
|
+
}
|
|
331
|
+
// Đảm bảo binding listeners được gắn vào các element mới
|
|
332
|
+
this.controller._bindingManager.startBindingEventListener();
|
|
333
|
+
}
|
|
334
|
+
}
|