onelaraveljs 1.21.0 → 1.21.2

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.
@@ -0,0 +1,501 @@
1
+ # 📋 Phân Tích Chi Tiết: Luồng & Logic Hoạt Động của mountView
2
+
3
+ ## 🎯 Tổng Quan
4
+
5
+ **mountView** là hàm chính dùng để **mount/render một view** vào DOM. Nó xử lý:
6
+ - Cache layout (tránh destroy layout không cần thiết)
7
+ - Destroy layout cũ nếu cần
8
+ - Render HTML mới
9
+ - Mount lifecycle hooks
10
+ - Scroll position handling
11
+
12
+ ---
13
+
14
+ ## 📊 Luồng Hoạt Động Chính
15
+
16
+ ### **PHASE 1: Lưu thông tin layout cũ (TRƯỚC loadView)**
17
+
18
+ ```javascript
19
+ const oldSuperViewPath = this.CURRENT_SUPER_VIEW_PATH;
20
+ const oldSuperView = this.CURRENT_SUPER_VIEW;
21
+ const oldPageView = this.PAGE_VIEW;
22
+
23
+ if (oldSuperView && oldSuperView instanceof ViewEngine) {
24
+ oldSuperView.__._lifecycleManager.unmounted();
25
+ }
26
+ ```
27
+
28
+ **Mục đích:**
29
+ - Lưu lại path của layout cũ trước khi `loadView()` thay đổi nó
30
+ - Gọi `unmounted()` cleanup CSS/scripts của layout cũ
31
+
32
+ **Tại sao cần?**
33
+ - Nếu không lưu, sẽ mất thông tin khi check cache ở bước 2
34
+ - `loadView()` sẽ reset `CURRENT_SUPER_VIEW` → không thể so sánh
35
+
36
+ ---
37
+
38
+ ### **PHASE 2: Gọi loadView() - Render view mới**
39
+
40
+ ```javascript
41
+ const viewResult = this.loadView(viewName, params, route?.$urlPath || '');
42
+ if (viewResult.error) {
43
+ console.error('View rendering error:', viewResult.error);
44
+ return;
45
+ }
46
+ ```
47
+
48
+ **Có 2 trường hợp:**
49
+
50
+ #### **2A. Normal Rendering (Client-Side)**
51
+ ```
52
+ loadView()
53
+
54
+ ├─ 1. Tạo view instance: view(name, data)
55
+ ├─ 2. Check view.__.hasSuperView (có layout không?)
56
+ ├─ 3. Nếu có → Tìm super view → Render layout
57
+ ├─ 4. Store trong PAGE_VIEW, CURRENT_SUPER_VIEW
58
+ └─ 5. Return {html, superView, ultraView, needInsert}
59
+ ```
60
+
61
+ #### **2B. SSR Scanning (Server-Side)**
62
+ ```
63
+ scanView()
64
+
65
+ ├─ 1. Lấy SSR data từ HTML comments
66
+ ├─ 2. Tạo view instances từ SSR data
67
+ ├─ 3. Call virtualRender() (chỉ setup relationships, không render HTML)
68
+ ├─ 4. Scan DOM + attach event handlers
69
+ └─ 5. Return {html từ SSR, superView, ultraView}
70
+ ```
71
+
72
+ ---
73
+
74
+ ### **PHASE 3: Kiểm tra Cache Layout**
75
+
76
+ ```javascript
77
+ const newSuperViewPath = viewResult.superView?.__.path;
78
+ const isSameLayout = newSuperViewPath === oldSuperViewPath;
79
+
80
+ if (!isSameLayout) {
81
+ if (oldSuperView && oldSuperView instanceof ViewEngine) {
82
+ oldSuperView.__._lifecycleManager.unmounted();
83
+ }
84
+ }
85
+ ```
86
+
87
+ **Logic:**
88
+ | Kịch bản | isSameLayout | Hành động |
89
+ |---------|-------------|----------|
90
+ | Home → User List | `true` | ❌ KHÔNG destroy layout → Reuse |
91
+ | Home (bảng) → Home (kảnh) | `true` | ❌ KHÔNG destroy layout → Reuse |
92
+ | Home (bảng) → Admin | `false` | ✅ Destroy layout cũ |
93
+ | Home → Login | `false` | ✅ Destroy layout cũ |
94
+
95
+ **Ưu điểm:**
96
+ - Tránh destroy/recreate layout không cần thiết
97
+ - CSS/JS chỉ load 1 lần nếu layout không đổi
98
+ - Performance tốt hơn
99
+
100
+ ---
101
+
102
+ ### **PHASE 4: Render HTML & Insert DOM**
103
+
104
+ ```javascript
105
+ if (viewResult.needInsert && viewResult.html) {
106
+ const container = this.container ||
107
+ document.querySelector('#app-root') ||
108
+ document.querySelector('#app') ||
109
+ document.body;
110
+ if (container) {
111
+ OneDOM.setHTML(container, html);
112
+ }
113
+ }
114
+ ```
115
+
116
+ **Trường hợp `needInsert = false`:**
117
+ - SSR mode: HTML từ server, DOM đã có → chỉ attach events
118
+ - Layout cache: Content view đổi, layout cũ → chỉ update content
119
+
120
+ **Trường hợp `needInsert = true`:**
121
+ - CSR mode: Render HTML từ client
122
+ - Layout khác: Phải render lại toàn bộ
123
+
124
+ ---
125
+
126
+ ### **PHASE 5: Emit & Mount Lifecycle**
127
+
128
+ ```javascript
129
+ if (this.emitChangedSections) {
130
+ this.emitChangedSections();
131
+ }
132
+
133
+ if (viewResult.ultraView && viewResult.ultraView instanceof ViewEngine) {
134
+ viewResult.ultraView.__._lifecycleManager.mounted();
135
+ }
136
+
137
+ this.CURRENT_SUPER_VIEW_MOUNTED = true;
138
+ this.PAGE_VIEW?.__.scrollToOldPosition() || this.scrollToTop();
139
+ ```
140
+
141
+ **Chi tiết:**
142
+
143
+ | Bước | Hành động | Mục đích |
144
+ |------|----------|---------|
145
+ | 1 | `emitChangedSections()` | Notify UI sections thay đổi |
146
+ | 2 | `mounted()` lifecycle | Chạy hook mounted của views |
147
+ | 3 | Set flag `CURRENT_SUPER_VIEW_MOUNTED = true` | Đánh dấu layout đã mount |
148
+ | 4 | `scrollToOldPosition()` hoặc `scrollToTop()` | Xử lý scroll position |
149
+
150
+ ---
151
+
152
+ ## 🔄 Luồng Tổng Quát (Visual)
153
+
154
+ ```
155
+ mountView(viewName)
156
+
157
+ ├─ PHASE 1: Lưu layout cũ
158
+ │ └─ oldSuperView, oldSuperViewPath
159
+
160
+ ├─ PHASE 2: loadView() → Render view mới
161
+ │ ├─ CSR: render() → HTML
162
+ │ └─ SSR: virtualRender() → relationships only
163
+ │ └─ Store: viewResult
164
+
165
+ ├─ PHASE 3: Cache check
166
+ │ ├─ isSameLayout = newPath === oldPath?
167
+ │ └─ Nếu khác → Destroy oldSuperView
168
+
169
+ ├─ PHASE 4: Insert DOM
170
+ │ └─ OneDOM.setHTML(container, html)
171
+
172
+ └─ PHASE 5: Mount lifecycle
173
+ ├─ emitChangedSections()
174
+ ├─ viewResult.ultraView.mounted()
175
+ ├─ Set CURRENT_SUPER_VIEW_MOUNTED = true
176
+ └─ Handle scroll position
177
+ ```
178
+
179
+ ---
180
+
181
+ ## 🎯 Hàm Liên Quan
182
+
183
+ ### **1. loadView(name, data, urlPath)**
184
+
185
+ **Input:** View name, data, URL path
186
+ **Output:** `{ html, superView, ultraView, needInsert, error }`
187
+
188
+ **Logic:**
189
+ ```
190
+ loadView()
191
+ ├─ Check cache: viewStoreKey
192
+ ├─ Nếu cached → return cached HTML
193
+ ├─ Nếu không:
194
+ │ ├─ Create view: this.view(name, data)
195
+ │ ├─ Check super view: view.__.hasSuperView?
196
+ │ ├─ Nếu có → renderOrScanView(superView, mode='csr')
197
+ │ ├─ Store PAGE_VIEW, CURRENT_SUPER_VIEW
198
+ │ └─ Return { html, superView, ultraView, needInsert }
199
+ └─ Catch error → Return { error }
200
+ ```
201
+
202
+ **Cache strategy:**
203
+ ```javascript
204
+ const viewStoreKey = name.replace('.', '_') + '_' + urlPath?.replace(/[\/\:]/g, '_');
205
+ const cachedPageView = this.cachedPageViews.get(viewStoreKey);
206
+ if (cachedPageView instanceof ViewEngine) {
207
+ return cachedHTML; // Reuse!
208
+ }
209
+ ```
210
+
211
+ ---
212
+
213
+ ### **2. scanView(name)**
214
+
215
+ **Input:** View name (SSR mode)
216
+ **Output:** `{ html, superView, ultraView, needInsert, error }`
217
+
218
+ **Logic:**
219
+ ```
220
+ scanView()
221
+ ├─ Get SSR data from server
222
+ ├─ this.ssrViewManager.scan(name)
223
+ ├─ Create view instance from SSR data
224
+ ├─ Loop through super views:
225
+ │ ├─ While view.__.hasSuperView:
226
+ │ │ ├─ Get super view path
227
+ │ │ ├─ Call scanRenderedView(view)
228
+ │ │ ├─ Scan SSR data for super view
229
+ │ │ └─ Call view.__.__scan(ssrData)
230
+ │ └─ Attach event handlers via DOM scan
231
+ ├─ Build ALL_VIEW_STACK (all views in hierarchy)
232
+ └─ Return { html, superView, ultraView, needInsert }
233
+ ```
234
+
235
+ **Vòng lặp super views:**
236
+ ```
237
+ PAGE_VIEW → LAYOUT1 → LAYOUT2 → ROOT_LAYOUT
238
+ ↑ ↓
239
+ └────────── ALL_VIEW_STACK ──────────┘
240
+ ```
241
+
242
+ ---
243
+
244
+ ### **3. renderOrScanView(view, variableData, mode)**
245
+
246
+ **Input:** View, data, mode ('csr' or 'ssr')
247
+ **Output:** Rendered HTML (CSR) or Nothing (SSR)
248
+
249
+ **Logic:**
250
+ ```
251
+ renderOrScanView()
252
+ ├─ Determine mode:
253
+ │ ├─ CSR → use render(), prerender()
254
+ │ └─ SSR → use virtualRender(), virtualPrerender()
255
+
256
+ ├─ CASE 1: No async data
257
+ │ └─ view.render() → return HTML
258
+
259
+ ├─ CASE 2: Has @await directive
260
+ │ ├─ CSR: Load data from current URL
261
+ │ │ ├─ getURIData() → fetch from API
262
+ │ │ ├─ Store in this.store
263
+ │ │ └─ view.refresh(data)
264
+ │ └─ SSR: Just setup relationships
265
+
266
+ └─ CASE 3: Has @fetch directive
267
+ ├─ CSR: Fetch data using config
268
+ │ ├─ this.App.Http.request(config)
269
+ │ └─ view.refresh(response.data)
270
+ └─ SSR: Just setup relationships
271
+ ```
272
+
273
+ ---
274
+
275
+ ### **4. clearOldRendering()**
276
+
277
+ **Mục đích:** Reset state giữa renders
278
+
279
+ **Logic:**
280
+ ```
281
+ clearOldRendering()
282
+ ├─ 1. Clear templates cache
283
+ │ └─ Object.keys(templates).forEach(clear)
284
+
285
+ ├─ 2. Clear cachedViews
286
+ │ └─ Delete old view instances
287
+
288
+ ├─ 3. Clear stacks (GỮ LẠI cache info!)
289
+ │ ├─ ALL_VIEW_STACK = []
290
+ │ ├─ SUPER_VIEW_STACK = []
291
+ │ ├─ PAGE_VIEW = null
292
+ │ ├─ ⚠️ KHÔNG xóa: CURRENT_SUPER_VIEW_PATH
293
+ │ └─ ⚠️ KHÔNG xóa: CURRENT_SUPER_VIEW
294
+
295
+ └─ 4. Clear orphaned event data
296
+ └─ Prevent memory leaks
297
+ ```
298
+
299
+ **Tại sao giữ cache info?**
300
+ - Để check layout cache ở `mountView()` PHASE 3
301
+ - Nếu xóa → sẽ luôn destroy layout (inefficient)
302
+
303
+ ---
304
+
305
+ ### **5. unmountView(view)**
306
+
307
+ **Mục đích:** Cleanup một view hoàn toàn
308
+
309
+ **Logic (5 bước):**
310
+ ```
311
+ unmountView(view)
312
+ ├─ 1. Call beforeUnmount() lifecycle
313
+ ├─ 2. Call removeEvents() - remove listeners
314
+ ├─ 3. Remove from viewMap
315
+ ├─ 4. Call unmounted() lifecycle
316
+ └─ 5. Call destroy() if defined
317
+ ```
318
+
319
+ **Khi nào gọi?**
320
+ - Layout thay đổi (PHASE 3)
321
+ - Navigation away from view
322
+ - Component cleanup
323
+
324
+ ---
325
+
326
+ ## 🔑 Key Concepts
327
+
328
+ ### **1. View Hierarchy (Super Views)**
329
+
330
+ ```
331
+ ROOT_LAYOUT (Master page)
332
+
333
+ ADMIN_LAYOUT (Admin section)
334
+
335
+ USER_LIST (Page view)
336
+ ```
337
+
338
+ **Scan order:** Bottom-up (USER_LIST → ADMIN_LAYOUT → ROOT_LAYOUT)
339
+ **Mount order:** Top-down (ROOT_LAYOUT → ADMIN_LAYOUT → USER_LIST)
340
+
341
+ ---
342
+
343
+ ### **2. Cache Strategy**
344
+
345
+ | Type | Key | TTL | Reuse |
346
+ |------|-----|-----|-------|
347
+ | Layout | `CURRENT_SUPER_VIEW_PATH` | Session | ✅ Reuse nếu giống |
348
+ | Page | `viewStoreKey` | 10 min | ✅ Reuse nếu không expired |
349
+ | Sections | `_sections[name]` | Session | ✅ Reuse |
350
+
351
+ ---
352
+
353
+ ### **3. CSR vs SSR Flow**
354
+
355
+ #### **CSR (Client-Side Rendering)**
356
+ ```
357
+ mountView()
358
+
359
+ loadView()
360
+ ├─ render() → HTML
361
+ └─ Return { html, needInsert: true }
362
+
363
+ OneDOM.setHTML(container, html)
364
+
365
+ mounted() lifecycle
366
+ ```
367
+
368
+ #### **SSR (Server-Side Rendering)**
369
+ ```
370
+ mountViewScan()
371
+
372
+ scanView()
373
+ ├─ Get SSR HTML from server
374
+ ├─ virtualRender() → setup relationships only
375
+ └─ Return { html, needInsert: false }
376
+
377
+ OneDOM.setHTML(container, html) [optional]
378
+
379
+ Scan DOM + attach events
380
+
381
+ mounted() lifecycle
382
+ ```
383
+
384
+ ---
385
+
386
+ ### **4. Data Flow: @await & @fetch**
387
+
388
+ #### **@await Directive**
389
+ ```
390
+ View có @await
391
+
392
+ renderOrScanView()
393
+ ├─ CSR:
394
+ │ ├─ Check this.store[apiDataKey]
395
+ │ ├─ Nếu cache hit: view.refresh(cachedData)
396
+ │ └─ Nếu miss: this.App.Api.getURIData() → fetch → refresh
397
+ └─ SSR:
398
+ └─ Skip (data từ server)
399
+ ```
400
+
401
+ #### **@fetch Directive**
402
+ ```
403
+ View có @fetch
404
+
405
+ renderOrScanView()
406
+ ├─ CSR:
407
+ │ ├─ Parse fetch config: { url, method, headers, ... }
408
+ │ ├─ this.App.Http.request(config)
409
+ │ └─ view.refresh(response.data)
410
+ └─ SSR:
411
+ └─ Skip (data từ server)
412
+ ```
413
+
414
+ ---
415
+
416
+ ## ⚡ Performance Optimizations
417
+
418
+ ### **1. Layout Cache**
419
+ ```javascript
420
+ // ❌ BAD: Destroy layout mỗi lần navigate
421
+ if (layoutPath !== oldPath) {
422
+ destroy layout;
423
+ }
424
+
425
+ // ✅ GOOD: Reuse layout nếu giống
426
+ if (newLayoutPath !== oldLayoutPath) {
427
+ destroy oldLayout;
428
+ }
429
+ ```
430
+
431
+ ### **2. Page Cache**
432
+ ```javascript
433
+ // Store page view đã render
434
+ this.cachedPageViews.set(viewStoreKey, viewInstance);
435
+
436
+ // Reuse nếu navigate lại
437
+ if (cachedPageView) {
438
+ return cachedView.html;
439
+ }
440
+ ```
441
+
442
+ ### **3. Event Cleanup**
443
+ ```javascript
444
+ // Avoid memory leaks
445
+ clearOldRendering() → clearOrphanedEventData();
446
+
447
+ // Clear only when needed
448
+ unmountView() → removeEvents();
449
+ ```
450
+
451
+ ### **4. Scroll Position**
452
+ ```javascript
453
+ // Remember old position
454
+ this.PAGE_VIEW?.__.scrollToOldPosition();
455
+
456
+ // Or go to top for new page
457
+ this.scrollToTop();
458
+ ```
459
+
460
+ ---
461
+
462
+ ## 🐛 Potential Issues & Solutions
463
+
464
+ ### **Issue 1: Layout cache mất khi refresh**
465
+ **Solution:** Store `CURRENT_SUPER_VIEW_PATH` persistent
466
+
467
+ ### **Issue 2: Memory leak từ event listeners**
468
+ **Solution:** `clearOrphanedEventData()` + `removeEvents()`
469
+
470
+ ### **Issue 3: Data stale trong @await**
471
+ **Solution:** Cache key includes `urlPath` → Cache per route
472
+
473
+ ### **Issue 4: SSR hydration mismatch**
474
+ **Solution:** `virtualRender()` setup relationships before scan
475
+
476
+ ---
477
+
478
+ ## 📝 Summary Table
479
+
480
+ | Hàm | Mục đích | Input | Output | Khi gọi |
481
+ |-----|----------|-------|--------|---------|
482
+ | **mountView** | Main mount logic | viewName, params | HTML inserted | Navigation |
483
+ | **loadView** | Render view (CSR) | name, data, urlPath | {html, superView} | mountView PHASE 2 |
484
+ | **scanView** | Scan SSR HTML | name | {html, views} | mountViewScan |
485
+ | **renderOrScanView** | Render/scan with async | view, data, mode | HTML or None | loadView, scanView |
486
+ | **clearOldRendering** | Reset state | - | - | Before render |
487
+ | **unmountView** | Cleanup view | view | boolean | On destroy |
488
+
489
+ ---
490
+
491
+ ## 🎓 Learning Points
492
+
493
+ 1. **Cache optimization:** Layout reuse nếu path giống
494
+ 2. **Lifecycle management:** unmounted → render → mounted
495
+ 3. **Super view hierarchy:** Scan từ page up to root
496
+ 4. **Async data handling:** @await vs @fetch directive
497
+ 5. **Memory management:** Clear orphaned events, remove listeners
498
+ 6. **CSR vs SSR:** Different rendering vs setup paths
499
+ 7. **Event delegation:** Scan DOM and attach handlers
500
+ 8. **Scroll position:** Restore position on back/forward
501
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onelaraveljs",
3
- "version": "1.21.0",
3
+ "version": "1.21.2",
4
4
  "description": "OneLaravel JS Framework Core & Compiler",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -363,10 +363,6 @@ export class ViewController {
363
363
  // Set basic properties (giữ nguyên tên từ code gốc)
364
364
  this.id = config.viewId || uniqId();
365
365
  deleteProp(config, 'viewId');
366
- this.addCSS = config.addCSS || this._emptyFn;
367
- deleteProp(config, 'addCSS');
368
- this.removeCSS = config.removeCSS || this._emptyFn;
369
- deleteProp(config, 'removeCSS');
370
366
  this.superViewPath = config.superViewPath || config.superView;
371
367
  deleteProp(config, 'superViewPath');
372
368
  deleteProp(config, 'superView');
@@ -1354,6 +1350,7 @@ export class ViewController {
1354
1350
  this.children.forEach(childCtrl => {
1355
1351
  if (childCtrl && childCtrl instanceof ViewController) {
1356
1352
  childCtrl.destroy();
1353
+ childCtrl._lifecycleManager.destroy();
1357
1354
  }
1358
1355
  });
1359
1356
  this.children = [];
@@ -389,8 +389,8 @@ export class ViewManager {
389
389
  const awaitData = null;
390
390
  this.PAGE_VIEW = null;
391
391
  try {
392
-
393
- const viewStoreKey = name.replace('.', '_') + '_' + urlPath?.replace(/[\/\:]/g, '_');
392
+
393
+ const viewStoreKey = name + '__' + urlPath + '__ciew';
394
394
  const cachedPageView = this.cachedPageViews.get(viewStoreKey);
395
395
  if (cachedPageView && cachedPageView instanceof ViewEngine) {
396
396
  // Sử dụng lại cached page view
@@ -398,7 +398,7 @@ export class ViewManager {
398
398
  this.PAGE_VIEW = cachedPageView;
399
399
  let ultraView = cachedPageView;
400
400
  let superView = null;
401
- if(cachedPageView.superView && cachedPageView.superView instanceof ViewEngine){
401
+ if (cachedPageView.superView && cachedPageView.superView instanceof ViewEngine) {
402
402
  superView = cachedPageView.superView;
403
403
  ultraView = superView;
404
404
  this.CURRENT_SUPER_VIEW_MOUNTED = true;
@@ -407,7 +407,7 @@ export class ViewManager {
407
407
  cachedPageView.__._templateManager.pushCachedSections();
408
408
  html = cachedLayoutPath === this.CURRENT_SUPER_VIEW_PATH ? superView.__.renderedHtml : this.renderView(superView);
409
409
  }
410
-
410
+ logger.debug(`🔍 App.View.loadView: Using cached view for '${name}' with URL path '${urlPath}'`);
411
411
  return {
412
412
  html: html,
413
413
  isSuperView: superView ? true : false,
@@ -421,7 +421,7 @@ export class ViewManager {
421
421
 
422
422
  let hasCache = false;
423
423
  if (this.cachedTimes > 0) {
424
- let cacheKey = name.replace('.', '_') + '_' + urlPath?.replace(/[\/\:]/g, '_');
424
+ let cacheKey = name + '__' + urlPath + '__ciew';
425
425
  const cachedData = this.storageService.get(cacheKey);
426
426
  if (cachedData) {
427
427
  data = { ...data, ...cachedData };
@@ -447,16 +447,16 @@ export class ViewManager {
447
447
  if (this.cachedTimes > 0) {
448
448
  if (this.PAGE_VIEW instanceof ViewEngine) {
449
449
  const oldCacheData = this.PAGE_VIEW.data;
450
- let cacheKey = this.PAGE_VIEW.path.replace('.', '_') + '_' + this.PAGE_VIEW.urlPath?.replace(/\//g, '_');
450
+ let cacheKey = this.PAGE_VIEW.path + '__' + this.PAGE_VIEW.urlPath + '__ciew';
451
451
  this.storageService.set(cacheKey, oldCacheData, 3600); // cache trong 1 giờ
452
452
  }
453
453
  }
454
454
 
455
455
  // Lưu view vào store để quản lý vòng đời (ttl mặc định 30 phút)
456
456
  this.store.set(viewStoreKey, view, this.storeTTL); // lưu view vào store
457
- this.store.onExpire(viewStoreKey, (view) => {
458
- if (view && view instanceof ViewEngine) {
459
- view.__._lifecycleManager.destroy();
457
+ this.store.onExpire(viewStoreKey, (viewStore) => {
458
+ if (viewStore && viewStore instanceof ViewEngine && viewStore != this.PAGE_VIEW) {
459
+ viewStore.__._lifecycleManager.destroy();
460
460
  }
461
461
  });
462
462
 
@@ -626,6 +626,13 @@ export class ViewManager {
626
626
  if (route && route.$urlPath) {
627
627
  view.__.urlPath = route.$urlPath;
628
628
  }
629
+ const viewStoreKey = name.replace('.', '_') + '_' + (route?.$urlPath || '').replace(/[\/\:]/g, '_');
630
+ this.store.set(viewStoreKey, view, this.storeTTL); // lưu view vào store
631
+ this.store.onExpire(viewStoreKey, (viewStore) => {
632
+ if (viewStore && viewStore instanceof ViewEngine && viewStore != this.PAGE_VIEW) {
633
+ viewStore.__._lifecycleManager.destroy();
634
+ }
635
+ });
629
636
 
630
637
 
631
638
  // Store view in array for tracking
@@ -795,8 +802,7 @@ export class ViewManager {
795
802
  oldSuperView.__._lifecycleManager.unmounted();
796
803
  // oldSuperView.__._lifecycleManager.destroyOriginalView();
797
804
  }
798
-
799
-
805
+
800
806
 
801
807
 
802
808
 
@@ -806,22 +812,20 @@ export class ViewManager {
806
812
  console.error('View rendering error:', viewResult.error);
807
813
  return;
808
814
  }
815
+ const newPageView = this.PAGE_VIEW, newSuperView = this.CURRENT_SUPER_VIEW;
809
816
 
810
817
  // ============================================================
811
818
  // STEP 2: Check cache layout - nếu giống thì KHÔNG destroy
812
819
  // ============================================================
813
- const newSuperViewPath = viewResult.superView?.__.path;
820
+ const newSuperViewPath = newSuperView?.__.path;
814
821
  const isSameLayout = newSuperViewPath === oldSuperViewPath;
815
822
 
816
- // Chỉ destroy nếu layout KHÁC
817
- if (!isSameLayout) {
818
- if (oldSuperView && oldSuperView instanceof ViewEngine) {
819
- // Call destroy() to remove CSS and scripts
820
- oldSuperView.__._lifecycleManager.unmounted();
821
- // oldSuperView.__._lifecycleManager.destroyOriginalView();
822
- }
823
-
823
+ // Chỉ unmount layout cũ nếu layout KHÁC
824
+ if (!isSameLayout && oldSuperView && oldSuperView instanceof ViewEngine) {
825
+ oldSuperView.__.removeStyles();
824
826
  }
827
+ oldPageView?.__.removeStyles();
828
+
825
829
 
826
830
  // ============================================================
827
831
  // STEP 3: Render và mount như bình thường
@@ -839,14 +843,21 @@ export class ViewManager {
839
843
  this.emitChangedSections();
840
844
  }
841
845
 
842
-
846
+ if (newPageView && newPageView instanceof ViewEngine) {
847
+ newPageView.__.insertStyles();
848
+ }
849
+ if (newSuperView && newSuperView instanceof ViewEngine) {
850
+ newSuperView.__.insertStyles();
851
+ }
852
+
853
+
843
854
  if (viewResult.ultraView && viewResult.ultraView instanceof ViewEngine) {
844
855
  viewResult.ultraView.__._lifecycleManager.mounted();
845
856
  }
846
857
 
847
858
  this.CURRENT_SUPER_VIEW_MOUNTED = true; // set trang thái super view mounted = true
848
859
 
849
- this.PAGE_VIEW? this.PAGE_VIEW.__.scrollToOldPosition() : this.scrollToTop();
860
+ this.PAGE_VIEW ? this.PAGE_VIEW.__.scrollToOldPosition() : this.scrollToTop();
850
861
 
851
862
  } catch (error) {
852
863
  console.error('Error rendering view:', error);
@@ -1351,7 +1362,7 @@ export class ViewManager {
1351
1362
  }
1352
1363
  result = view.__[renderMethod]();
1353
1364
  }
1354
- const apiDataKey = view.__.path.replace(/\//g, '_') + '_' + view.__.urlPath.replace(/[\/\?\=\&]/g, '_') + '_data';
1365
+ const apiDataKey = view.__.path + '_' + view.__.urlPath + ':data';
1355
1366
 
1356
1367
  // ====================================================================
1357
1368
  // CASE 3A: Has @await - Load data by current URL
@@ -1642,35 +1653,10 @@ export class ViewManager {
1642
1653
  }
1643
1654
 
1644
1655
  try {
1645
- logger.log(`🗑️ View.unmountView: Unmounting view ${view.id} (${view.path})`);
1646
-
1647
- // Step 1: Call beforeUnmount lifecycle
1648
- if (view.__ && typeof view.__._lifecycleManager.beforeUnmount === 'function') {
1649
- view.__._lifecycleManager.beforeUnmount();
1650
- }
1651
-
1652
- // Step 2: Remove event listeners
1653
- if (typeof view.__._lifecycleManager.removeEvents === 'function') {
1654
- view.__._lifecycleManager.removeEvents();
1655
- }
1656
1656
 
1657
- // Step 3: Remove from viewMap
1658
- if (this.viewMap.has(view.id)) {
1659
- this.viewMap.delete(view.id);
1660
- }
1661
-
1662
- // Step 4: Call unmounted lifecycle
1663
- if (view.__ && typeof view.__._lifecycleManager.unmounted === 'function') {
1664
- view.__._lifecycleManager.unmounted();
1665
- }
1666
-
1667
- // Step 5: Call destroy if defined
1668
- if (view.__ && typeof view.__._lifecycleManager.destroy === 'function') {
1669
- view.__._lifecycleManager.destroy();
1670
- }
1657
+ view.__._lifecycleManager.unmounted();
1658
+ view.__.removeStyles();
1671
1659
 
1672
- logger.log(`✅ View.unmountView: View ${view.id} unmounted successfully`);
1673
- return true;
1674
1660
 
1675
1661
  } catch (error) {
1676
1662
  logger.error(`❌ View.unmountView: Error unmounting view ${view.id}:`, error);
@@ -216,9 +216,6 @@ export class StateManager {
216
216
  }
217
217
 
218
218
  // Log để debug (có thể tắt trong production)
219
- if (this.controller?.App?.env?.debug) {
220
- logger.log(`[StateManager] Flushing ${changesToProcess.length} changes (flush #${this._flushCount}):`, changesToProcess);
221
- }
222
219
 
223
220
  // Reset cờ called của multi-key listeners
224
221
  for (const listener of this.multiKeyListeners) {
@@ -256,7 +256,6 @@ export class LifecycleManager {
256
256
  this.beforeUnmount();
257
257
  this.unmounting();
258
258
 
259
- // Remove scripts
260
259
  ctrl.removeScripts();
261
260
 
262
261
  // Stop event listeners
@@ -327,6 +326,7 @@ export class LifecycleManager {
327
326
 
328
327
  // Remove styles (will use fallback if styles array is empty)
329
328
  ctrl.removeStyles();
329
+ ctrl.removeScripts();
330
330
 
331
331
  // Final cleanup: Remove all styles with this view path from DOM
332
332
  // This ensures CSS is removed even if registry is out of sync
@@ -16,6 +16,12 @@ export class ResourceManager {
16
16
  constructor(controller) {
17
17
  this.controller = controller;
18
18
  this.path = controller.path;
19
+ /**
20
+ * Flag to track if styles/scripts have been inserted
21
+ * @type {boolean}
22
+ */
23
+ this.insertedStyles = false;
24
+ this.insertedScripts = false;
19
25
  }
20
26
 
21
27
  updateController(newController) {
@@ -23,6 +29,47 @@ export class ResourceManager {
23
29
  this.path = newController.path;
24
30
  }
25
31
 
32
+ /**
33
+ * Check if this view's styles have been inserted into DOM
34
+ * @returns {boolean}
35
+ */
36
+ hasInsertedStyles() {
37
+ return this.insertedStyles && this.controller.insertedResourceKeys && this.controller.insertedResourceKeys.size > 0;
38
+ }
39
+
40
+ /**
41
+ * Check if this view's scripts have been inserted into DOM
42
+ * @returns {boolean}
43
+ */
44
+ hasInsertedScripts() {
45
+ return this.insertedScripts;
46
+ }
47
+
48
+ /**
49
+ * Check if specific resource key has been inserted
50
+ * @param {string} resourceKey
51
+ * @returns {boolean}
52
+ */
53
+ hasInsertedResource(resourceKey) {
54
+ return this.controller.insertedResourceKeys && this.controller.insertedResourceKeys.has(resourceKey);
55
+ }
56
+
57
+ /**
58
+ * Get the number of inserted styles for this view
59
+ * @returns {number}
60
+ */
61
+ getInsertedStylesCount() {
62
+ if (!this.controller.insertedResourceKeys) return 0;
63
+ let count = 0;
64
+ this.controller.insertedResourceKeys.forEach(key => {
65
+ const entry = this.controller.App.View.Engine.resourceRegistry.get(key);
66
+ if (entry && entry.resourceType === 'style') {
67
+ count++;
68
+ }
69
+ });
70
+ return count;
71
+ }
72
+
26
73
  /**
27
74
  * Insert all resources (styles + scripts)
28
75
  */
@@ -42,12 +89,25 @@ export class ResourceManager {
42
89
  /**
43
90
  * Insert styles into DOM with reference counting
44
91
  * Extracted from ViewController.js line 428
92
+ * Will skip if already inserted to prevent duplicate execution
45
93
  */
46
94
  insertStyles() {
95
+ this.controller.children?.forEach(childCtrl => {
96
+ childCtrl._resourceManager.insertStyles();
97
+ });
98
+ // Skip if already inserted
99
+ if (this.insertedStyles) {
100
+ return;
101
+ }
102
+
47
103
  if (!this.controller.styles || this.controller.styles.length === 0) {
104
+ this.insertedStyles = true;
48
105
  return;
49
106
  }
50
107
 
108
+ let insertedCount = 0;
109
+ let skippedCount = 0;
110
+
51
111
  this.controller.styles.forEach(style => {
52
112
  const resourceKey = this.controller.App.View.Engine.getResourceKey({
53
113
  ...style,
@@ -63,8 +123,11 @@ export class ResourceManager {
63
123
  registryEntry.viewPaths.add(this.path);
64
124
  registryEntry.referenceCount++;
65
125
  this.controller.insertedResourceKeys.add(resourceKey);
126
+ skippedCount++;
66
127
  return; // Don't insert again
67
128
  }
129
+
130
+ insertedCount++;
68
131
 
69
132
  // Create and insert new style element
70
133
  let element;
@@ -131,18 +194,31 @@ export class ResourceManager {
131
194
  });
132
195
 
133
196
  this.controller.insertedResourceKeys.add(resourceKey);
197
+ logger.debug(` ✅ Style inserted (key: ${resourceKey}, type: ${style.type})`);
134
198
  });
199
+
200
+ this.insertedStyles = true;
135
201
  }
136
202
 
137
203
  /**
138
204
  * Insert scripts into DOM with reference counting
139
205
  * Extracted from ViewController.js line 523
206
+ * Will skip if already inserted to prevent duplicate execution
140
207
  */
141
208
  insertScripts() {
209
+ // Skip if already inserted
210
+ if (this.insertedScripts) {
211
+ return;
212
+ }
213
+
142
214
  if (!this.controller.scripts || this.controller.scripts.length === 0) {
215
+ this.insertedScripts = true;
143
216
  return;
144
217
  }
145
218
 
219
+ let insertedCount = 0;
220
+ let skippedCount = 0;
221
+
146
222
  this.controller.scripts.forEach(script => {
147
223
  const resourceKey = this.controller.App.View.Engine.getResourceKey({
148
224
  ...script,
@@ -158,6 +234,7 @@ export class ResourceManager {
158
234
  registryEntry.viewPaths.add(this.path);
159
235
  registryEntry.referenceCount++;
160
236
  this.controller.insertedResourceKeys.add(resourceKey);
237
+ skippedCount++;
161
238
 
162
239
  // If script already loaded, call onload callback (only for external scripts with element)
163
240
  if (script.onload && registryEntry.element && registryEntry.element.readyState === 'complete') {
@@ -176,6 +253,8 @@ export class ResourceManager {
176
253
  return; // Don't insert again
177
254
  }
178
255
 
256
+ insertedCount++;
257
+
179
258
  // Create and insert new script element
180
259
  let element;
181
260
 
@@ -293,19 +372,33 @@ export class ResourceManager {
293
372
  this.controller.insertedResourceKeys.add(resourceKey);
294
373
  }
295
374
  });
375
+
376
+ this.insertedScripts = true;
296
377
  }
297
378
 
298
379
  /**
299
380
  * Remove styles from registry with reference counting
300
381
  * Extracted from ViewController.js line 683
382
+ * Will skip if not inserted to prevent unnecessary operations
301
383
  */
302
384
  removeStyles() {
385
+ this.controller.children?.forEach(childCtrl => {
386
+ childCtrl._resourceManager.removeStyles();
387
+ });
388
+ // Skip if not inserted
389
+ if (!this.insertedStyles) {
390
+ return;
391
+ }
392
+
303
393
  if (!this.controller.styles || this.controller.styles.length === 0) {
304
394
  // Fallback: Remove all styles with this view path from DOM
305
395
  this.removeStylesByViewPath();
396
+ this.insertedStyles = false;
306
397
  return;
307
398
  }
308
399
 
400
+ let removedCount = 0;
401
+
309
402
  this.controller.styles.forEach(style => {
310
403
  const resourceKey = this.controller.App.View.Engine.getResourceKey({
311
404
  ...style,
@@ -324,10 +417,13 @@ export class ResourceManager {
324
417
  if (registryEntry.referenceCount <= 0) {
325
418
  if (registryEntry.element && registryEntry.element.parentNode) {
326
419
  registryEntry.element.remove();
420
+ removedCount++;
327
421
  }
328
422
 
329
423
  // Remove from registry
330
424
  this.controller.App.View.Engine.resourceRegistry.delete(resourceKey);
425
+ } else {
426
+ logger.debug(` ⏭️ Style still in use by other views (key: ${resourceKey}, refCount: ${registryEntry.referenceCount})`);
331
427
  }
332
428
 
333
429
  this.controller.insertedResourceKeys.delete(resourceKey);
@@ -337,6 +433,7 @@ export class ResourceManager {
337
433
  elements.forEach(element => {
338
434
  if (element.parentNode) {
339
435
  element.remove();
436
+ removedCount++;
340
437
  }
341
438
  });
342
439
  }
@@ -344,6 +441,8 @@ export class ResourceManager {
344
441
 
345
442
  // Final fallback: Remove any remaining styles with this view path
346
443
  this.removeStylesByViewPath();
444
+
445
+ this.insertedStyles = false;
347
446
  }
348
447
 
349
448
  /**
@@ -402,12 +501,24 @@ export class ResourceManager {
402
501
  /**
403
502
  * Remove scripts from registry with reference counting
404
503
  * Extracted from ViewController.js line 790
504
+ * Will skip if not inserted to prevent unnecessary operations
405
505
  */
406
506
  removeScripts() {
507
+ // Skip if not inserted
508
+ this.controller.children?.forEach(childCtrl => {
509
+ childCtrl._resourceManager.removeScripts();
510
+ });
511
+ if (!this.insertedScripts) {
512
+ return;
513
+ }
514
+
407
515
  if (!this.controller.scripts || this.controller.scripts.length === 0) {
516
+ this.insertedScripts = false;
408
517
  return;
409
518
  }
410
519
 
520
+ let removedCount = 0;
521
+
411
522
  this.controller.scripts.forEach(script => {
412
523
  const resourceKey = this.controller.App.View.Engine.getResourceKey({
413
524
  ...script,
@@ -428,14 +539,19 @@ export class ResourceManager {
428
539
  // Just remove from registry
429
540
  if (registryEntry.element && registryEntry.element.parentNode) {
430
541
  registryEntry.element.remove();
542
+ removedCount++;
431
543
  }
432
544
 
433
545
  // Remove from registry
434
546
  this.controller.App.View.Engine.resourceRegistry.delete(resourceKey);
547
+ } else {
548
+ logger.debug(` ⏭️ Script still in use by other views (key: ${resourceKey}, refCount: ${registryEntry.referenceCount})`);
435
549
  }
436
550
 
437
551
  this.controller.insertedResourceKeys.delete(resourceKey);
438
552
  }
439
553
  });
554
+
555
+ this.insertedScripts = false;
440
556
  }
441
557
  }