@sprlab/wccompiler 0.13.0 → 0.14.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.
@@ -1,488 +1,488 @@
1
- /**
2
- * Angular adapter for WCC Scoped Slots and Event Binding.
3
- *
4
- * Exports:
5
- * - WccSlotDef: Auxiliary directive for ng-template[slot]
6
- * - WccSlotsDirective: Main directive activated via [wccSlots] attribute
7
- * - WccEvent: Single-event directive (wccEvent="name" + wccEmit output)
8
- * - WccEvents: Multi-event bridging directive (kebab-case → camelCase)
9
- * - SlotContext: Interface for template context typing
10
- *
11
- * Usage:
12
- * import { WccSlotsDirective, WccSlotDef, WccEvent, WccEvents } from '@sprlab/wccompiler/adapters/angular';
13
- *
14
- * @Component({
15
- * imports: [WccSlotsDirective, WccSlotDef, WccEvent, WccEvents],
16
- * schemas: [CUSTOM_ELEMENTS_SCHEMA],
17
- * template: `
18
- * <wcc-card wccSlots>
19
- * <ng-template slot="header"><strong>Header</strong></ng-template>
20
- * <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
21
- * </wcc-card>
22
- *
23
- * <!-- Event binding option 1: single event with unwrapped detail -->
24
- * <wcc-counter wccEvent="count-changed" (wccEmit)="onCount($event)"></wcc-counter>
25
- *
26
- * <!-- Event binding option 2: camelCase event names -->
27
- * <wcc-counter wccEvents (countChanged)="onCount($event.detail)"></wcc-counter>
28
- *
29
- * <!-- Event binding option 3: standard Angular (always works) -->
30
- * <wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
31
- * `
32
- * })
33
- *
34
- * Note: Add the `wccSlots` attribute to any WCC element that uses slots.
35
- * This is required because Angular AOT cannot evaluate dynamic selectors.
36
- *
37
- * @module @sprlab/wccompiler/adapters/angular
38
- */
39
- import { Directive, TemplateRef, ElementRef, ViewContainerRef, ChangeDetectorRef, ContentChildren, Output, EventEmitter, inject, Attribute, Input, } from '@angular/core';
40
- import * as i0 from "@angular/core";
41
- // ─── WccSlotDef — Auxiliary Directive ───────────────────────────────────────
42
- /**
43
- * Auxiliary directive that marks an ng-template as slot content.
44
- * Captures the TemplateRef and the slot name from the HTML 'slot' attribute.
45
- *
46
- * Usage:
47
- * <ng-template slot="header">...</ng-template>
48
- * <ng-template slot="stats" let-likes>{{likes}}</ng-template>
49
- */
50
- export class WccSlotDef {
51
- templateRef = inject(TemplateRef);
52
- slotName;
53
- constructor(name) {
54
- this.slotName = name || '';
55
- }
56
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccSlotDef, deps: [{ token: 'slot', attribute: true }], target: i0.ɵɵFactoryTarget.Directive });
57
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.21", type: WccSlotDef, isStandalone: true, selector: "ng-template[slot]", ngImport: i0 });
58
- }
59
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccSlotDef, decorators: [{
60
- type: Directive,
61
- args: [{
62
- selector: 'ng-template[slot]',
63
- standalone: true,
64
- }]
65
- }], ctorParameters: () => [{ type: undefined, decorators: [{
66
- type: Attribute,
67
- args: ['slot']
68
- }] }] });
69
- // ─── WccSlotsDirective — Main Directive ─────────────────────────────────────
70
- /**
71
- * Main directive that activates on elements with the [wccSlots] attribute.
72
- * Classifies ng-template[slot] children as named or scoped slots and manages
73
- * their lifecycle.
74
- *
75
- * Uses a simple attribute selector `[wccSlots]` instead of a dynamic exclusion
76
- * selector, because Angular AOT cannot evaluate computed selector expressions.
77
- */
78
- export class WccSlotsDirective {
79
- slotDefs;
80
- el = inject(ElementRef);
81
- vcr = inject(ViewContainerRef);
82
- cdr = inject(ChangeDetectorRef);
83
- slots = new Map();
84
- eventCleanups = [];
85
- destroyed = false;
86
- ngAfterContentInit() {
87
- // Runtime guard: only proceed for custom elements (tag name contains hyphen)
88
- if (!this.el.nativeElement.tagName.toLowerCase().includes('-'))
89
- return;
90
- // Normalize Angular-style slot attributes: slot-header → slot="header"
91
- this.normalizeSlotAttributes();
92
- this.classifyAndInitSlots();
93
- }
94
- ngOnDestroy() {
95
- this.destroyed = true;
96
- this.cleanup();
97
- }
98
- // ─── Slot Attribute Normalization ───────────────────────────────────────
99
- /**
100
- * Normalizes Angular-style slot attributes to standard HTML slot attributes.
101
- * Converts: <div slot-header> → <div slot="header">
102
- *
103
- * This enables the Angular ng-content select pattern:
104
- * <wcc-card wccSlots>
105
- * <nav slot-header>Title</nav>
106
- * <span slot-footer>Footer</span>
107
- * </wcc-card>
108
- *
109
- * Skips reserved prefixes: slot-props, slot-template-*
110
- */
111
- normalizeSlotAttributes() {
112
- const hostEl = this.el.nativeElement;
113
- for (const child of Array.from(hostEl.children)) {
114
- for (const attr of Array.from(child.attributes)) {
115
- if (attr.name.startsWith('slot-') &&
116
- !attr.value &&
117
- attr.name !== 'slot-props' &&
118
- !attr.name.startsWith('slot-template-')) {
119
- const slotName = attr.name.slice(5); // "slot-header" → "header"
120
- child.removeAttribute(attr.name);
121
- child.setAttribute('slot', slotName);
122
- }
123
- }
124
- }
125
- }
126
- // ─── Classification ─────────────────────────────────────────────────────
127
- /** Classifies slots using __scopedSlots from the host element and initializes them */
128
- async classifyAndInitSlots() {
129
- const hostEl = this.el.nativeElement;
130
- const tagName = hostEl.tagName.toLowerCase();
131
- // Wait for the custom element to be defined (ensures the class is upgraded)
132
- await customElements.whenDefined(tagName);
133
- if (this.destroyed)
134
- return;
135
- const element = hostEl;
136
- // Read from instance getter or static property
137
- const scopedNames = element.__scopedSlots
138
- || (element.constructor && element.constructor.__scopedSlots)
139
- || [];
140
- for (const slotDef of this.slotDefs) {
141
- if (!slotDef.slotName)
142
- continue;
143
- if (scopedNames.includes(slotDef.slotName)) {
144
- this.initScopedSlot(slotDef);
145
- }
146
- else {
147
- this.initNamedSlot(slotDef);
148
- }
149
- }
150
- }
151
- // ─── Named Slot ─────────────────────────────────────────────────────────
152
- /** Named Slot: immediate static rendering */
153
- initNamedSlot(slotDef) {
154
- const hostEl = this.el.nativeElement;
155
- // Strategy 1: Find [data-slot] container inside the component's internal DOM
156
- const dataSlotEl = hostEl.querySelector(`[data-slot="${slotDef.slotName}"]`);
157
- let wrapper;
158
- if (dataSlotEl) {
159
- // Use the data-slot element directly — clear fallback content and insert rendered nodes
160
- wrapper = dataSlotEl;
161
- wrapper.innerHTML = '';
162
- }
163
- else {
164
- // Strategy 2: Fallback for Shadow DOM / native <slot> elements
165
- wrapper = document.createElement('div');
166
- wrapper.setAttribute('slot', slotDef.slotName);
167
- wrapper.style.display = 'contents';
168
- hostEl.appendChild(wrapper);
169
- }
170
- const viewRef = this.vcr.createEmbeddedView(slotDef.templateRef);
171
- for (const node of viewRef.rootNodes) {
172
- wrapper.appendChild(node);
173
- }
174
- this.slots.set(slotDef.slotName, {
175
- type: 'named',
176
- slotDef,
177
- viewRef,
178
- cleanup: null,
179
- wrapperEl: wrapper,
180
- context: null,
181
- });
182
- this.cdr.detectChanges();
183
- }
184
- // ─── Scoped Slot ────────────────────────────────────────────────────────
185
- /** Scoped Slot: registration + reactive rendering */
186
- initScopedSlot(slotDef) {
187
- const hostEl = this.el.nativeElement;
188
- const state = {
189
- type: 'scoped',
190
- slotDef,
191
- viewRef: null,
192
- cleanup: null,
193
- wrapperEl: null,
194
- context: null,
195
- };
196
- this.slots.set(slotDef.slotName, state);
197
- // Register renderer
198
- const element = hostEl;
199
- if (typeof element.registerSlotRenderer === 'function') {
200
- state.cleanup = element.registerSlotRenderer(slotDef.slotName, (props) => this.renderSlot(slotDef.slotName, props));
201
- }
202
- else {
203
- // Fallback: listen for wcc:slot-update event
204
- const handler = (e) => {
205
- if (e.detail?.slot === slotDef.slotName) {
206
- this.renderSlot(slotDef.slotName, e.detail.props);
207
- }
208
- };
209
- hostEl.addEventListener('wcc:slot-update', handler);
210
- this.eventCleanups.push(() => hostEl.removeEventListener('wcc:slot-update', handler));
211
- }
212
- }
213
- // ─── Context Construction ───────────────────────────────────────────────
214
- /**
215
- * Builds the Angular context for createEmbeddedView.
216
- *
217
- * Rules:
218
- * - 0 props: $implicit = undefined
219
- * - 1 prop: $implicit = that single value, plus the named prop key
220
- * - N props (N > 1): $implicit = full props object, plus all named props
221
- */
222
- buildContext(props) {
223
- const keys = Object.keys(props);
224
- if (keys.length === 0) {
225
- return { $implicit: undefined };
226
- }
227
- if (keys.length === 1) {
228
- return { $implicit: props[keys[0]], ...props };
229
- }
230
- return { $implicit: props, ...props };
231
- }
232
- // ─── Render Slot ────────────────────────────────────────────────────────
233
- /** Creates or updates the EmbeddedView of a scoped slot */
234
- renderSlot(slotName, props) {
235
- const state = this.slots.get(slotName);
236
- if (!state || this.destroyed)
237
- return;
238
- if (props == null) {
239
- if (state.viewRef) {
240
- state.viewRef.destroy();
241
- state.viewRef = null;
242
- }
243
- return;
244
- }
245
- const context = this.buildContext(props);
246
- state.context = context;
247
- if (state.viewRef) {
248
- // Update existing view context
249
- Object.assign(state.viewRef.context, context);
250
- state.viewRef.markForCheck();
251
- // Re-insert nodes to reflect updated content (Angular doesn't auto-update DOM for detached views)
252
- if (state.wrapperEl) {
253
- state.wrapperEl.innerHTML = '';
254
- for (const node of state.viewRef.rootNodes) {
255
- state.wrapperEl.appendChild(node);
256
- }
257
- }
258
- }
259
- else {
260
- state.viewRef = this.vcr.createEmbeddedView(state.slotDef.templateRef, context);
261
- this.insertView(slotName, state);
262
- }
263
- this.cdr.detectChanges();
264
- }
265
- // ─── DOM Insertion ──────────────────────────────────────────────────────
266
- /**
267
- * Inserts view root nodes into the custom element's DOM.
268
- *
269
- * Strategy:
270
- * 1. Look for a [data-slot="slotName"] element inside the component (non-Shadow DOM)
271
- * → clear its content and insert the rendered nodes there
272
- * 2. Fallback: append a wrapper <div slot="slotName"> to the host (Shadow DOM / native slots)
273
- */
274
- insertView(slotName, state) {
275
- if (!state.viewRef)
276
- return;
277
- const hostEl = this.el.nativeElement;
278
- // Strategy 1: Find [data-slot] container inside the component's internal DOM
279
- const dataSlotEl = hostEl.querySelector(`[data-slot="${slotName}"]`);
280
- if (dataSlotEl) {
281
- // Use the data-slot element as the wrapper (no extra div needed)
282
- state.wrapperEl = dataSlotEl;
283
- state.wrapperEl.innerHTML = '';
284
- for (const node of state.viewRef.rootNodes) {
285
- state.wrapperEl.appendChild(node);
286
- }
287
- return;
288
- }
289
- // Strategy 2: Fallback for Shadow DOM / native <slot> elements
290
- if (!state.wrapperEl) {
291
- state.wrapperEl = document.createElement('div');
292
- state.wrapperEl.setAttribute('slot', slotName);
293
- state.wrapperEl.style.display = 'contents';
294
- hostEl.appendChild(state.wrapperEl);
295
- }
296
- state.wrapperEl.innerHTML = '';
297
- for (const node of state.viewRef.rootNodes) {
298
- state.wrapperEl.appendChild(node);
299
- }
300
- }
301
- // ─── Cleanup ────────────────────────────────────────────────────────────
302
- /** Full cleanup on destroy */
303
- cleanup() {
304
- for (const [, state] of this.slots) {
305
- if (state.viewRef) {
306
- state.viewRef.destroy();
307
- }
308
- if (state.cleanup) {
309
- state.cleanup();
310
- }
311
- if (state.wrapperEl) {
312
- // If the wrapper is a [data-slot] element (part of the component's internal DOM),
313
- // just clear its content rather than removing it from the DOM
314
- if (state.wrapperEl.hasAttribute('data-slot')) {
315
- state.wrapperEl.innerHTML = '';
316
- }
317
- else if (state.wrapperEl.parentNode) {
318
- state.wrapperEl.parentNode.removeChild(state.wrapperEl);
319
- }
320
- }
321
- }
322
- this.slots.clear();
323
- for (const fn of this.eventCleanups) {
324
- fn();
325
- }
326
- this.eventCleanups = [];
327
- }
328
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccSlotsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
329
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.21", type: WccSlotsDirective, isStandalone: true, selector: "[wccSlots]", queries: [{ propertyName: "slotDefs", predicate: WccSlotDef }], ngImport: i0 });
330
- }
331
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccSlotsDirective, decorators: [{
332
- type: Directive,
333
- args: [{
334
- selector: '[wccSlots]',
335
- standalone: true,
336
- }]
337
- }], propDecorators: { slotDefs: [{
338
- type: ContentChildren,
339
- args: [WccSlotDef]
340
- }] } });
341
- // ─── WccEvent — Event Binding Directive ─────────────────────────────────────
342
- /**
343
- * Directive that bridges WCC custom element events to Angular output bindings.
344
- *
345
- * Problem: Angular's `(event-name)="handler($event)"` works on custom elements,
346
- * but `$event` is the raw CustomEvent. The developer must write `$event.detail`
347
- * to get the payload. This is verbose and error-prone.
348
- *
349
- * Solution: This directive listens for CustomEvents on the host element and
350
- * re-emits them as Angular outputs with `$event = event.detail`.
351
- *
352
- * Usage:
353
- * <wcc-counter wccEvent="count-changed" (wccEmit)="onCount($event)"></wcc-counter>
354
- *
355
- * Or for multiple events, use WccEvents (plural) with a comma-separated list:
356
- * <wcc-counter wccEvents="count-changed, value-changed"
357
- * (countChanged)="onCount($event)"
358
- * (valueChanged)="onValue($event)">
359
- * </wcc-counter>
360
- *
361
- * The event name is converted from kebab-case to camelCase for the output:
362
- * 'count-changed' → (countChanged)
363
- * 'value-changed' → (valueChanged)
364
- * 'change' → (change)
365
- */
366
- /**
367
- * Single-event directive: listens for one CustomEvent and emits its detail.
368
- *
369
- * Usage:
370
- * <wcc-counter wccEvent="count-changed" (wccEmit)="handler($event)"></wcc-counter>
371
- */
372
- export class WccEvent {
373
- wccEvent = '';
374
- wccEmit = new EventEmitter();
375
- el = inject(ElementRef);
376
- listener = null;
377
- ngOnInit() {
378
- if (!this.wccEvent)
379
- return;
380
- this.listener = (e) => {
381
- this.wccEmit.emit(e.detail);
382
- };
383
- this.el.nativeElement.addEventListener(this.wccEvent, this.listener);
384
- }
385
- ngOnDestroy() {
386
- if (this.listener && this.wccEvent) {
387
- this.el.nativeElement.removeEventListener(this.wccEvent, this.listener);
388
- }
389
- }
390
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccEvent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
391
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.21", type: WccEvent, isStandalone: true, selector: "[wccEvent]", inputs: { wccEvent: "wccEvent" }, outputs: { wccEmit: "wccEmit" }, ngImport: i0 });
392
- }
393
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccEvent, decorators: [{
394
- type: Directive,
395
- args: [{
396
- selector: '[wccEvent]',
397
- standalone: true,
398
- }]
399
- }], propDecorators: { wccEvent: [{
400
- type: Input
401
- }], wccEmit: [{
402
- type: Output
403
- }] } });
404
- /**
405
- * Event bridging directive: allows using camelCase event bindings on WCC elements.
406
- *
407
- * Without this directive, Angular devs must use kebab-case event names:
408
- * <wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
409
- *
410
- * With this directive, they can use camelCase (more Angular-idiomatic):
411
- * <wcc-counter wccEvents (countChanged)="onCount($event.detail)"></wcc-counter>
412
- *
413
- * The directive listens for kebab-case CustomEvents from the WCC component
414
- * and re-dispatches them with camelCase names so Angular's event binding picks them up.
415
- *
416
- * Event name conversion:
417
- * 'count-changed' → dispatches 'countChanged'
418
- * 'value-changed' → dispatches 'valueChanged'
419
- * 'change' → dispatches 'change' (no conversion needed)
420
- *
421
- * Event discovery:
422
- * - Auto: reads `static __events` from the WCC component class (set by codegen)
423
- * - Manual: pass an explicit array via [wccEvents]="['count-changed', 'value-changed']"
424
- *
425
- * Note: $event is still the CustomEvent — use $event.detail to get the payload.
426
- * This is consistent with how Angular handles all DOM events.
427
- */
428
- export class WccEvents {
429
- /** Optional explicit list of kebab-case event names to bridge */
430
- wccEvents = '';
431
- el = inject(ElementRef);
432
- listeners = [];
433
- ngOnInit() {
434
- const hostEl = this.el.nativeElement;
435
- const tagName = hostEl.tagName.toLowerCase();
436
- if (!tagName.includes('-'))
437
- return;
438
- this.setupEvents(hostEl, tagName);
439
- }
440
- async setupEvents(hostEl, tagName) {
441
- let eventNames;
442
- if (Array.isArray(this.wccEvents) && this.wccEvents.length > 0) {
443
- eventNames = this.wccEvents;
444
- }
445
- else {
446
- // Auto-discover from component metadata
447
- await customElements.whenDefined(tagName);
448
- const ctor = customElements.get(tagName);
449
- eventNames = ctor?.__events || [];
450
- }
451
- if (eventNames.length === 0)
452
- return;
453
- for (const eventName of eventNames) {
454
- // Only bridge events that have hyphens (already camelCase events don't need bridging)
455
- if (!eventName.includes('-'))
456
- continue;
457
- const camelName = eventName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
458
- const listener = (e) => {
459
- // Re-dispatch with camelCase name — Angular's (camelName) binding will catch it
460
- hostEl.dispatchEvent(new CustomEvent(camelName, {
461
- detail: e.detail,
462
- bubbles: false,
463
- cancelable: false,
464
- }));
465
- };
466
- hostEl.addEventListener(eventName, listener);
467
- this.listeners.push([eventName, listener]);
468
- }
469
- }
470
- ngOnDestroy() {
471
- const hostEl = this.el.nativeElement;
472
- for (const [name, listener] of this.listeners) {
473
- hostEl.removeEventListener(name, listener);
474
- }
475
- this.listeners = [];
476
- }
477
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccEvents, deps: [], target: i0.ɵɵFactoryTarget.Directive });
478
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.21", type: WccEvents, isStandalone: true, selector: "[wccEvents]", inputs: { wccEvents: "wccEvents" }, ngImport: i0 });
479
- }
480
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccEvents, decorators: [{
481
- type: Directive,
482
- args: [{
483
- selector: '[wccEvents]',
484
- standalone: true,
485
- }]
486
- }], propDecorators: { wccEvents: [{
487
- type: Input
488
- }] } });
1
+ /**
2
+ * Angular adapter for WCC Scoped Slots and Event Binding.
3
+ *
4
+ * Exports:
5
+ * - WccSlotDef: Auxiliary directive for ng-template[slot]
6
+ * - WccSlotsDirective: Main directive activated via [wccSlots] attribute
7
+ * - WccEvent: Single-event directive (wccEvent="name" + wccEmit output)
8
+ * - WccEvents: Multi-event bridging directive (kebab-case → camelCase)
9
+ * - SlotContext: Interface for template context typing
10
+ *
11
+ * Usage:
12
+ * import { WccSlotsDirective, WccSlotDef, WccEvent, WccEvents } from '@sprlab/wccompiler/adapters/angular';
13
+ *
14
+ * @Component({
15
+ * imports: [WccSlotsDirective, WccSlotDef, WccEvent, WccEvents],
16
+ * schemas: [CUSTOM_ELEMENTS_SCHEMA],
17
+ * template: `
18
+ * <wcc-card wccSlots>
19
+ * <ng-template slot="header"><strong>Header</strong></ng-template>
20
+ * <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
21
+ * </wcc-card>
22
+ *
23
+ * <!-- Event binding option 1: single event with unwrapped detail -->
24
+ * <wcc-counter wccEvent="count-changed" (wccEmit)="onCount($event)"></wcc-counter>
25
+ *
26
+ * <!-- Event binding option 2: camelCase event names -->
27
+ * <wcc-counter wccEvents (countChanged)="onCount($event.detail)"></wcc-counter>
28
+ *
29
+ * <!-- Event binding option 3: standard Angular (always works) -->
30
+ * <wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
31
+ * `
32
+ * })
33
+ *
34
+ * Note: Add the `wccSlots` attribute to any WCC element that uses slots.
35
+ * This is required because Angular AOT cannot evaluate dynamic selectors.
36
+ *
37
+ * @module @sprlab/wccompiler/adapters/angular
38
+ */
39
+ import { Directive, TemplateRef, ElementRef, ViewContainerRef, ChangeDetectorRef, ContentChildren, Output, EventEmitter, inject, Attribute, Input, } from '@angular/core';
40
+ import * as i0 from "@angular/core";
41
+ // ─── WccSlotDef — Auxiliary Directive ───────────────────────────────────────
42
+ /**
43
+ * Auxiliary directive that marks an ng-template as slot content.
44
+ * Captures the TemplateRef and the slot name from the HTML 'slot' attribute.
45
+ *
46
+ * Usage:
47
+ * <ng-template slot="header">...</ng-template>
48
+ * <ng-template slot="stats" let-likes>{{likes}}</ng-template>
49
+ */
50
+ export class WccSlotDef {
51
+ templateRef = inject(TemplateRef);
52
+ slotName;
53
+ constructor(name) {
54
+ this.slotName = name || '';
55
+ }
56
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccSlotDef, deps: [{ token: 'slot', attribute: true }], target: i0.ɵɵFactoryTarget.Directive });
57
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.21", type: WccSlotDef, isStandalone: true, selector: "ng-template[slot]", ngImport: i0 });
58
+ }
59
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccSlotDef, decorators: [{
60
+ type: Directive,
61
+ args: [{
62
+ selector: 'ng-template[slot]',
63
+ standalone: true,
64
+ }]
65
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
66
+ type: Attribute,
67
+ args: ['slot']
68
+ }] }] });
69
+ // ─── WccSlotsDirective — Main Directive ─────────────────────────────────────
70
+ /**
71
+ * Main directive that activates on elements with the [wccSlots] attribute.
72
+ * Classifies ng-template[slot] children as named or scoped slots and manages
73
+ * their lifecycle.
74
+ *
75
+ * Uses a simple attribute selector `[wccSlots]` instead of a dynamic exclusion
76
+ * selector, because Angular AOT cannot evaluate computed selector expressions.
77
+ */
78
+ export class WccSlotsDirective {
79
+ slotDefs;
80
+ el = inject(ElementRef);
81
+ vcr = inject(ViewContainerRef);
82
+ cdr = inject(ChangeDetectorRef);
83
+ slots = new Map();
84
+ eventCleanups = [];
85
+ destroyed = false;
86
+ ngAfterContentInit() {
87
+ // Runtime guard: only proceed for custom elements (tag name contains hyphen)
88
+ if (!this.el.nativeElement.tagName.toLowerCase().includes('-'))
89
+ return;
90
+ // Normalize Angular-style slot attributes: slot-header → slot="header"
91
+ this.normalizeSlotAttributes();
92
+ this.classifyAndInitSlots();
93
+ }
94
+ ngOnDestroy() {
95
+ this.destroyed = true;
96
+ this.cleanup();
97
+ }
98
+ // ─── Slot Attribute Normalization ───────────────────────────────────────
99
+ /**
100
+ * Normalizes Angular-style slot attributes to standard HTML slot attributes.
101
+ * Converts: <div slot-header> → <div slot="header">
102
+ *
103
+ * This enables the Angular ng-content select pattern:
104
+ * <wcc-card wccSlots>
105
+ * <nav slot-header>Title</nav>
106
+ * <span slot-footer>Footer</span>
107
+ * </wcc-card>
108
+ *
109
+ * Skips reserved prefixes: slot-props, slot-template-*
110
+ */
111
+ normalizeSlotAttributes() {
112
+ const hostEl = this.el.nativeElement;
113
+ for (const child of Array.from(hostEl.children)) {
114
+ for (const attr of Array.from(child.attributes)) {
115
+ if (attr.name.startsWith('slot-') &&
116
+ !attr.value &&
117
+ attr.name !== 'slot-props' &&
118
+ !attr.name.startsWith('slot-template-')) {
119
+ const slotName = attr.name.slice(5); // "slot-header" → "header"
120
+ child.removeAttribute(attr.name);
121
+ child.setAttribute('slot', slotName);
122
+ }
123
+ }
124
+ }
125
+ }
126
+ // ─── Classification ─────────────────────────────────────────────────────
127
+ /** Classifies slots using __scopedSlots from the host element and initializes them */
128
+ async classifyAndInitSlots() {
129
+ const hostEl = this.el.nativeElement;
130
+ const tagName = hostEl.tagName.toLowerCase();
131
+ // Wait for the custom element to be defined (ensures the class is upgraded)
132
+ await customElements.whenDefined(tagName);
133
+ if (this.destroyed)
134
+ return;
135
+ const element = hostEl;
136
+ // Read from instance getter or static property
137
+ const scopedNames = element.__scopedSlots
138
+ || (element.constructor && element.constructor.__scopedSlots)
139
+ || [];
140
+ for (const slotDef of this.slotDefs) {
141
+ if (!slotDef.slotName)
142
+ continue;
143
+ if (scopedNames.includes(slotDef.slotName)) {
144
+ this.initScopedSlot(slotDef);
145
+ }
146
+ else {
147
+ this.initNamedSlot(slotDef);
148
+ }
149
+ }
150
+ }
151
+ // ─── Named Slot ─────────────────────────────────────────────────────────
152
+ /** Named Slot: immediate static rendering */
153
+ initNamedSlot(slotDef) {
154
+ const hostEl = this.el.nativeElement;
155
+ // Strategy 1: Find [data-slot] container inside the component's internal DOM
156
+ const dataSlotEl = hostEl.querySelector(`[data-slot="${slotDef.slotName}"]`);
157
+ let wrapper;
158
+ if (dataSlotEl) {
159
+ // Use the data-slot element directly — clear fallback content and insert rendered nodes
160
+ wrapper = dataSlotEl;
161
+ wrapper.innerHTML = '';
162
+ }
163
+ else {
164
+ // Strategy 2: Fallback for Shadow DOM / native <slot> elements
165
+ wrapper = document.createElement('div');
166
+ wrapper.setAttribute('slot', slotDef.slotName);
167
+ wrapper.style.display = 'contents';
168
+ hostEl.appendChild(wrapper);
169
+ }
170
+ const viewRef = this.vcr.createEmbeddedView(slotDef.templateRef);
171
+ for (const node of viewRef.rootNodes) {
172
+ wrapper.appendChild(node);
173
+ }
174
+ this.slots.set(slotDef.slotName, {
175
+ type: 'named',
176
+ slotDef,
177
+ viewRef,
178
+ cleanup: null,
179
+ wrapperEl: wrapper,
180
+ context: null,
181
+ });
182
+ this.cdr.detectChanges();
183
+ }
184
+ // ─── Scoped Slot ────────────────────────────────────────────────────────
185
+ /** Scoped Slot: registration + reactive rendering */
186
+ initScopedSlot(slotDef) {
187
+ const hostEl = this.el.nativeElement;
188
+ const state = {
189
+ type: 'scoped',
190
+ slotDef,
191
+ viewRef: null,
192
+ cleanup: null,
193
+ wrapperEl: null,
194
+ context: null,
195
+ };
196
+ this.slots.set(slotDef.slotName, state);
197
+ // Register renderer
198
+ const element = hostEl;
199
+ if (typeof element.registerSlotRenderer === 'function') {
200
+ state.cleanup = element.registerSlotRenderer(slotDef.slotName, (props) => this.renderSlot(slotDef.slotName, props));
201
+ }
202
+ else {
203
+ // Fallback: listen for wcc:slot-update event
204
+ const handler = (e) => {
205
+ if (e.detail?.slot === slotDef.slotName) {
206
+ this.renderSlot(slotDef.slotName, e.detail.props);
207
+ }
208
+ };
209
+ hostEl.addEventListener('wcc:slot-update', handler);
210
+ this.eventCleanups.push(() => hostEl.removeEventListener('wcc:slot-update', handler));
211
+ }
212
+ }
213
+ // ─── Context Construction ───────────────────────────────────────────────
214
+ /**
215
+ * Builds the Angular context for createEmbeddedView.
216
+ *
217
+ * Rules:
218
+ * - 0 props: $implicit = undefined
219
+ * - 1 prop: $implicit = that single value, plus the named prop key
220
+ * - N props (N > 1): $implicit = full props object, plus all named props
221
+ */
222
+ buildContext(props) {
223
+ const keys = Object.keys(props);
224
+ if (keys.length === 0) {
225
+ return { $implicit: undefined };
226
+ }
227
+ if (keys.length === 1) {
228
+ return { $implicit: props[keys[0]], ...props };
229
+ }
230
+ return { $implicit: props, ...props };
231
+ }
232
+ // ─── Render Slot ────────────────────────────────────────────────────────
233
+ /** Creates or updates the EmbeddedView of a scoped slot */
234
+ renderSlot(slotName, props) {
235
+ const state = this.slots.get(slotName);
236
+ if (!state || this.destroyed)
237
+ return;
238
+ if (props == null) {
239
+ if (state.viewRef) {
240
+ state.viewRef.destroy();
241
+ state.viewRef = null;
242
+ }
243
+ return;
244
+ }
245
+ const context = this.buildContext(props);
246
+ state.context = context;
247
+ if (state.viewRef) {
248
+ // Update existing view context
249
+ Object.assign(state.viewRef.context, context);
250
+ state.viewRef.markForCheck();
251
+ // Re-insert nodes to reflect updated content (Angular doesn't auto-update DOM for detached views)
252
+ if (state.wrapperEl) {
253
+ state.wrapperEl.innerHTML = '';
254
+ for (const node of state.viewRef.rootNodes) {
255
+ state.wrapperEl.appendChild(node);
256
+ }
257
+ }
258
+ }
259
+ else {
260
+ state.viewRef = this.vcr.createEmbeddedView(state.slotDef.templateRef, context);
261
+ this.insertView(slotName, state);
262
+ }
263
+ this.cdr.detectChanges();
264
+ }
265
+ // ─── DOM Insertion ──────────────────────────────────────────────────────
266
+ /**
267
+ * Inserts view root nodes into the custom element's DOM.
268
+ *
269
+ * Strategy:
270
+ * 1. Look for a [data-slot="slotName"] element inside the component (non-Shadow DOM)
271
+ * → clear its content and insert the rendered nodes there
272
+ * 2. Fallback: append a wrapper <div slot="slotName"> to the host (Shadow DOM / native slots)
273
+ */
274
+ insertView(slotName, state) {
275
+ if (!state.viewRef)
276
+ return;
277
+ const hostEl = this.el.nativeElement;
278
+ // Strategy 1: Find [data-slot] container inside the component's internal DOM
279
+ const dataSlotEl = hostEl.querySelector(`[data-slot="${slotName}"]`);
280
+ if (dataSlotEl) {
281
+ // Use the data-slot element as the wrapper (no extra div needed)
282
+ state.wrapperEl = dataSlotEl;
283
+ state.wrapperEl.innerHTML = '';
284
+ for (const node of state.viewRef.rootNodes) {
285
+ state.wrapperEl.appendChild(node);
286
+ }
287
+ return;
288
+ }
289
+ // Strategy 2: Fallback for Shadow DOM / native <slot> elements
290
+ if (!state.wrapperEl) {
291
+ state.wrapperEl = document.createElement('div');
292
+ state.wrapperEl.setAttribute('slot', slotName);
293
+ state.wrapperEl.style.display = 'contents';
294
+ hostEl.appendChild(state.wrapperEl);
295
+ }
296
+ state.wrapperEl.innerHTML = '';
297
+ for (const node of state.viewRef.rootNodes) {
298
+ state.wrapperEl.appendChild(node);
299
+ }
300
+ }
301
+ // ─── Cleanup ────────────────────────────────────────────────────────────
302
+ /** Full cleanup on destroy */
303
+ cleanup() {
304
+ for (const [, state] of this.slots) {
305
+ if (state.viewRef) {
306
+ state.viewRef.destroy();
307
+ }
308
+ if (state.cleanup) {
309
+ state.cleanup();
310
+ }
311
+ if (state.wrapperEl) {
312
+ // If the wrapper is a [data-slot] element (part of the component's internal DOM),
313
+ // just clear its content rather than removing it from the DOM
314
+ if (state.wrapperEl.hasAttribute('data-slot')) {
315
+ state.wrapperEl.innerHTML = '';
316
+ }
317
+ else if (state.wrapperEl.parentNode) {
318
+ state.wrapperEl.parentNode.removeChild(state.wrapperEl);
319
+ }
320
+ }
321
+ }
322
+ this.slots.clear();
323
+ for (const fn of this.eventCleanups) {
324
+ fn();
325
+ }
326
+ this.eventCleanups = [];
327
+ }
328
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccSlotsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
329
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.21", type: WccSlotsDirective, isStandalone: true, selector: "[wccSlots]", queries: [{ propertyName: "slotDefs", predicate: WccSlotDef }], ngImport: i0 });
330
+ }
331
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccSlotsDirective, decorators: [{
332
+ type: Directive,
333
+ args: [{
334
+ selector: '[wccSlots]',
335
+ standalone: true,
336
+ }]
337
+ }], propDecorators: { slotDefs: [{
338
+ type: ContentChildren,
339
+ args: [WccSlotDef]
340
+ }] } });
341
+ // ─── WccEvent — Event Binding Directive ─────────────────────────────────────
342
+ /**
343
+ * Directive that bridges WCC custom element events to Angular output bindings.
344
+ *
345
+ * Problem: Angular's `(event-name)="handler($event)"` works on custom elements,
346
+ * but `$event` is the raw CustomEvent. The developer must write `$event.detail`
347
+ * to get the payload. This is verbose and error-prone.
348
+ *
349
+ * Solution: This directive listens for CustomEvents on the host element and
350
+ * re-emits them as Angular outputs with `$event = event.detail`.
351
+ *
352
+ * Usage:
353
+ * <wcc-counter wccEvent="count-changed" (wccEmit)="onCount($event)"></wcc-counter>
354
+ *
355
+ * Or for multiple events, use WccEvents (plural) with a comma-separated list:
356
+ * <wcc-counter wccEvents="count-changed, value-changed"
357
+ * (countChanged)="onCount($event)"
358
+ * (valueChanged)="onValue($event)">
359
+ * </wcc-counter>
360
+ *
361
+ * The event name is converted from kebab-case to camelCase for the output:
362
+ * 'count-changed' → (countChanged)
363
+ * 'value-changed' → (valueChanged)
364
+ * 'change' → (change)
365
+ */
366
+ /**
367
+ * Single-event directive: listens for one CustomEvent and emits its detail.
368
+ *
369
+ * Usage:
370
+ * <wcc-counter wccEvent="count-changed" (wccEmit)="handler($event)"></wcc-counter>
371
+ */
372
+ export class WccEvent {
373
+ wccEvent = '';
374
+ wccEmit = new EventEmitter();
375
+ el = inject(ElementRef);
376
+ listener = null;
377
+ ngOnInit() {
378
+ if (!this.wccEvent)
379
+ return;
380
+ this.listener = (e) => {
381
+ this.wccEmit.emit(e.detail);
382
+ };
383
+ this.el.nativeElement.addEventListener(this.wccEvent, this.listener);
384
+ }
385
+ ngOnDestroy() {
386
+ if (this.listener && this.wccEvent) {
387
+ this.el.nativeElement.removeEventListener(this.wccEvent, this.listener);
388
+ }
389
+ }
390
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccEvent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
391
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.21", type: WccEvent, isStandalone: true, selector: "[wccEvent]", inputs: { wccEvent: "wccEvent" }, outputs: { wccEmit: "wccEmit" }, ngImport: i0 });
392
+ }
393
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccEvent, decorators: [{
394
+ type: Directive,
395
+ args: [{
396
+ selector: '[wccEvent]',
397
+ standalone: true,
398
+ }]
399
+ }], propDecorators: { wccEvent: [{
400
+ type: Input
401
+ }], wccEmit: [{
402
+ type: Output
403
+ }] } });
404
+ /**
405
+ * Event bridging directive: allows using camelCase event bindings on WCC elements.
406
+ *
407
+ * Without this directive, Angular devs must use kebab-case event names:
408
+ * <wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
409
+ *
410
+ * With this directive, they can use camelCase (more Angular-idiomatic):
411
+ * <wcc-counter wccEvents (countChanged)="onCount($event.detail)"></wcc-counter>
412
+ *
413
+ * The directive listens for kebab-case CustomEvents from the WCC component
414
+ * and re-dispatches them with camelCase names so Angular's event binding picks them up.
415
+ *
416
+ * Event name conversion:
417
+ * 'count-changed' → dispatches 'countChanged'
418
+ * 'value-changed' → dispatches 'valueChanged'
419
+ * 'change' → dispatches 'change' (no conversion needed)
420
+ *
421
+ * Event discovery:
422
+ * - Auto: reads `static __events` from the WCC component class (set by codegen)
423
+ * - Manual: pass an explicit array via [wccEvents]="['count-changed', 'value-changed']"
424
+ *
425
+ * Note: $event is still the CustomEvent — use $event.detail to get the payload.
426
+ * This is consistent with how Angular handles all DOM events.
427
+ */
428
+ export class WccEvents {
429
+ /** Optional explicit list of kebab-case event names to bridge */
430
+ wccEvents = '';
431
+ el = inject(ElementRef);
432
+ listeners = [];
433
+ ngOnInit() {
434
+ const hostEl = this.el.nativeElement;
435
+ const tagName = hostEl.tagName.toLowerCase();
436
+ if (!tagName.includes('-'))
437
+ return;
438
+ this.setupEvents(hostEl, tagName);
439
+ }
440
+ async setupEvents(hostEl, tagName) {
441
+ let eventNames;
442
+ if (Array.isArray(this.wccEvents) && this.wccEvents.length > 0) {
443
+ eventNames = this.wccEvents;
444
+ }
445
+ else {
446
+ // Auto-discover from component metadata
447
+ await customElements.whenDefined(tagName);
448
+ const ctor = customElements.get(tagName);
449
+ eventNames = ctor?.__events || [];
450
+ }
451
+ if (eventNames.length === 0)
452
+ return;
453
+ for (const eventName of eventNames) {
454
+ // Only bridge events that have hyphens (already camelCase events don't need bridging)
455
+ if (!eventName.includes('-'))
456
+ continue;
457
+ const camelName = eventName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
458
+ const listener = (e) => {
459
+ // Re-dispatch with camelCase name — Angular's (camelName) binding will catch it
460
+ hostEl.dispatchEvent(new CustomEvent(camelName, {
461
+ detail: e.detail,
462
+ bubbles: false,
463
+ cancelable: false,
464
+ }));
465
+ };
466
+ hostEl.addEventListener(eventName, listener);
467
+ this.listeners.push([eventName, listener]);
468
+ }
469
+ }
470
+ ngOnDestroy() {
471
+ const hostEl = this.el.nativeElement;
472
+ for (const [name, listener] of this.listeners) {
473
+ hostEl.removeEventListener(name, listener);
474
+ }
475
+ this.listeners = [];
476
+ }
477
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccEvents, deps: [], target: i0.ɵɵFactoryTarget.Directive });
478
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.21", type: WccEvents, isStandalone: true, selector: "[wccEvents]", inputs: { wccEvents: "wccEvents" }, ngImport: i0 });
479
+ }
480
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccEvents, decorators: [{
481
+ type: Directive,
482
+ args: [{
483
+ selector: '[wccEvents]',
484
+ standalone: true,
485
+ }]
486
+ }], propDecorators: { wccEvents: [{
487
+ type: Input
488
+ }] } });