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,592 @@
|
|
|
1
|
+
import { uniqId } from "../../helpers/utils.js";
|
|
2
|
+
import OneMarkup, { OneMarkupModel } from "../OneMarkup.js";
|
|
3
|
+
import OneDOM from "../OneDOM.js";
|
|
4
|
+
import { ViewState } from "../ViewState.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ReactiveComponent - Reactive component thống nhất
|
|
8
|
+
* Thay thế cả WatchComponent và OutputComponent để hiệu suất và bảo trì tốt hơn
|
|
9
|
+
*
|
|
10
|
+
* @class ReactiveComponent
|
|
11
|
+
* @description Xử lý reactive rendering cho cả watch blocks và output expressions
|
|
12
|
+
*
|
|
13
|
+
* Tính năng:
|
|
14
|
+
* - Quản lý lifecycle thống nhất
|
|
15
|
+
* - Tối ưu state subscriptions
|
|
16
|
+
* - Xử lý children view tốt hơn
|
|
17
|
+
* - Hỗ trợ escape HTML cho loại output
|
|
18
|
+
* - Xử lý lỗi cải thiện
|
|
19
|
+
* - Ngăn memory leak
|
|
20
|
+
*/
|
|
21
|
+
export class ReactiveComponent {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
* @param {Application} options.App - Instance Application
|
|
25
|
+
* @param {ViewController} options.controller - View controller
|
|
26
|
+
* @param {Array<string>} options.stateKeys - Các state keys để theo dõi
|
|
27
|
+
* @param {Function} options.renderBlock - Hàm render
|
|
28
|
+
* @param {string} options.renderID - Component ID
|
|
29
|
+
* @param {ReactiveComponent} options.parentWatchComponent - Component cha
|
|
30
|
+
* @param {string} options.type - Loại component: 'watch' hoặc 'output'
|
|
31
|
+
* @param {boolean} options.escapeHTML - Escape HTML cho output
|
|
32
|
+
*/
|
|
33
|
+
constructor({
|
|
34
|
+
App,
|
|
35
|
+
controller,
|
|
36
|
+
stateKeys = [],
|
|
37
|
+
renderBlock = () => '',
|
|
38
|
+
renderID = '',
|
|
39
|
+
parentWatchComponent = null,
|
|
40
|
+
type = 'watch',
|
|
41
|
+
escapeHTML = false
|
|
42
|
+
}) {
|
|
43
|
+
/**
|
|
44
|
+
* @type {Application}
|
|
45
|
+
*/
|
|
46
|
+
this.App = App;
|
|
47
|
+
/**
|
|
48
|
+
* @type {ViewController}
|
|
49
|
+
*/
|
|
50
|
+
this.controller = controller;
|
|
51
|
+
/**
|
|
52
|
+
* @type {ViewState}
|
|
53
|
+
*/
|
|
54
|
+
this.states = controller.states;
|
|
55
|
+
/**
|
|
56
|
+
* @type {Array<string>}
|
|
57
|
+
*/
|
|
58
|
+
this.stateKeys = stateKeys;
|
|
59
|
+
/**
|
|
60
|
+
* @type {Function}
|
|
61
|
+
*/
|
|
62
|
+
this.renderBlock = renderBlock;
|
|
63
|
+
/**
|
|
64
|
+
* @type {string}
|
|
65
|
+
*/
|
|
66
|
+
this.id = renderID || uniqId();
|
|
67
|
+
/**
|
|
68
|
+
* @type {string} - 'watch' hoặc 'output'
|
|
69
|
+
*/
|
|
70
|
+
this.type = type;
|
|
71
|
+
/**
|
|
72
|
+
* @type {boolean}
|
|
73
|
+
*/
|
|
74
|
+
this.escapeHTML = escapeHTML;
|
|
75
|
+
|
|
76
|
+
// Các cờ lifecycle
|
|
77
|
+
this.isMounted = false;
|
|
78
|
+
this.isScanned = false;
|
|
79
|
+
this.isDestroyed = false;
|
|
80
|
+
this._isUpdating = false;
|
|
81
|
+
|
|
82
|
+
// Tham chiếu DOM
|
|
83
|
+
this.openTag = null;
|
|
84
|
+
this.closeTag = null;
|
|
85
|
+
this.refElements = [];
|
|
86
|
+
/**
|
|
87
|
+
* @type {OneMarkupModel|null}
|
|
88
|
+
*/
|
|
89
|
+
this.markup = null;
|
|
90
|
+
|
|
91
|
+
// Quản lý state
|
|
92
|
+
this.subscribes = [];
|
|
93
|
+
this.renderedContent = '';
|
|
94
|
+
|
|
95
|
+
// Cấp bậc
|
|
96
|
+
this.parentWatchComponent = parentWatchComponent;
|
|
97
|
+
this.childrenIDs = [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Mount component và thiết lập state subscriptions
|
|
102
|
+
*/
|
|
103
|
+
mounted() {
|
|
104
|
+
if (this.isMounted || this.isDestroyed) return;
|
|
105
|
+
|
|
106
|
+
// console.log(`ReactiveComponent mounted [${this.type}]:`, this.id);
|
|
107
|
+
|
|
108
|
+
this.scan();
|
|
109
|
+
|
|
110
|
+
// Đăng ký theo dõi state changes
|
|
111
|
+
if (this.stateKeys.length > 0) {
|
|
112
|
+
const unsubscribe = this.states.__.subscribe(this.stateKeys, () => {
|
|
113
|
+
if (!this._isUpdating && this.isMounted) {
|
|
114
|
+
// console.log(`ReactiveComponent state changed [${this.type}], updating:`, this.id);
|
|
115
|
+
this.update();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
this.subscribes.push(unsubscribe);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.isMounted = true;
|
|
122
|
+
|
|
123
|
+
// Mount children cho loại watch
|
|
124
|
+
if (this.type === 'watch') {
|
|
125
|
+
this._mountChildren();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Unmount component và dọn dẹp subscriptions
|
|
131
|
+
*/
|
|
132
|
+
unmounted() {
|
|
133
|
+
if (!this.isMounted || this.isDestroyed) return;
|
|
134
|
+
|
|
135
|
+
// console.log(`ReactiveComponent unmounted [${this.type}]:`, this.id);
|
|
136
|
+
|
|
137
|
+
this.isMounted = false;
|
|
138
|
+
|
|
139
|
+
// Unmount children cho loại watch
|
|
140
|
+
if (this.type === 'watch') {
|
|
141
|
+
this._unmountChildren();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this._unsubscribeAll();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Cập nhật component khi state thay đổi
|
|
149
|
+
*/
|
|
150
|
+
update() {
|
|
151
|
+
if (this._isUpdating || !this.isMounted || this.isDestroyed) return;
|
|
152
|
+
|
|
153
|
+
this._isUpdating = true;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
this.controller._reactiveManager?.onWatchComponentUpdating?.();
|
|
157
|
+
|
|
158
|
+
if (this.type === 'watch') {
|
|
159
|
+
// Cập nhật đầy đủ cho watch components với quản lý children
|
|
160
|
+
this._updateWatchComponent();
|
|
161
|
+
} else {
|
|
162
|
+
// Cập nhật nhanh cho output components (không có children)
|
|
163
|
+
this._updateOutputComponent();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.controller._reactiveManager?.onWatchComponentUpdated?.();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(`ReactiveComponent update error [${this.type}][${this.id}]:`, error);
|
|
169
|
+
} finally {
|
|
170
|
+
this._isUpdating = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Cập nhật watch component với quản lý children
|
|
176
|
+
* @private
|
|
177
|
+
*/
|
|
178
|
+
_updateWatchComponent() {
|
|
179
|
+
// Lưu context gốc
|
|
180
|
+
const originalChildrenIDs = this.controller._hierarchyManager?.renewChildrenIDs || [];
|
|
181
|
+
const originRCChildrenIDs = this.controller._hierarchyManager?.rcChildrenIDs || [];
|
|
182
|
+
|
|
183
|
+
if (this.controller._hierarchyManager) {
|
|
184
|
+
this.controller._hierarchyManager.renewChildrenIDs = this.childrenIDs;
|
|
185
|
+
this.controller._hierarchyManager.rcChildrenIDs = [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// Unmount và xóa
|
|
190
|
+
this._unmountChildren();
|
|
191
|
+
this.clear();
|
|
192
|
+
|
|
193
|
+
// Render lại
|
|
194
|
+
if (this.closeTag?.parentNode) {
|
|
195
|
+
const newContent = this.renderContent();
|
|
196
|
+
OneDOM.before(this.closeTag, newContent);
|
|
197
|
+
|
|
198
|
+
// Cập nhật children IDs
|
|
199
|
+
if (this.controller._hierarchyManager) {
|
|
200
|
+
this.childrenIDs = this.controller._hierarchyManager.rcChildrenIDs.slice();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Re-scan and mount
|
|
204
|
+
this.scan();
|
|
205
|
+
this._mountChildren();
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
// Restore context
|
|
209
|
+
if (this.controller._hierarchyManager) {
|
|
210
|
+
this.controller._hierarchyManager.renewChildrenIDs = originalChildrenIDs;
|
|
211
|
+
this.controller._hierarchyManager.rcChildrenIDs = originRCChildrenIDs;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Quick update for output component (no children management)
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
_updateOutputComponent() {
|
|
221
|
+
this.clear();
|
|
222
|
+
|
|
223
|
+
if (this.closeTag?.parentNode) {
|
|
224
|
+
const newContent = this.renderContent();
|
|
225
|
+
|
|
226
|
+
// Output component uses text node for performance
|
|
227
|
+
const textNode = document.createTextNode(newContent);
|
|
228
|
+
OneDOM.before(this.closeTag, textNode);
|
|
229
|
+
|
|
230
|
+
this.scan();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Render content with error handling and escaping
|
|
236
|
+
*/
|
|
237
|
+
renderContent() {
|
|
238
|
+
try {
|
|
239
|
+
let content = this.renderBlock(this);
|
|
240
|
+
|
|
241
|
+
// Apply HTML escaping for output type
|
|
242
|
+
if (this.type === 'output' && this.escapeHTML) {
|
|
243
|
+
if (typeof content === 'string') {
|
|
244
|
+
content = this.App.View.escString(content);
|
|
245
|
+
} else if (content != null) {
|
|
246
|
+
content = this.App.View.escString(String(content));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.renderedContent = content ?? '';
|
|
251
|
+
return this.renderedContent;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error(`ReactiveComponent renderContent error [${this.type}][${this.id}]:`, error);
|
|
254
|
+
this.renderedContent = '';
|
|
255
|
+
return this.type === 'watch'
|
|
256
|
+
? `<!-- Error in ${this.type}: ${error.message} -->`
|
|
257
|
+
: '';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Tạo output render đầy đủ với comment markers
|
|
263
|
+
*/
|
|
264
|
+
render() {
|
|
265
|
+
const content = this.renderContent();
|
|
266
|
+
|
|
267
|
+
// Dùng tên tag khác nhau cho các loại khác nhau
|
|
268
|
+
if (this.type === 'output') {
|
|
269
|
+
return `<!-- [one:reactive-out id="${this.id}"] -->${content}<!-- [/one:reactive-out] -->`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return `<!-- [one:reactive id="${this.id}"] -->${content}<!-- [/one:reactive] -->`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Quét và cache các tham chiếu DOM giữa comment markers
|
|
277
|
+
*
|
|
278
|
+
* v2.0.0 - Cải thiện validation:
|
|
279
|
+
* - Kiểm tra kết nối markup trước khi quét
|
|
280
|
+
* - Validate sự tồn tại của openTag/closeTag
|
|
281
|
+
* - Xử lý lỗi và logging tốt hơn
|
|
282
|
+
*/
|
|
283
|
+
scan() {
|
|
284
|
+
if (this.isScanned || this.isDestroyed) return;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
if (!this.markup) {
|
|
288
|
+
// Thử reactive markup mới trước
|
|
289
|
+
const tagName = this.type === 'output' ? 'reactive-out' : 'reactive';
|
|
290
|
+
this.markup = OneMarkup.first(tagName, { id: this.id }, { useCache: false });
|
|
291
|
+
|
|
292
|
+
// Fallback về legacy markup để tương thích ngược
|
|
293
|
+
if (!this.markup) {
|
|
294
|
+
const legacyTag = this.type === 'output' ? 'output' : 'watch';
|
|
295
|
+
this.markup = OneMarkup.first(legacyTag, { id: this.id }, { useCache: false });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!this.markup) {
|
|
299
|
+
this.isScanned = true;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Validate markup có các tags cần thiết
|
|
304
|
+
if (!this.markup.openTag || !this.markup.closeTag) {
|
|
305
|
+
console.warn(`ReactiveComponent [${this.type}][${this.id}]: Markup missing tags`);
|
|
306
|
+
this.isScanned = true;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Validate các tags vẫn còn trong DOM
|
|
311
|
+
if (!this.markup.openTag.isConnected || !this.markup.closeTag.isConnected) {
|
|
312
|
+
console.warn(`ReactiveComponent [${this.type}][${this.id}]: Markup tags not connected`);
|
|
313
|
+
this.isScanned = true;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.openTag = this.markup.openTag;
|
|
318
|
+
this.closeTag = this.markup.closeTag;
|
|
319
|
+
this.refElements = this.markup.nodes.slice();
|
|
320
|
+
} else {
|
|
321
|
+
// Quét lại markup hiện tại - validate trước
|
|
322
|
+
if (!this.markup.openTag?.isConnected || !this.markup.closeTag?.isConnected) {
|
|
323
|
+
console.warn(`ReactiveComponent [${this.type}][${this.id}]: Cannot re-scan, markup disconnected`);
|
|
324
|
+
this.isScanned = true;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
this.refElements = [];
|
|
329
|
+
this.markup.__scan();
|
|
330
|
+
this.refElements = this.markup.nodes.slice();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.isScanned = true;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.error(`ReactiveComponent scan error [${this.type}][${this.id}]:`, error);
|
|
336
|
+
this.isScanned = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Xóa các DOM elements giữa markers
|
|
342
|
+
*
|
|
343
|
+
* v2.0.0 - Cải thiện dọn dẹp:
|
|
344
|
+
* - Validates kết nối node trước khi xóa
|
|
345
|
+
* - Xóa mảng sau khi xóa
|
|
346
|
+
* - Ngăn memory leaks từ các DOM refs bị bỏ rơi
|
|
347
|
+
*/
|
|
348
|
+
clear() {
|
|
349
|
+
this.isScanned = false;
|
|
350
|
+
|
|
351
|
+
// Remove all DOM nodes between markers
|
|
352
|
+
this.refElements.forEach(node => {
|
|
353
|
+
try {
|
|
354
|
+
// Only remove if node is still in DOM
|
|
355
|
+
if (node && node.parentNode && node.isConnected) {
|
|
356
|
+
node.parentNode.removeChild(node);
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error(`ReactiveComponent clear node error [${this.type}]:`, error);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Clear array to release references
|
|
364
|
+
this.refElements.length = 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Destroy component completely
|
|
369
|
+
*
|
|
370
|
+
* v2.0.0 - Enhanced memory leak prevention:
|
|
371
|
+
* - Proper OneMarkup disposal
|
|
372
|
+
* - Break circular references
|
|
373
|
+
* - Clear all arrays
|
|
374
|
+
* - Cleanup DOM references
|
|
375
|
+
*/
|
|
376
|
+
destroy() {
|
|
377
|
+
if (this.isDestroyed) return;
|
|
378
|
+
|
|
379
|
+
console.log(`ReactiveComponent destroyed [${this.type}]:`, this.id);
|
|
380
|
+
|
|
381
|
+
// Unmount first
|
|
382
|
+
this.unmounted();
|
|
383
|
+
|
|
384
|
+
// Clear DOM elements
|
|
385
|
+
this.clear();
|
|
386
|
+
|
|
387
|
+
// Dispose OneMarkup (clear internal cache)
|
|
388
|
+
if (this.markup && typeof this.markup.dispose === 'function') {
|
|
389
|
+
try {
|
|
390
|
+
this.markup.dispose();
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error(`ReactiveComponent markup disposal error [${this.type}]:`, error);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Clear all arrays to release references
|
|
397
|
+
this.refElements = [];
|
|
398
|
+
this.childrenIDs = [];
|
|
399
|
+
this.stateKeys = [];
|
|
400
|
+
|
|
401
|
+
// Break circular reference with parent
|
|
402
|
+
if (this.parentWatchComponent) {
|
|
403
|
+
// Remove this component from parent's children
|
|
404
|
+
if (this.parentWatchComponent.childrenIDs) {
|
|
405
|
+
const index = this.parentWatchComponent.childrenIDs.indexOf(this.id);
|
|
406
|
+
if (index > -1) {
|
|
407
|
+
this.parentWatchComponent.childrenIDs.splice(index, 1);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
this.parentWatchComponent = null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Set destroyed flag early to prevent operations
|
|
414
|
+
this.isDestroyed = true;
|
|
415
|
+
|
|
416
|
+
// Nullify all references for garbage collection
|
|
417
|
+
this.markup = null;
|
|
418
|
+
this.openTag = null;
|
|
419
|
+
this.closeTag = null;
|
|
420
|
+
this.renderBlock = null;
|
|
421
|
+
this.controller = null;
|
|
422
|
+
this.states = null;
|
|
423
|
+
this.App = null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Mount child view controllers (using ChildrenRegistry)
|
|
428
|
+
* @private
|
|
429
|
+
*/
|
|
430
|
+
_mountChildren() {
|
|
431
|
+
if (!this.controller?._childrenRegistry) return;
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
const registry = this.controller._childrenRegistry;
|
|
435
|
+
|
|
436
|
+
// Get children belonging to this reactive component from registry
|
|
437
|
+
const rcChildren = registry.getReactiveComponentChildren(this.id);
|
|
438
|
+
|
|
439
|
+
// Mount all children belonging to this reactive component
|
|
440
|
+
rcChildren.forEach(childNode => {
|
|
441
|
+
const childId = childNode.scope.id;
|
|
442
|
+
if (!registry.isMounted(childId)) {
|
|
443
|
+
registry.mount(childId);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Also mount children in childrenIDs (for backward compatibility)
|
|
448
|
+
if (this.childrenIDs.length > 0) {
|
|
449
|
+
this.childrenIDs.forEach(childId => {
|
|
450
|
+
if (!registry.isMounted(childId)) {
|
|
451
|
+
registry.mount(childId);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
console.error(`ReactiveComponent mount children error [${this.type}][${this.id}]:`, error);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Unmount child view controllers (using ChildrenRegistry)
|
|
462
|
+
* @private
|
|
463
|
+
*/
|
|
464
|
+
_unmountChildren() {
|
|
465
|
+
if (!this.controller?._childrenRegistry) return;
|
|
466
|
+
|
|
467
|
+
try {
|
|
468
|
+
const registry = this.controller._childrenRegistry;
|
|
469
|
+
|
|
470
|
+
// Get children belonging to this reactive component from registry
|
|
471
|
+
const rcChildren = registry.getReactiveComponentChildren(this.id);
|
|
472
|
+
|
|
473
|
+
// Unmount all children belonging to this reactive component
|
|
474
|
+
rcChildren.forEach(childNode => {
|
|
475
|
+
const childId = childNode.scope.id;
|
|
476
|
+
if (registry.isMounted(childId)) {
|
|
477
|
+
registry.unmount(childId);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Also unmount children in childrenIDs (for backward compatibility)
|
|
482
|
+
if (this.childrenIDs.length > 0) {
|
|
483
|
+
this.childrenIDs.forEach(childId => {
|
|
484
|
+
if (registry.isMounted(childId)) {
|
|
485
|
+
registry.unmount(childId);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
} catch (error) {
|
|
490
|
+
console.error(`ReactiveComponent unmount children error [${this.type}][${this.id}]:`, error);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Unsubscribe all state subscriptions
|
|
496
|
+
*
|
|
497
|
+
* v2.0.0 - Enhanced error handling:
|
|
498
|
+
* - Continues even if individual unsubscribe fails
|
|
499
|
+
* - Clears array after all attempts
|
|
500
|
+
* - Prevents partial cleanup
|
|
501
|
+
*
|
|
502
|
+
* @private
|
|
503
|
+
*/
|
|
504
|
+
_unsubscribeAll() {
|
|
505
|
+
const errors = [];
|
|
506
|
+
|
|
507
|
+
// Try to unsubscribe all, collecting errors
|
|
508
|
+
this.subscribes.forEach((unsubscribe, index) => {
|
|
509
|
+
try {
|
|
510
|
+
if (typeof unsubscribe === 'function') {
|
|
511
|
+
unsubscribe();
|
|
512
|
+
}
|
|
513
|
+
} catch (error) {
|
|
514
|
+
errors.push({ index, error });
|
|
515
|
+
console.error(`ReactiveComponent unsubscribe error [${this.type}] at index ${index}:`, error);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Always clear array, even if some failed
|
|
520
|
+
this.subscribes.length = 0;
|
|
521
|
+
|
|
522
|
+
// Log summary if errors occurred
|
|
523
|
+
if (errors.length > 0) {
|
|
524
|
+
console.warn(`ReactiveComponent [${this.type}][${this.id}]: ${errors.length} unsubscribe errors occurred`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Get current state values
|
|
530
|
+
* @returns {Object}
|
|
531
|
+
*/
|
|
532
|
+
getStateValues() {
|
|
533
|
+
if (!this.stateKeys.length) return {};
|
|
534
|
+
|
|
535
|
+
return this.stateKeys.reduce((acc, key) => {
|
|
536
|
+
acc[key] = this.states[key];
|
|
537
|
+
return acc;
|
|
538
|
+
}, {});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Check if component has state dependencies
|
|
543
|
+
* @returns {boolean}
|
|
544
|
+
*/
|
|
545
|
+
hasStateDependencies() {
|
|
546
|
+
return this.stateKeys.length > 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Force update without state change check
|
|
551
|
+
*/
|
|
552
|
+
forceUpdate() {
|
|
553
|
+
const wasUpdating = this._isUpdating;
|
|
554
|
+
this._isUpdating = false;
|
|
555
|
+
this.update();
|
|
556
|
+
this._isUpdating = wasUpdating;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Factory function for creating reactive components
|
|
562
|
+
* Used by compiler: __reactive(reactiveID, stateKeys, renderBlock, options)
|
|
563
|
+
*
|
|
564
|
+
* @param {ViewController} controller - View controller
|
|
565
|
+
* @param {string} reactiveID - Component ID
|
|
566
|
+
* @param {Array<string>} stateKeys - State keys to watch
|
|
567
|
+
* @param {Function} renderBlock - Render function
|
|
568
|
+
* @param {Object} options - Component options
|
|
569
|
+
* @returns {ReactiveComponent}
|
|
570
|
+
*/
|
|
571
|
+
export function __reactive(controller, reactiveID, stateKeys, renderBlock, options = {}) {
|
|
572
|
+
const {
|
|
573
|
+
type = 'output',
|
|
574
|
+
escapeHTML = false,
|
|
575
|
+
parentWatchComponent = null
|
|
576
|
+
} = options;
|
|
577
|
+
|
|
578
|
+
const component = new ReactiveComponent({
|
|
579
|
+
App: controller.App,
|
|
580
|
+
controller,
|
|
581
|
+
stateKeys,
|
|
582
|
+
renderBlock,
|
|
583
|
+
renderID: reactiveID,
|
|
584
|
+
type,
|
|
585
|
+
escapeHTML,
|
|
586
|
+
parentWatchComponent
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
return component;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export default ReactiveComponent;
|