@sprlab/wccompiler 0.9.4 → 0.9.6

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,86 @@
1
+ /**
2
+ * Angular adapter for WCC Scoped Slots.
3
+ *
4
+ * Exports:
5
+ * - WccSlotDef: Auxiliary directive for ng-template[slot]
6
+ * - WccSlotsDirective: Main directive activated via [wccSlots] attribute
7
+ * - SlotContext: Interface for template context typing
8
+ *
9
+ * Usage:
10
+ * import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular';
11
+ *
12
+ * @Component({
13
+ * imports: [WccSlotsDirective, WccSlotDef],
14
+ * schemas: [CUSTOM_ELEMENTS_SCHEMA],
15
+ * template: `
16
+ * <wcc-card wccSlots>
17
+ * <ng-template slot="header"><strong>Header</strong></ng-template>
18
+ * <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
19
+ * </wcc-card>
20
+ * `
21
+ * })
22
+ *
23
+ * Note: Add the `wccSlots` attribute to any WCC custom element that uses slots.
24
+ * This is required because Angular AOT cannot evaluate dynamic selectors.
25
+ *
26
+ * @module @sprlab/wccompiler/adapters/angular
27
+ */
28
+ import { QueryList, AfterContentInit, OnDestroy } from '@angular/core';
29
+ /** Context object passed to createEmbeddedView for scoped slots */
30
+ export interface SlotContext {
31
+ $implicit: any;
32
+ [key: string]: any;
33
+ }
34
+ /**
35
+ * Auxiliary directive that marks an ng-template as slot content.
36
+ * Captures the TemplateRef and the slot name from the HTML 'slot' attribute.
37
+ *
38
+ * Usage:
39
+ * <ng-template slot="header">...</ng-template>
40
+ * <ng-template slot="stats" let-likes>{{likes}}</ng-template>
41
+ */
42
+ export declare class WccSlotDef {
43
+ readonly templateRef: any;
44
+ readonly slotName: string;
45
+ constructor(name: string | null);
46
+ }
47
+ /**
48
+ * Main directive that activates on elements with the [wccSlots] attribute.
49
+ * Classifies ng-template[slot] children as named or scoped slots and manages
50
+ * their lifecycle.
51
+ *
52
+ * Uses a simple attribute selector `[wccSlots]` instead of a dynamic exclusion
53
+ * selector, because Angular AOT cannot evaluate computed selector expressions.
54
+ */
55
+ export declare class WccSlotsDirective implements AfterContentInit, OnDestroy {
56
+ slotDefs: QueryList<WccSlotDef>;
57
+ private el;
58
+ private vcr;
59
+ private cdr;
60
+ private slots;
61
+ private eventCleanups;
62
+ private destroyed;
63
+ ngAfterContentInit(): void;
64
+ ngOnDestroy(): void;
65
+ /** Classifies slots using __scopedSlots from the host element and initializes them */
66
+ private classifyAndInitSlots;
67
+ /** Named Slot: immediate static rendering */
68
+ private initNamedSlot;
69
+ /** Scoped Slot: registration + reactive rendering */
70
+ private initScopedSlot;
71
+ /**
72
+ * Builds the Angular context for createEmbeddedView.
73
+ *
74
+ * Rules:
75
+ * - 0 props: $implicit = undefined
76
+ * - 1 prop: $implicit = that single value, plus the named prop key
77
+ * - N props (N > 1): $implicit = full props object, plus all named props
78
+ */
79
+ buildContext(props: Record<string, any>): SlotContext;
80
+ /** Creates or updates the EmbeddedView of a scoped slot */
81
+ private renderSlot;
82
+ /** Inserts view root nodes into the custom element's DOM via a wrapper div */
83
+ private insertView;
84
+ /** Full cleanup on destroy */
85
+ private cleanup;
86
+ }
@@ -1,146 +1,54 @@
1
1
  /**
2
- * Angular adapter for WCC (defineModel + Scoped Slots).
3
- *
4
- * This module provides Angular integration for WCC components:
5
- *
6
- * ═══════════════════════════════════════════════════════════════════════════════
7
- * SCOPED SLOTS (Native Angular Syntax)
8
- * ═══════════════════════════════════════════════════════════════════════════════
9
- *
10
- * For native Angular scoped slot support, use the TypeScript directives:
11
- *
12
- * import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular';
13
- *
14
- * Setup:
15
- * @Component({
16
- * imports: [WccSlotsDirective, WccSlotDef],
17
- * schemas: [CUSTOM_ELEMENTS_SCHEMA],
18
- * template: `...`
19
- * })
20
- *
21
- * The directives auto-activate on custom elements (tags with hyphen).
22
- * No [wccSlots] attribute is needed on the host element.
23
- *
24
- * Slot declaration uses ng-template[slot] syntax:
25
- *
26
- * <!-- Named slot (static content, no let-*) -->
27
- * <wcc-card>
28
- * <ng-template slot="header"><strong>My Header</strong></ng-template>
29
- * </wcc-card>
30
- *
31
- * <!-- Scoped slot (reactive data via let-*) -->
32
- * <wcc-card>
33
- * <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
34
- * </wcc-card>
35
- *
36
- * <!-- Multiple props in scoped slot -->
37
- * <wcc-card>
38
- * <ng-template slot="details" let-data let-likes="likes" let-total="total">
39
- * {{ likes }}/{{ total }}
40
- * </ng-template>
41
- * </wcc-card>
42
- *
43
- * How it works:
44
- * - WccSlotDef captures the TemplateRef and slot name from the 'slot' attribute
45
- * - WccSlotsDirective classifies slots as named or scoped using __scopedSlots
46
- * - Named slots render immediately into <div slot="name" style="display:contents">
47
- * - Scoped slots register a renderer via element.registerSlotRenderer()
48
- * - When slot props change, the renderer updates the Angular EmbeddedView
49
- * - Compatible with OnPush change detection strategy
50
- *
51
- * The directive source is in adapters/angular.ts (TypeScript with Angular decorators).
52
- *
53
- * ═══════════════════════════════════════════════════════════════════════════════
54
- * BACKWARD COMPATIBILITY: slot-template-* (Token Replacement)
55
- * ═══════════════════════════════════════════════════════════════════════════════
56
- *
57
- * The legacy slot-template-* attribute approach continues to work without
58
- * importing the directives:
59
- *
60
- * <wcc-list>
61
- * <div slot-template-item="<li>{%item%}</li>"></div>
62
- * </wcc-list>
63
- *
64
- * When WccSlotsDirective IS imported, ng-template[slot] takes priority over
65
- * slot-template-* for the same slot name. Slots not covered by ng-template
66
- * continue using the token replacement path.
67
- *
68
- * ═══════════════════════════════════════════════════════════════════════════════
69
- * defineModel (Two-Way Binding)
70
- * ═══════════════════════════════════════════════════════════════════════════════
71
- *
72
- * The WCC component already emits `propNameChange` directly from _modelSet,
73
- * so Angular's [(prop)] banana-box syntax works WITHOUT this adapter.
74
- *
75
- * This file is kept for:
76
- * 1. Documentation of the Angular integration approach
77
- * 2. The ControlValueAccessor guide for ngModel support
78
- *
79
- * Setup (Angular):
80
- * // No adapter import needed for [(prop)]! Just use CUSTOM_ELEMENTS_SCHEMA:
81
- * import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
82
- * @Component({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })
83
- *
84
- * Usage:
85
- * <wcc-input [(value)]="text"></wcc-input>
86
- * <wcc-counter [(count)]="myCount"></wcc-counter>
87
- *
88
- * How it works:
89
- * Angular's [(prop)] expands to [prop]="value" (propChange)="value = $event.detail"
90
- * WCC _modelSet emits propNameChange CustomEvent with detail=newValue
91
- * Angular picks it up automatically — no adapter needed.
2
+ * Angular adapter for WCC custom elements.
92
3
  *
93
4
  * @module @sprlab/wccompiler/adapters/angular
5
+ *
6
+ * ANGULAR INTEGRATION:
7
+ *
8
+ * The adapter ships as TypeScript source (adapters/angular.ts) because Angular
9
+ * AOT requires directives to be compiled within the consuming project's scope.
10
+ * The package.json "exports" map points directly to the .ts file, which Angular's
11
+ * esbuild-based `application` builder (Angular 17+) handles natively.
12
+ *
13
+ * SETUP:
14
+ *
15
+ * 1. Install the package:
16
+ * npm install @sprlab/wccompiler
17
+ *
18
+ * 2. Add a tsconfig path mapping (tsconfig.json):
19
+ * {
20
+ * "compilerOptions": {
21
+ * "paths": {
22
+ * "@sprlab/wccompiler/adapters/angular": ["node_modules/@sprlab/wccompiler/adapters/angular.ts"]
23
+ * }
24
+ * }
25
+ * }
26
+ *
27
+ * 3. Import in your component:
28
+ * import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular';
29
+ *
30
+ * @Component({
31
+ * imports: [WccSlotsDirective, WccSlotDef],
32
+ * schemas: [CUSTOM_ELEMENTS_SCHEMA],
33
+ * template: `
34
+ * <wcc-card wccSlots>
35
+ * <ng-template slot="header"><strong>Title</strong></ng-template>
36
+ * <ng-template slot="stats" let-likes>⭐ {{likes}} stars!</ng-template>
37
+ * </wcc-card>
38
+ * `
39
+ * })
40
+ *
41
+ * HOW IT WORKS:
42
+ *
43
+ * - WccSlotDef: Captures ng-template[slot] elements and their slot names
44
+ * - WccSlotsDirective: Activated via [wccSlots] attribute on the host element
45
+ * - Classifies slots as "named" or "scoped" using the component's __scopedSlots metadata
46
+ * - Named slots: rendered immediately into the component's [data-slot] container
47
+ * - Scoped slots: registered via registerSlotRenderer() for reactive updates
48
+ *
49
+ * REQUIREMENTS:
50
+ * - Angular 17+ with the `application` builder (esbuild-based)
51
+ * - The [wccSlots] attribute must be added to WCC elements that use slots
52
+ *
53
+ * NOTE: This .js file is documentation only. The actual source is adapters/angular.ts.
94
54
  */
95
-
96
- // ── ControlValueAccessor for ngModel/ReactiveForms ──────────────────
97
- // Angular's ngModel requires a ControlValueAccessor to bridge form controls.
98
- // Copy this into your Angular project as a .ts file:
99
- //
100
- // ```ts
101
- // import { Directive, ElementRef, forwardRef, HostListener } from '@angular/core';
102
- // import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
103
- //
104
- // @Directive({
105
- // selector: '[wccModel]',
106
- // providers: [{
107
- // provide: NG_VALUE_ACCESSOR,
108
- // useExisting: forwardRef(() => WccValueAccessor),
109
- // multi: true
110
- // }]
111
- // })
112
- // export class WccValueAccessor implements ControlValueAccessor {
113
- // private onChange: (value: any) => void = () => {};
114
- // private onTouched: () => void = () => {};
115
- //
116
- // constructor(private el: ElementRef<HTMLElement>) {}
117
- //
118
- // writeValue(value: any): void {
119
- // if (value != null) {
120
- // this.el.nativeElement.setAttribute('value', String(value));
121
- // } else {
122
- // this.el.nativeElement.removeAttribute('value');
123
- // }
124
- // }
125
- //
126
- // registerOnChange(fn: (value: any) => void): void {
127
- // this.onChange = fn;
128
- // }
129
- //
130
- // registerOnTouched(fn: () => void): void {
131
- // this.onTouched = fn;
132
- // }
133
- //
134
- // @HostListener('wcc:model', ['$event'])
135
- // onModelChange(event: CustomEvent): void {
136
- // if (event.detail && event.detail.prop === 'value') {
137
- // this.onChange(event.detail.value);
138
- // }
139
- // }
140
- //
141
- // @HostListener('blur')
142
- // onBlur(): void {
143
- // this.onTouched();
144
- // }
145
- // }
146
- // ```
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Exports:
5
5
  * - WccSlotDef: Auxiliary directive for ng-template[slot]
6
- * - WccSlotsDirective: Main directive that auto-activates on custom elements
6
+ * - WccSlotsDirective: Main directive activated via [wccSlots] attribute
7
7
  * - SlotContext: Interface for template context typing
8
8
  *
9
9
  * Usage:
@@ -13,13 +13,16 @@
13
13
  * imports: [WccSlotsDirective, WccSlotDef],
14
14
  * schemas: [CUSTOM_ELEMENTS_SCHEMA],
15
15
  * template: `
16
- * <wcc-card>
16
+ * <wcc-card wccSlots>
17
17
  * <ng-template slot="header"><strong>Header</strong></ng-template>
18
18
  * <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
19
19
  * </wcc-card>
20
20
  * `
21
21
  * })
22
22
  *
23
+ * Note: Add the `wccSlots` attribute to any WCC custom element that uses slots.
24
+ * This is required because Angular AOT cannot evaluate dynamic selectors.
25
+ *
23
26
  * @module @sprlab/wccompiler/adapters/angular
24
27
  */
25
28
 
@@ -83,21 +86,15 @@ export class WccSlotDef {
83
86
  // ─── WccSlotsDirective — Main Directive ─────────────────────────────────────
84
87
 
85
88
  /**
86
- * Exclusion selector: lists all standard HTML elements.
87
- * Custom elements (which MUST contain a hyphen) are not excluded.
88
- */
89
- const STANDARD_ELEMENTS = 'div,span,p,a,button,input,form,section,article,header,footer,nav,main,ul,ol,li,table,tr,td,th,thead,tbody,tfoot,img,h1,h2,h3,h4,h5,h6,label,select,textarea,option,fieldset,legend,details,summary,dialog,slot,template,canvas,video,audio,source,iframe,pre,code,blockquote,hr,br,strong,em,small,sub,sup,mark,del,ins,figure,figcaption,picture,svg,math,body,html,head,script,style,link,meta,title,base,col,colgroup,caption,abbr,address,area,aside,b,bdi,bdo,cite,data,dd,dfn,dl,dt,i,kbd,map,meter,noscript,output,progress,q,rp,rt,ruby,s,samp,time,u,var,wbr';
90
-
91
- /** Build the exclusion selector string */
92
- const EXCLUSION_SELECTOR = STANDARD_ELEMENTS.split(',').map(t => `:not(${t})`).join('');
93
-
94
- /**
95
- * Main directive that auto-activates on custom elements (tags with hyphen).
89
+ * Main directive that activates on elements with the [wccSlots] attribute.
96
90
  * Classifies ng-template[slot] children as named or scoped slots and manages
97
91
  * their lifecycle.
92
+ *
93
+ * Uses a simple attribute selector `[wccSlots]` instead of a dynamic exclusion
94
+ * selector, because Angular AOT cannot evaluate computed selector expressions.
98
95
  */
99
96
  @Directive({
100
- selector: EXCLUSION_SELECTOR,
97
+ selector: '[wccSlots]',
101
98
  standalone: true,
102
99
  })
103
100
  export class WccSlotsDirective implements AfterContentInit, OnDestroy {
@@ -126,12 +123,21 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
126
123
  // ─── Classification ─────────────────────────────────────────────────────
127
124
 
128
125
  /** Classifies slots using __scopedSlots from the host element and initializes them */
129
- private classifyAndInitSlots(): void {
130
- const element = this.el.nativeElement as any;
131
- const scopedNames: string[] = element.__scopedSlots || [];
126
+ private async classifyAndInitSlots(): Promise<void> {
127
+ const hostEl = this.el.nativeElement;
128
+ const tagName = hostEl.tagName.toLowerCase();
129
+
130
+ // Wait for the custom element to be defined (ensures the class is upgraded)
131
+ await customElements.whenDefined(tagName);
132
+ if (this.destroyed) return;
133
+
134
+ const element = hostEl as any;
135
+ // Read from instance getter or static property
136
+ const scopedNames: string[] = element.__scopedSlots
137
+ || (element.constructor && element.constructor.__scopedSlots)
138
+ || [];
132
139
 
133
140
  for (const slotDef of this.slotDefs) {
134
- // Ignore templates with empty slot name
135
141
  if (!slotDef.slotName) continue;
136
142
 
137
143
  if (scopedNames.includes(slotDef.slotName)) {
@@ -147,15 +153,27 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
147
153
  /** Named Slot: immediate static rendering */
148
154
  private initNamedSlot(slotDef: WccSlotDef): void {
149
155
  const hostEl = this.el.nativeElement;
150
- const wrapper = document.createElement('div');
151
- wrapper.setAttribute('slot', slotDef.slotName);
152
- wrapper.style.display = 'contents';
156
+
157
+ // Strategy 1: Find [data-slot] container inside the component's internal DOM
158
+ const dataSlotEl = hostEl.querySelector(`[data-slot="${slotDef.slotName}"]`);
159
+ let wrapper: HTMLElement;
160
+
161
+ if (dataSlotEl) {
162
+ // Use the data-slot element directly — clear fallback content and insert rendered nodes
163
+ wrapper = dataSlotEl as HTMLElement;
164
+ wrapper.innerHTML = '';
165
+ } else {
166
+ // Strategy 2: Fallback for Shadow DOM / native <slot> elements
167
+ wrapper = document.createElement('div');
168
+ wrapper.setAttribute('slot', slotDef.slotName);
169
+ wrapper.style.display = 'contents';
170
+ hostEl.appendChild(wrapper);
171
+ }
153
172
 
154
173
  const viewRef = this.vcr.createEmbeddedView(slotDef.templateRef);
155
174
  for (const node of viewRef.rootNodes) {
156
175
  wrapper.appendChild(node);
157
176
  }
158
- hostEl.appendChild(wrapper);
159
177
 
160
178
  this.slots.set(slotDef.slotName, {
161
179
  type: 'named',
@@ -165,18 +183,15 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
165
183
  wrapperEl: wrapper,
166
184
  context: null,
167
185
  });
186
+
187
+ this.cdr.detectChanges();
168
188
  }
169
189
 
170
190
  // ─── Scoped Slot ────────────────────────────────────────────────────────
171
191
 
172
- /** Scoped Slot: async registration + reactive rendering */
173
- private async initScopedSlot(slotDef: WccSlotDef): Promise<void> {
192
+ /** Scoped Slot: registration + reactive rendering */
193
+ private initScopedSlot(slotDef: WccSlotDef): void {
174
194
  const hostEl = this.el.nativeElement;
175
- const tagName = hostEl.tagName.toLowerCase();
176
-
177
- // Wait for the custom element to be defined
178
- await customElements.whenDefined(tagName);
179
- if (this.destroyed) return;
180
195
 
181
196
  const state: SlotState = {
182
197
  type: 'scoped',
@@ -237,7 +252,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
237
252
  const state = this.slots.get(slotName);
238
253
  if (!state || this.destroyed) return;
239
254
 
240
- // Props null/undefined: clear the view
241
255
  if (props == null) {
242
256
  if (state.viewRef) {
243
257
  state.viewRef.destroy();
@@ -250,25 +264,51 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
250
264
  state.context = context;
251
265
 
252
266
  if (state.viewRef) {
253
- // Update existing context
267
+ // Update existing view context
254
268
  Object.assign(state.viewRef.context, context);
255
269
  state.viewRef.markForCheck();
270
+ // Re-insert nodes to reflect updated content (Angular doesn't auto-update DOM for detached views)
271
+ if (state.wrapperEl) {
272
+ state.wrapperEl.innerHTML = '';
273
+ for (const node of state.viewRef.rootNodes) {
274
+ state.wrapperEl.appendChild(node);
275
+ }
276
+ }
256
277
  } else {
257
- // Create new view
258
278
  state.viewRef = this.vcr.createEmbeddedView(state.slotDef.templateRef, context);
259
279
  this.insertView(slotName, state);
260
280
  }
261
281
 
262
- this.cdr.markForCheck();
282
+ this.cdr.detectChanges();
263
283
  }
264
284
 
265
285
  // ─── DOM Insertion ──────────────────────────────────────────────────────
266
286
 
267
- /** Inserts view root nodes into the custom element's DOM via a wrapper div */
287
+ /**
288
+ * Inserts view root nodes into the custom element's DOM.
289
+ *
290
+ * Strategy:
291
+ * 1. Look for a [data-slot="slotName"] element inside the component (non-Shadow DOM)
292
+ * → clear its content and insert the rendered nodes there
293
+ * 2. Fallback: append a wrapper <div slot="slotName"> to the host (Shadow DOM / native slots)
294
+ */
268
295
  private insertView(slotName: string, state: SlotState): void {
269
296
  if (!state.viewRef) return;
270
297
  const hostEl = this.el.nativeElement;
271
298
 
299
+ // Strategy 1: Find [data-slot] container inside the component's internal DOM
300
+ const dataSlotEl = hostEl.querySelector(`[data-slot="${slotName}"]`);
301
+ if (dataSlotEl) {
302
+ // Use the data-slot element as the wrapper (no extra div needed)
303
+ state.wrapperEl = dataSlotEl as HTMLElement;
304
+ state.wrapperEl.innerHTML = '';
305
+ for (const node of state.viewRef.rootNodes) {
306
+ state.wrapperEl.appendChild(node);
307
+ }
308
+ return;
309
+ }
310
+
311
+ // Strategy 2: Fallback for Shadow DOM / native <slot> elements
272
312
  if (!state.wrapperEl) {
273
313
  state.wrapperEl = document.createElement('div');
274
314
  state.wrapperEl.setAttribute('slot', slotName);
@@ -276,7 +316,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
276
316
  hostEl.appendChild(state.wrapperEl);
277
317
  }
278
318
 
279
- // Clear previous wrapper content and append new nodes
280
319
  state.wrapperEl.innerHTML = '';
281
320
  for (const node of state.viewRef.rootNodes) {
282
321
  state.wrapperEl.appendChild(node);
@@ -287,7 +326,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
287
326
 
288
327
  /** Full cleanup on destroy */
289
328
  private cleanup(): void {
290
- // Destroy views, invoke cleanup functions, remove wrappers
291
329
  for (const [, state] of this.slots) {
292
330
  if (state.viewRef) {
293
331
  state.viewRef.destroy();
@@ -295,13 +333,18 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
295
333
  if (state.cleanup) {
296
334
  state.cleanup();
297
335
  }
298
- if (state.wrapperEl && state.wrapperEl.parentNode) {
299
- state.wrapperEl.parentNode.removeChild(state.wrapperEl);
336
+ if (state.wrapperEl) {
337
+ // If the wrapper is a [data-slot] element (part of the component's internal DOM),
338
+ // just clear its content rather than removing it from the DOM
339
+ if (state.wrapperEl.hasAttribute('data-slot')) {
340
+ state.wrapperEl.innerHTML = '';
341
+ } else if (state.wrapperEl.parentNode) {
342
+ state.wrapperEl.parentNode.removeChild(state.wrapperEl);
343
+ }
300
344
  }
301
345
  }
302
346
  this.slots.clear();
303
347
 
304
- // Remove event listeners
305
348
  for (const fn of this.eventCleanups) {
306
349
  fn();
307
350
  }
package/bin/wcc.js CHANGED
File without changes
package/lib/codegen.js CHANGED
@@ -1288,17 +1288,19 @@ export function generateComponent(parseResult, options = {}) {
1288
1288
  const ref = slotPropRef(sp.source, signalNames, computedNames, propNames);
1289
1289
  return `${sp.prop}: ${ref}`;
1290
1290
  }).join(', ');
1291
- lines.push(` if (this.__slotTpl_${s.name}) {`);
1291
+ // Scoped slot effect: always compute props and notify renderers
1292
+ // The effect runs regardless of whether a template was provided (Angular uses registerSlotRenderer)
1292
1293
  lines.push(' __effect(() => {');
1293
1294
  lines.push(` const __props = { ${propsObj} };`);
1294
- // Task 3.1: Store current props in __slotProps
1295
+ // Store current props for late-registering renderers
1295
1296
  lines.push(` this.__slotProps['${s.name}'] = __props;`);
1296
- // Task 3.2: Emit wcc:slot-update event
1297
+ // Emit wcc:slot-update event
1297
1298
  lines.push(` this.dispatchEvent(new CustomEvent('wcc:slot-update', { detail: { slot: '${s.name}', props: __props }, bubbles: false }));`);
1298
- // Task 3.3: Check for registered renderer, skip token replacement if present
1299
+ // Check for registered renderer (Angular directive)
1299
1300
  lines.push(` if (this.__slotRenderers && this.__slotRenderers['${s.name}']) {`);
1300
1301
  lines.push(` this.__slotRenderers['${s.name}'](__props);`);
1301
- lines.push(' } else {');
1302
+ lines.push(` } else if (this.__slotTpl_${s.name}) {`);
1303
+ // Fallback: template-based token replacement (WCC-to-WCC, Vue, React)
1302
1304
  lines.push(` let __html = this.__slotTpl_${s.name};`);
1303
1305
  lines.push(" for (const [k, v] of Object.entries(__props)) {");
1304
1306
  lines.push(` __html = __html.replace(new RegExp('(?:\\\\{\\\\{|\\\\{%)\\\\s*' + k + '(\\\\(\\\\))?\\\\s*(?:\\\\}\\\\}|%\\\\})', 'g'), v ?? '');`);
@@ -1306,7 +1308,6 @@ export function generateComponent(parseResult, options = {}) {
1306
1308
  lines.push(` this.${s.varName}.innerHTML = __html;`);
1307
1309
  lines.push(' }');
1308
1310
  lines.push(' });');
1309
- lines.push(' }');
1310
1311
  }
1311
1312
  }
1312
1313
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Zero-runtime compiler that transforms .wcc single-file components into native web components with signals-based reactivity",
5
5
  "type": "module",
6
6
  "exports": {
@@ -9,7 +9,10 @@
9
9
  "./integrations/react": "./integrations/react.js",
10
10
  "./integrations/angular": "./integrations/angular.js",
11
11
  "./adapters/vue": "./adapters/vue.js",
12
- "./adapters/angular": "./adapters/angular.js",
12
+ "./adapters/angular": {
13
+ "types": "./adapters/angular-compiled/angular.d.ts",
14
+ "default": "./adapters/angular.ts"
15
+ },
13
16
  "./adapters/react": "./adapters/react.js"
14
17
  },
15
18
  "bin": {