@sprlab/wccompiler 0.9.4 → 0.9.5

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: async 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,42 @@
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
+ * 1. For basic two-way binding with [(prop)]:
9
+ * No adapter needed! WCC components emit propNameChange directly.
10
+ * Just add CUSTOM_ELEMENTS_SCHEMA to your component.
11
+ *
12
+ * 2. For slots (named + scoped):
13
+ * Copy `adapters/angular.ts` to your Angular project's src/ directory.
14
+ * This is the standard pattern for Web Component libraries in Angular
15
+ * (same as Shoelace, Lit, FAST, etc.) because Angular AOT requires
16
+ * directives to be compiled within the project.
17
+ *
18
+ * Steps:
19
+ * a) Copy adapters/angular.ts → src/directives/wcc-slots.directive.ts
20
+ * b) Import in your component:
21
+ * import { WccSlotsDirective, WccSlotDef } from './directives/wcc-slots.directive';
22
+ * c) Add to imports: @Component({ imports: [WccSlotsDirective, WccSlotDef] })
23
+ * d) Add wccSlots attribute to WCC elements that use slots:
24
+ * <wcc-card wccSlots>
25
+ * <ng-template slot="header">...</ng-template>
26
+ * </wcc-card>
27
+ *
28
+ * WHY can't we distribute as a compiled package?
29
+ * Angular AOT requires decorator metadata that can only be generated by
30
+ * ng-packagr or the Angular compiler. Standard tsc output doesn't include
31
+ * the Ivy metadata Angular needs. A future version may provide a separate
32
+ * @sprlab/wccompiler-angular package compiled with ng-packagr.
33
+ *
34
+ * SELECTOR:
35
+ * The directive uses [wccSlots] attribute selector (not a dynamic exclusion
36
+ * selector) because Angular AOT cannot evaluate computed expressions in
37
+ * decorator metadata.
94
38
  */
95
39
 
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
- // ```
40
+ // This file is intentionally a documentation-only .js file.
41
+ // The actual directive source is in adapters/angular.ts (TypeScript).
42
+ // Users copy it to their Angular project for AOT compilation.
@@ -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 {
@@ -131,7 +128,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
131
128
  const scopedNames: string[] = element.__scopedSlots || [];
132
129
 
133
130
  for (const slotDef of this.slotDefs) {
134
- // Ignore templates with empty slot name
135
131
  if (!slotDef.slotName) continue;
136
132
 
137
133
  if (scopedNames.includes(slotDef.slotName)) {
@@ -237,7 +233,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
237
233
  const state = this.slots.get(slotName);
238
234
  if (!state || this.destroyed) return;
239
235
 
240
- // Props null/undefined: clear the view
241
236
  if (props == null) {
242
237
  if (state.viewRef) {
243
238
  state.viewRef.destroy();
@@ -250,11 +245,9 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
250
245
  state.context = context;
251
246
 
252
247
  if (state.viewRef) {
253
- // Update existing context
254
248
  Object.assign(state.viewRef.context, context);
255
249
  state.viewRef.markForCheck();
256
250
  } else {
257
- // Create new view
258
251
  state.viewRef = this.vcr.createEmbeddedView(state.slotDef.templateRef, context);
259
252
  this.insertView(slotName, state);
260
253
  }
@@ -276,7 +269,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
276
269
  hostEl.appendChild(state.wrapperEl);
277
270
  }
278
271
 
279
- // Clear previous wrapper content and append new nodes
280
272
  state.wrapperEl.innerHTML = '';
281
273
  for (const node of state.viewRef.rootNodes) {
282
274
  state.wrapperEl.appendChild(node);
@@ -287,7 +279,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
287
279
 
288
280
  /** Full cleanup on destroy */
289
281
  private cleanup(): void {
290
- // Destroy views, invoke cleanup functions, remove wrappers
291
282
  for (const [, state] of this.slots) {
292
283
  if (state.viewRef) {
293
284
  state.viewRef.destroy();
@@ -301,7 +292,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
301
292
  }
302
293
  this.slots.clear();
303
294
 
304
- // Remove event listeners
305
295
  for (const fn of this.eventCleanups) {
306
296
  fn();
307
297
  }
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.5",
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": {