@sprlab/wccompiler 0.9.5 → 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.
@@ -66,7 +66,7 @@ export declare class WccSlotsDirective implements AfterContentInit, OnDestroy {
66
66
  private classifyAndInitSlots;
67
67
  /** Named Slot: immediate static rendering */
68
68
  private initNamedSlot;
69
- /** Scoped Slot: async registration + reactive rendering */
69
+ /** Scoped Slot: registration + reactive rendering */
70
70
  private initScopedSlot;
71
71
  /**
72
72
  * Builds the Angular context for createEmbeddedView.
@@ -5,38 +5,50 @@
5
5
  *
6
6
  * ANGULAR INTEGRATION:
7
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.
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.
38
54
  */
39
-
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.
@@ -123,9 +123,19 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
123
123
  // ─── Classification ─────────────────────────────────────────────────────
124
124
 
125
125
  /** Classifies slots using __scopedSlots from the host element and initializes them */
126
- private classifyAndInitSlots(): void {
127
- const element = this.el.nativeElement as any;
128
- 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
+ || [];
129
139
 
130
140
  for (const slotDef of this.slotDefs) {
131
141
  if (!slotDef.slotName) continue;
@@ -143,15 +153,27 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
143
153
  /** Named Slot: immediate static rendering */
144
154
  private initNamedSlot(slotDef: WccSlotDef): void {
145
155
  const hostEl = this.el.nativeElement;
146
- const wrapper = document.createElement('div');
147
- wrapper.setAttribute('slot', slotDef.slotName);
148
- 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
+ }
149
172
 
150
173
  const viewRef = this.vcr.createEmbeddedView(slotDef.templateRef);
151
174
  for (const node of viewRef.rootNodes) {
152
175
  wrapper.appendChild(node);
153
176
  }
154
- hostEl.appendChild(wrapper);
155
177
 
156
178
  this.slots.set(slotDef.slotName, {
157
179
  type: 'named',
@@ -161,18 +183,15 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
161
183
  wrapperEl: wrapper,
162
184
  context: null,
163
185
  });
186
+
187
+ this.cdr.detectChanges();
164
188
  }
165
189
 
166
190
  // ─── Scoped Slot ────────────────────────────────────────────────────────
167
191
 
168
- /** Scoped Slot: async registration + reactive rendering */
169
- private async initScopedSlot(slotDef: WccSlotDef): Promise<void> {
192
+ /** Scoped Slot: registration + reactive rendering */
193
+ private initScopedSlot(slotDef: WccSlotDef): void {
170
194
  const hostEl = this.el.nativeElement;
171
- const tagName = hostEl.tagName.toLowerCase();
172
-
173
- // Wait for the custom element to be defined
174
- await customElements.whenDefined(tagName);
175
- if (this.destroyed) return;
176
195
 
177
196
  const state: SlotState = {
178
197
  type: 'scoped',
@@ -245,23 +264,51 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
245
264
  state.context = context;
246
265
 
247
266
  if (state.viewRef) {
267
+ // Update existing view context
248
268
  Object.assign(state.viewRef.context, context);
249
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
+ }
250
277
  } else {
251
278
  state.viewRef = this.vcr.createEmbeddedView(state.slotDef.templateRef, context);
252
279
  this.insertView(slotName, state);
253
280
  }
254
281
 
255
- this.cdr.markForCheck();
282
+ this.cdr.detectChanges();
256
283
  }
257
284
 
258
285
  // ─── DOM Insertion ──────────────────────────────────────────────────────
259
286
 
260
- /** 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
+ */
261
295
  private insertView(slotName: string, state: SlotState): void {
262
296
  if (!state.viewRef) return;
263
297
  const hostEl = this.el.nativeElement;
264
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
265
312
  if (!state.wrapperEl) {
266
313
  state.wrapperEl = document.createElement('div');
267
314
  state.wrapperEl.setAttribute('slot', slotName);
@@ -286,8 +333,14 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
286
333
  if (state.cleanup) {
287
334
  state.cleanup();
288
335
  }
289
- if (state.wrapperEl && state.wrapperEl.parentNode) {
290
- 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
+ }
291
344
  }
292
345
  }
293
346
  this.slots.clear();
package/bin/wcc.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.9.5",
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": {