@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
|
-
/**
|
|
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.
|
|
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.
|
|
14
|
+
"default": "./adapters/angular-compiled/angular.mjs"
|
|
15
15
|
},
|
|
16
16
|
"./adapters/react": "./adapters/react.js"
|
|
17
17
|
},
|