@sprlab/wccompiler 0.9.6 → 0.9.7

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.
@@ -25,7 +25,8 @@
25
25
  *
26
26
  * @module @sprlab/wccompiler/adapters/angular
27
27
  */
28
- import { QueryList, AfterContentInit, OnDestroy } from '@angular/core';
28
+ import { TemplateRef, QueryList, AfterContentInit, OnDestroy } from '@angular/core';
29
+ import * as i0 from "@angular/core";
29
30
  /** Context object passed to createEmbeddedView for scoped slots */
30
31
  export interface SlotContext {
31
32
  $implicit: any;
@@ -40,9 +41,11 @@ export interface SlotContext {
40
41
  * <ng-template slot="stats" let-likes>{{likes}}</ng-template>
41
42
  */
42
43
  export declare class WccSlotDef {
43
- readonly templateRef: any;
44
+ readonly templateRef: TemplateRef<any>;
44
45
  readonly slotName: string;
45
46
  constructor(name: string | null);
47
+ static ɵfac: i0.ɵɵFactoryDeclaration<WccSlotDef, [{ attribute: "slot"; }]>;
48
+ static ɵdir: i0.ɵɵDirectiveDeclaration<WccSlotDef, "ng-template[slot]", never, {}, {}, never, never, true, never>;
46
49
  }
47
50
  /**
48
51
  * Main directive that activates on elements with the [wccSlots] attribute.
@@ -79,8 +82,17 @@ export declare class WccSlotsDirective implements AfterContentInit, OnDestroy {
79
82
  buildContext(props: Record<string, any>): SlotContext;
80
83
  /** Creates or updates the EmbeddedView of a scoped slot */
81
84
  private renderSlot;
82
- /** Inserts view root nodes into the custom element's DOM via a wrapper div */
85
+ /**
86
+ * Inserts view root nodes into the custom element's DOM.
87
+ *
88
+ * Strategy:
89
+ * 1. Look for a [data-slot="slotName"] element inside the component (non-Shadow DOM)
90
+ * → clear its content and insert the rendered nodes there
91
+ * 2. Fallback: append a wrapper <div slot="slotName"> to the host (Shadow DOM / native slots)
92
+ */
83
93
  private insertView;
84
94
  /** Full cleanup on destroy */
85
95
  private cleanup;
96
+ static ɵfac: i0.ɵɵFactoryDeclaration<WccSlotsDirective, never>;
97
+ static ɵdir: i0.ɵɵDirectiveDeclaration<WccSlotsDirective, "[wccSlots]", never, {}, {}, ["slotDefs"], never, true, never>;
86
98
  }
@@ -0,0 +1,299 @@
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 { Directive, TemplateRef, ElementRef, ViewContainerRef, ChangeDetectorRef, ContentChildren, inject, Attribute, } from '@angular/core';
29
+ import * as i0 from "@angular/core";
30
+ // ─── WccSlotDef — Auxiliary Directive ───────────────────────────────────────
31
+ /**
32
+ * Auxiliary directive that marks an ng-template as slot content.
33
+ * Captures the TemplateRef and the slot name from the HTML 'slot' attribute.
34
+ *
35
+ * Usage:
36
+ * <ng-template slot="header">...</ng-template>
37
+ * <ng-template slot="stats" let-likes>{{likes}}</ng-template>
38
+ */
39
+ export class WccSlotDef {
40
+ templateRef = inject(TemplateRef);
41
+ slotName;
42
+ constructor(name) {
43
+ this.slotName = name || '';
44
+ }
45
+ 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 });
46
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.21", type: WccSlotDef, isStandalone: true, selector: "ng-template[slot]", ngImport: i0 });
47
+ }
48
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccSlotDef, decorators: [{
49
+ type: Directive,
50
+ args: [{
51
+ selector: 'ng-template[slot]',
52
+ standalone: true,
53
+ }]
54
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
55
+ type: Attribute,
56
+ args: ['slot']
57
+ }] }] });
58
+ // ─── WccSlotsDirective — Main Directive ─────────────────────────────────────
59
+ /**
60
+ * Main directive that activates on elements with the [wccSlots] attribute.
61
+ * Classifies ng-template[slot] children as named or scoped slots and manages
62
+ * their lifecycle.
63
+ *
64
+ * Uses a simple attribute selector `[wccSlots]` instead of a dynamic exclusion
65
+ * selector, because Angular AOT cannot evaluate computed selector expressions.
66
+ */
67
+ export class WccSlotsDirective {
68
+ slotDefs;
69
+ el = inject(ElementRef);
70
+ vcr = inject(ViewContainerRef);
71
+ cdr = inject(ChangeDetectorRef);
72
+ slots = new Map();
73
+ eventCleanups = [];
74
+ destroyed = false;
75
+ ngAfterContentInit() {
76
+ // Runtime guard: only proceed for custom elements (tag name contains hyphen)
77
+ if (!this.el.nativeElement.tagName.toLowerCase().includes('-'))
78
+ return;
79
+ this.classifyAndInitSlots();
80
+ }
81
+ ngOnDestroy() {
82
+ this.destroyed = true;
83
+ this.cleanup();
84
+ }
85
+ // ─── Classification ─────────────────────────────────────────────────────
86
+ /** Classifies slots using __scopedSlots from the host element and initializes them */
87
+ async classifyAndInitSlots() {
88
+ const hostEl = this.el.nativeElement;
89
+ const tagName = hostEl.tagName.toLowerCase();
90
+ // Wait for the custom element to be defined (ensures the class is upgraded)
91
+ await customElements.whenDefined(tagName);
92
+ if (this.destroyed)
93
+ return;
94
+ const element = hostEl;
95
+ // Read from instance getter or static property
96
+ const scopedNames = element.__scopedSlots
97
+ || (element.constructor && element.constructor.__scopedSlots)
98
+ || [];
99
+ for (const slotDef of this.slotDefs) {
100
+ if (!slotDef.slotName)
101
+ continue;
102
+ if (scopedNames.includes(slotDef.slotName)) {
103
+ this.initScopedSlot(slotDef);
104
+ }
105
+ else {
106
+ this.initNamedSlot(slotDef);
107
+ }
108
+ }
109
+ }
110
+ // ─── Named Slot ─────────────────────────────────────────────────────────
111
+ /** Named Slot: immediate static rendering */
112
+ initNamedSlot(slotDef) {
113
+ const hostEl = this.el.nativeElement;
114
+ // Strategy 1: Find [data-slot] container inside the component's internal DOM
115
+ const dataSlotEl = hostEl.querySelector(`[data-slot="${slotDef.slotName}"]`);
116
+ let wrapper;
117
+ if (dataSlotEl) {
118
+ // Use the data-slot element directly — clear fallback content and insert rendered nodes
119
+ wrapper = dataSlotEl;
120
+ wrapper.innerHTML = '';
121
+ }
122
+ else {
123
+ // Strategy 2: Fallback for Shadow DOM / native <slot> elements
124
+ wrapper = document.createElement('div');
125
+ wrapper.setAttribute('slot', slotDef.slotName);
126
+ wrapper.style.display = 'contents';
127
+ hostEl.appendChild(wrapper);
128
+ }
129
+ const viewRef = this.vcr.createEmbeddedView(slotDef.templateRef);
130
+ for (const node of viewRef.rootNodes) {
131
+ wrapper.appendChild(node);
132
+ }
133
+ this.slots.set(slotDef.slotName, {
134
+ type: 'named',
135
+ slotDef,
136
+ viewRef,
137
+ cleanup: null,
138
+ wrapperEl: wrapper,
139
+ context: null,
140
+ });
141
+ this.cdr.detectChanges();
142
+ }
143
+ // ─── Scoped Slot ────────────────────────────────────────────────────────
144
+ /** Scoped Slot: registration + reactive rendering */
145
+ initScopedSlot(slotDef) {
146
+ const hostEl = this.el.nativeElement;
147
+ const state = {
148
+ type: 'scoped',
149
+ slotDef,
150
+ viewRef: null,
151
+ cleanup: null,
152
+ wrapperEl: null,
153
+ context: null,
154
+ };
155
+ this.slots.set(slotDef.slotName, state);
156
+ // Register renderer
157
+ const element = hostEl;
158
+ if (typeof element.registerSlotRenderer === 'function') {
159
+ state.cleanup = element.registerSlotRenderer(slotDef.slotName, (props) => this.renderSlot(slotDef.slotName, props));
160
+ }
161
+ else {
162
+ // Fallback: listen for wcc:slot-update event
163
+ const handler = (e) => {
164
+ if (e.detail?.slot === slotDef.slotName) {
165
+ this.renderSlot(slotDef.slotName, e.detail.props);
166
+ }
167
+ };
168
+ hostEl.addEventListener('wcc:slot-update', handler);
169
+ this.eventCleanups.push(() => hostEl.removeEventListener('wcc:slot-update', handler));
170
+ }
171
+ }
172
+ // ─── Context Construction ───────────────────────────────────────────────
173
+ /**
174
+ * Builds the Angular context for createEmbeddedView.
175
+ *
176
+ * Rules:
177
+ * - 0 props: $implicit = undefined
178
+ * - 1 prop: $implicit = that single value, plus the named prop key
179
+ * - N props (N > 1): $implicit = full props object, plus all named props
180
+ */
181
+ buildContext(props) {
182
+ const keys = Object.keys(props);
183
+ if (keys.length === 0) {
184
+ return { $implicit: undefined };
185
+ }
186
+ if (keys.length === 1) {
187
+ return { $implicit: props[keys[0]], ...props };
188
+ }
189
+ return { $implicit: props, ...props };
190
+ }
191
+ // ─── Render Slot ────────────────────────────────────────────────────────
192
+ /** Creates or updates the EmbeddedView of a scoped slot */
193
+ renderSlot(slotName, props) {
194
+ const state = this.slots.get(slotName);
195
+ if (!state || this.destroyed)
196
+ return;
197
+ if (props == null) {
198
+ if (state.viewRef) {
199
+ state.viewRef.destroy();
200
+ state.viewRef = null;
201
+ }
202
+ return;
203
+ }
204
+ const context = this.buildContext(props);
205
+ state.context = context;
206
+ if (state.viewRef) {
207
+ // Update existing view context
208
+ Object.assign(state.viewRef.context, context);
209
+ state.viewRef.markForCheck();
210
+ // Re-insert nodes to reflect updated content (Angular doesn't auto-update DOM for detached views)
211
+ if (state.wrapperEl) {
212
+ state.wrapperEl.innerHTML = '';
213
+ for (const node of state.viewRef.rootNodes) {
214
+ state.wrapperEl.appendChild(node);
215
+ }
216
+ }
217
+ }
218
+ else {
219
+ state.viewRef = this.vcr.createEmbeddedView(state.slotDef.templateRef, context);
220
+ this.insertView(slotName, state);
221
+ }
222
+ this.cdr.detectChanges();
223
+ }
224
+ // ─── DOM Insertion ──────────────────────────────────────────────────────
225
+ /**
226
+ * Inserts view root nodes into the custom element's DOM.
227
+ *
228
+ * Strategy:
229
+ * 1. Look for a [data-slot="slotName"] element inside the component (non-Shadow DOM)
230
+ * → clear its content and insert the rendered nodes there
231
+ * 2. Fallback: append a wrapper <div slot="slotName"> to the host (Shadow DOM / native slots)
232
+ */
233
+ insertView(slotName, state) {
234
+ if (!state.viewRef)
235
+ return;
236
+ const hostEl = this.el.nativeElement;
237
+ // Strategy 1: Find [data-slot] container inside the component's internal DOM
238
+ const dataSlotEl = hostEl.querySelector(`[data-slot="${slotName}"]`);
239
+ if (dataSlotEl) {
240
+ // Use the data-slot element as the wrapper (no extra div needed)
241
+ state.wrapperEl = dataSlotEl;
242
+ state.wrapperEl.innerHTML = '';
243
+ for (const node of state.viewRef.rootNodes) {
244
+ state.wrapperEl.appendChild(node);
245
+ }
246
+ return;
247
+ }
248
+ // Strategy 2: Fallback for Shadow DOM / native <slot> elements
249
+ if (!state.wrapperEl) {
250
+ state.wrapperEl = document.createElement('div');
251
+ state.wrapperEl.setAttribute('slot', slotName);
252
+ state.wrapperEl.style.display = 'contents';
253
+ hostEl.appendChild(state.wrapperEl);
254
+ }
255
+ state.wrapperEl.innerHTML = '';
256
+ for (const node of state.viewRef.rootNodes) {
257
+ state.wrapperEl.appendChild(node);
258
+ }
259
+ }
260
+ // ─── Cleanup ────────────────────────────────────────────────────────────
261
+ /** Full cleanup on destroy */
262
+ cleanup() {
263
+ for (const [, state] of this.slots) {
264
+ if (state.viewRef) {
265
+ state.viewRef.destroy();
266
+ }
267
+ if (state.cleanup) {
268
+ state.cleanup();
269
+ }
270
+ if (state.wrapperEl) {
271
+ // If the wrapper is a [data-slot] element (part of the component's internal DOM),
272
+ // just clear its content rather than removing it from the DOM
273
+ if (state.wrapperEl.hasAttribute('data-slot')) {
274
+ state.wrapperEl.innerHTML = '';
275
+ }
276
+ else if (state.wrapperEl.parentNode) {
277
+ state.wrapperEl.parentNode.removeChild(state.wrapperEl);
278
+ }
279
+ }
280
+ }
281
+ this.slots.clear();
282
+ for (const fn of this.eventCleanups) {
283
+ fn();
284
+ }
285
+ this.eventCleanups = [];
286
+ }
287
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccSlotsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
288
+ 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 });
289
+ }
290
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccSlotsDirective, decorators: [{
291
+ type: Directive,
292
+ args: [{
293
+ selector: '[wccSlots]',
294
+ standalone: true,
295
+ }]
296
+ }], propDecorators: { slotDefs: [{
297
+ type: ContentChildren,
298
+ args: [WccSlotDef]
299
+ }] } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
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": {
@@ -11,7 +11,7 @@
11
11
  "./adapters/vue": "./adapters/vue.js",
12
12
  "./adapters/angular": {
13
13
  "types": "./adapters/angular-compiled/angular.d.ts",
14
- "default": "./adapters/angular.ts"
14
+ "default": "./adapters/angular-compiled/angular.mjs"
15
15
  },
16
16
  "./adapters/react": "./adapters/react.js"
17
17
  },