ezfw-core 1.0.79 → 1.0.82

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.
@@ -1094,6 +1094,11 @@ export class EzBaseComponent {
1094
1094
  }
1095
1095
 
1096
1096
  destroy(): void {
1097
+ // Call controller onDestroy if exists
1098
+ if (this.controller && typeof (this.controller as EzController & { onDestroy?: () => void }).onDestroy === 'function') {
1099
+ (this.controller as EzController & { onDestroy?: () => void }).onDestroy!();
1100
+ }
1101
+
1097
1102
  // Destroy children first
1098
1103
  if (this._children) {
1099
1104
  for (const child of this._children) {
package/core/renderer.ts CHANGED
@@ -421,30 +421,35 @@ export class EzRenderer {
421
421
  }
422
422
  }
423
423
 
424
- async executeOnInit(config: ComponentConfig, controllerName: string | null): Promise<void> {
425
- let controller = this.ez._controllers[controllerName || ''];
424
+ async executeOnInit(instance: EzBaseComponent | null, config: ComponentConfig): Promise<void> {
425
+ // Prefer instance's config and controller when available
426
+ const effectiveConfig = (instance?.config || config) as ComponentConfig;
426
427
 
427
- if (!controller && config.controller) {
428
- const ctrlName = typeof config.controller === 'string'
429
- ? config.controller
428
+ let controller: ControllerInstance | null = null;
429
+
430
+ if (instance?.controller) {
431
+ controller = instance.controller as unknown as ControllerInstance;
432
+ } else if (effectiveConfig.controller) {
433
+ const ctrlName = typeof effectiveConfig.controller === 'string'
434
+ ? effectiveConfig.controller
430
435
  : null;
431
436
  if (ctrlName) {
432
- controller = this.ez._controllers[ctrlName]
433
- || this.ez._controllers[`${ctrlName}Controller`];
434
- } else if (typeof config.controller === 'object') {
435
- controller = config.controller as ControllerInstance;
437
+ controller = this.ez._controllers[ctrlName] ||
438
+ this.ez._controllers[`${ctrlName}Controller`];
439
+ } else if (typeof effectiveConfig.controller === 'object') {
440
+ controller = effectiveConfig.controller as ControllerInstance;
436
441
  }
437
442
  }
438
443
 
439
444
  if (!controller) return;
440
445
 
441
- let fn: unknown = config.init;
446
+ let fn: unknown = effectiveConfig.init;
442
447
  if (typeof fn === 'string') {
443
448
  fn = controller[fn];
444
449
  }
445
450
 
446
451
  if (typeof fn === 'function') {
447
- await fn.call(controller, config);
452
+ await fn.call(controller, instance);
448
453
  }
449
454
  }
450
455
 
@@ -1051,7 +1056,7 @@ export class EzRenderer {
1051
1056
 
1052
1057
  if (config.init && config.skipInit !== true) {
1053
1058
  queueMicrotask(() => {
1054
- this.executeOnInit(config, controllerName)
1059
+ this.executeOnInit(instance || null, config)
1055
1060
  .catch(err => this.ez.handleFrameworkError(err));
1056
1061
  });
1057
1062
  }
@@ -11,6 +11,7 @@ export interface RenderContext {
11
11
  islands: IslandData[];
12
12
  props?: Record<string, unknown>;
13
13
  serverData?: Record<string, unknown>;
14
+ insideIsland?: boolean;
14
15
  }
15
16
  export interface IslandData {
16
17
  id: string;
@@ -108,7 +109,8 @@ export declare class StaticHtmlRenderer {
108
109
  */
109
110
  private serializableProps;
110
111
  /**
111
- * Reset island counter (for testing)
112
+ * Reset island counter and clear analyzer cache
113
+ * Called before each SSR render to ensure fresh analysis
112
114
  */
113
115
  resetIslandCounter(): void;
114
116
  }
@@ -36,7 +36,6 @@ let islandIdCounter = 0;
36
36
  * Main static renderer class
37
37
  */
38
38
  export class StaticHtmlRenderer {
39
- analyzer;
40
39
  constructor() {
41
40
  this.analyzer = analyzer;
42
41
  }
@@ -76,6 +75,11 @@ export class StaticHtmlRenderer {
76
75
  // Try to render as native HTML element
77
76
  return this.renderNativeElement(name, props, ctx);
78
77
  }
78
+ // If we're inside an island or template-based component, render children as static
79
+ // (they will be re-rendered when the parent hydrates or when SPA takes over)
80
+ if (ctx.insideIsland || ctx.insideTemplate) {
81
+ return this.renderStaticComponent(name, definition, props, ctx);
82
+ }
79
83
  // Analyze the component
80
84
  const analysis = this.analyzer.analyze(name, definition);
81
85
  if (analysis.type === 'island') {
@@ -94,6 +98,10 @@ export class StaticHtmlRenderer {
94
98
  ctx.styles.add(definition.css);
95
99
  }
96
100
  let config;
101
+ // If this component has a template, mark context so children render as static
102
+ // Template-based components are re-rendered by SPA, so their children don't need island markers
103
+ const hasTemplate = !!definition.template;
104
+ const childCtx = hasTemplate ? { ...ctx, insideTemplate: true } : ctx;
97
105
  // Get component config from template or items
98
106
  if (definition.template) {
99
107
  // Get controller state for SSR
@@ -138,8 +146,8 @@ export class StaticHtmlRenderer {
138
146
  config[key] = definition[key];
139
147
  }
140
148
  }
141
- // Render the config tree
142
- return this.renderConfig(config, ctx);
149
+ // Render the config tree using childCtx (which has insideTemplate=true if this has a template)
150
+ return this.renderConfig(config, childCtx);
143
151
  }
144
152
  /**
145
153
  * Render an island as a placeholder with hydration data
@@ -158,7 +166,10 @@ export class StaticHtmlRenderer {
158
166
  };
159
167
  ctx.islands.push(islandData);
160
168
  // Render the full static content (for SEO)
161
- const staticContent = await this.renderStaticComponent(name, definition, props, ctx);
169
+ // Mark that we're inside an island so child components don't become islands
170
+ // (they will be re-rendered when this island hydrates on the client)
171
+ const islandCtx = { ...ctx, insideIsland: true };
172
+ const staticContent = await this.renderStaticComponent(name, definition, props, islandCtx);
162
173
  // Wrap in island container with data attributes for hydration
163
174
  const propsJson = this.escapeAttr(JSON.stringify(islandData.props));
164
175
  return `<div data-ez-island="${name}" data-ez-island-id="${islandId}" data-ez-props="${propsJson}">${staticContent}</div>`;
@@ -447,10 +458,12 @@ hydrateIslands(window.__EZ_ISLANDS__);
447
458
  return result;
448
459
  }
449
460
  /**
450
- * Reset island counter (for testing)
461
+ * Reset island counter and clear analyzer cache
462
+ * Called before each SSR render to ensure fresh analysis
451
463
  */
452
464
  resetIslandCounter() {
453
465
  islandIdCounter = 0;
466
+ this.analyzer.clearCache();
454
467
  }
455
468
  }
456
469
  // Export singleton
@@ -19,6 +19,12 @@ export interface RenderContext {
19
19
  props?: Record<string, unknown>;
20
20
  // Server-side data loaded via load()
21
21
  serverData?: Record<string, unknown>;
22
+ // True when rendering inside an island - children should not be islands
23
+ // (they will be re-rendered when the parent island hydrates)
24
+ insideIsland?: boolean;
25
+ // True when rendering inside a template-based component
26
+ // Template components are re-rendered by SPA, so children don't need islands
27
+ insideTemplate?: boolean;
22
28
  }
23
29
 
24
30
  export interface IslandData {
@@ -150,6 +156,12 @@ export class StaticHtmlRenderer {
150
156
  return this.renderNativeElement(name, props as ComponentConfig, ctx);
151
157
  }
152
158
 
159
+ // If we're inside an island or template-based component, render children as static
160
+ // (they will be re-rendered when the parent hydrates or when SPA takes over)
161
+ if (ctx.insideIsland || ctx.insideTemplate) {
162
+ return this.renderStaticComponent(name, definition, props, ctx);
163
+ }
164
+
153
165
  // Analyze the component
154
166
  const analysis = this.analyzer.analyze(name, definition);
155
167
 
@@ -178,6 +190,11 @@ export class StaticHtmlRenderer {
178
190
 
179
191
  let config: ComponentConfig;
180
192
 
193
+ // If this component has a template, mark context so children render as static
194
+ // Template-based components are re-rendered by SPA, so their children don't need island markers
195
+ const hasTemplate = !!definition.template;
196
+ const childCtx = hasTemplate ? { ...ctx, insideTemplate: true } : ctx;
197
+
181
198
  // Get component config from template or items
182
199
  if (definition.template) {
183
200
  // Get controller state for SSR
@@ -224,8 +241,8 @@ export class StaticHtmlRenderer {
224
241
  }
225
242
  }
226
243
 
227
- // Render the config tree
228
- return this.renderConfig(config, ctx);
244
+ // Render the config tree using childCtx (which has insideTemplate=true if this has a template)
245
+ return this.renderConfig(config, childCtx);
229
246
  }
230
247
 
231
248
  /**
@@ -252,7 +269,10 @@ export class StaticHtmlRenderer {
252
269
  ctx.islands.push(islandData);
253
270
 
254
271
  // Render the full static content (for SEO)
255
- const staticContent = await this.renderStaticComponent(name, definition, props, ctx);
272
+ // Mark that we're inside an island so child components don't become islands
273
+ // (they will be re-rendered when this island hydrates on the client)
274
+ const islandCtx = { ...ctx, insideIsland: true };
275
+ const staticContent = await this.renderStaticComponent(name, definition, props, islandCtx);
256
276
 
257
277
  // Wrap in island container with data attributes for hydration
258
278
  const propsJson = this.escapeAttr(JSON.stringify(islandData.props));
@@ -554,10 +574,12 @@ hydrateIslands(window.__EZ_ISLANDS__);
554
574
  }
555
575
 
556
576
  /**
557
- * Reset island counter (for testing)
577
+ * Reset island counter and clear analyzer cache
578
+ * Called before each SSR render to ensure fresh analysis
558
579
  */
559
580
  resetIslandCounter(): void {
560
581
  islandIdCounter = 0;
582
+ this.analyzer.clearCache();
561
583
  }
562
584
  }
563
585
 
@@ -47,7 +47,8 @@ export interface ComponentDefinition {
47
47
  export declare class ComponentAnalyzer {
48
48
  private cache;
49
49
  /**
50
- * Analyze a component definition and determine its type
50
+ * Analyze a component definition and determine its type.
51
+ * Cache is cleared by StaticHtmlRenderer.resetIslandCounter() before each SSR render.
51
52
  */
52
53
  analyze(name: string, definition: ComponentDefinition): AnalysisResult;
53
54
  /**
@@ -58,9 +58,12 @@ const BROWSER_API_PATTERNS = [
58
58
  * Main analyzer class
59
59
  */
60
60
  export class ComponentAnalyzer {
61
- cache = new Map();
61
+ constructor() {
62
+ this.cache = new Map();
63
+ }
62
64
  /**
63
- * Analyze a component definition and determine its type
65
+ * Analyze a component definition and determine its type.
66
+ * Cache is cleared by StaticHtmlRenderer.resetIslandCounter() before each SSR render.
64
67
  */
65
68
  analyze(name, definition) {
66
69
  // Check cache first
@@ -95,7 +95,8 @@ export class ComponentAnalyzer {
95
95
  private cache: Map<string, AnalysisResult> = new Map();
96
96
 
97
97
  /**
98
- * Analyze a component definition and determine its type
98
+ * Analyze a component definition and determine its type.
99
+ * Cache is cleared by StaticHtmlRenderer.resetIslandCounter() before each SSR render.
99
100
  */
100
101
  analyze(name: string, definition: ComponentDefinition): AnalysisResult {
101
102
  // Check cache first
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ezfw-core",
3
- "version": "1.0.79",
3
+ "version": "1.0.82",
4
4
  "description": "Ez Framework - A declarative component framework for building modern web applications",
5
5
  "type": "module",
6
6
  "main": "./core/ez.ts",