@sprlab/wccompiler 0.9.5 → 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.
- package/adapters/angular-compiled/angular.d.ts +16 -4
- package/adapters/angular-compiled/angular.mjs +299 -0
- package/adapters/angular.js +46 -34
- package/adapters/angular.ts +71 -18
- package/bin/wcc.js +0 -0
- package/package.json +5 -2
|
@@ -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.
|
|
@@ -66,7 +69,7 @@ export declare class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
66
69
|
private classifyAndInitSlots;
|
|
67
70
|
/** Named Slot: immediate static rendering */
|
|
68
71
|
private initNamedSlot;
|
|
69
|
-
/** Scoped Slot:
|
|
72
|
+
/** Scoped Slot: registration + reactive rendering */
|
|
70
73
|
private initScopedSlot;
|
|
71
74
|
/**
|
|
72
75
|
* Builds the Angular context for createEmbeddedView.
|
|
@@ -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
|
-
/**
|
|
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/adapters/angular.js
CHANGED
|
@@ -5,38 +5,50 @@
|
|
|
5
5
|
*
|
|
6
6
|
* ANGULAR INTEGRATION:
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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.
|
package/adapters/angular.ts
CHANGED
|
@@ -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
|
|
128
|
-
const
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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:
|
|
169
|
-
private
|
|
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.
|
|
282
|
+
this.cdr.detectChanges();
|
|
256
283
|
}
|
|
257
284
|
|
|
258
285
|
// ─── DOM Insertion ──────────────────────────────────────────────────────
|
|
259
286
|
|
|
260
|
-
/**
|
|
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
|
|
290
|
-
|
|
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.
|
|
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": {
|
|
@@ -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":
|
|
12
|
+
"./adapters/angular": {
|
|
13
|
+
"types": "./adapters/angular-compiled/angular.d.ts",
|
|
14
|
+
"default": "./adapters/angular-compiled/angular.mjs"
|
|
15
|
+
},
|
|
13
16
|
"./adapters/react": "./adapters/react.js"
|
|
14
17
|
},
|
|
15
18
|
"bin": {
|