@sprlab/wccompiler 0.9.2 → 0.9.3
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.js +70 -2
- package/adapters/angular.ts +310 -0
- package/lib/codegen.js +46 -4
- package/package.json +1 -1
package/adapters/angular.js
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Angular adapter for WCC defineModel
|
|
2
|
+
* Angular adapter for WCC (defineModel + Scoped Slots).
|
|
3
|
+
*
|
|
4
|
+
* This module provides Angular integration for WCC components:
|
|
5
|
+
*
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
* SCOPED SLOTS (Native Angular Syntax)
|
|
8
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
9
|
+
*
|
|
10
|
+
* For native Angular scoped slot support, use the TypeScript directives:
|
|
11
|
+
*
|
|
12
|
+
* import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular';
|
|
13
|
+
*
|
|
14
|
+
* Setup:
|
|
15
|
+
* @Component({
|
|
16
|
+
* imports: [WccSlotsDirective, WccSlotDef],
|
|
17
|
+
* schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
18
|
+
* template: `...`
|
|
19
|
+
* })
|
|
20
|
+
*
|
|
21
|
+
* The directives auto-activate on custom elements (tags with hyphen).
|
|
22
|
+
* No [wccSlots] attribute is needed on the host element.
|
|
23
|
+
*
|
|
24
|
+
* Slot declaration uses ng-template[slot] syntax:
|
|
25
|
+
*
|
|
26
|
+
* <!-- Named slot (static content, no let-*) -->
|
|
27
|
+
* <wcc-card>
|
|
28
|
+
* <ng-template slot="header"><strong>My Header</strong></ng-template>
|
|
29
|
+
* </wcc-card>
|
|
30
|
+
*
|
|
31
|
+
* <!-- Scoped slot (reactive data via let-*) -->
|
|
32
|
+
* <wcc-card>
|
|
33
|
+
* <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
|
|
34
|
+
* </wcc-card>
|
|
35
|
+
*
|
|
36
|
+
* <!-- Multiple props in scoped slot -->
|
|
37
|
+
* <wcc-card>
|
|
38
|
+
* <ng-template slot="details" let-data let-likes="likes" let-total="total">
|
|
39
|
+
* {{ likes }}/{{ total }}
|
|
40
|
+
* </ng-template>
|
|
41
|
+
* </wcc-card>
|
|
42
|
+
*
|
|
43
|
+
* How it works:
|
|
44
|
+
* - WccSlotDef captures the TemplateRef and slot name from the 'slot' attribute
|
|
45
|
+
* - WccSlotsDirective classifies slots as named or scoped using __scopedSlots
|
|
46
|
+
* - Named slots render immediately into <div slot="name" style="display:contents">
|
|
47
|
+
* - Scoped slots register a renderer via element.registerSlotRenderer()
|
|
48
|
+
* - When slot props change, the renderer updates the Angular EmbeddedView
|
|
49
|
+
* - Compatible with OnPush change detection strategy
|
|
50
|
+
*
|
|
51
|
+
* The directive source is in adapters/angular.ts (TypeScript with Angular decorators).
|
|
52
|
+
*
|
|
53
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
54
|
+
* BACKWARD COMPATIBILITY: slot-template-* (Token Replacement)
|
|
55
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
56
|
+
*
|
|
57
|
+
* The legacy slot-template-* attribute approach continues to work without
|
|
58
|
+
* importing the directives:
|
|
59
|
+
*
|
|
60
|
+
* <wcc-list>
|
|
61
|
+
* <div slot-template-item="<li>{%item%}</li>"></div>
|
|
62
|
+
* </wcc-list>
|
|
63
|
+
*
|
|
64
|
+
* When WccSlotsDirective IS imported, ng-template[slot] takes priority over
|
|
65
|
+
* slot-template-* for the same slot name. Slots not covered by ng-template
|
|
66
|
+
* continue using the token replacement path.
|
|
67
|
+
*
|
|
68
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
69
|
+
* defineModel (Two-Way Binding)
|
|
70
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
3
71
|
*
|
|
4
72
|
* The WCC component already emits `propNameChange` directly from _modelSet,
|
|
5
73
|
* so Angular's [(prop)] banana-box syntax works WITHOUT this adapter.
|
|
@@ -9,7 +77,7 @@
|
|
|
9
77
|
* 2. The ControlValueAccessor guide for ngModel support
|
|
10
78
|
*
|
|
11
79
|
* Setup (Angular):
|
|
12
|
-
* // No adapter import needed! Just use CUSTOM_ELEMENTS_SCHEMA:
|
|
80
|
+
* // No adapter import needed for [(prop)]! Just use CUSTOM_ELEMENTS_SCHEMA:
|
|
13
81
|
* import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
|
|
14
82
|
* @Component({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })
|
|
15
83
|
*
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Angular adapter for WCC Scoped Slots.
|
|
3
|
+
*
|
|
4
|
+
* Exports:
|
|
5
|
+
* - WccSlotDef: Auxiliary directive for ng-template[slot]
|
|
6
|
+
* - WccSlotsDirective: Main directive that auto-activates on custom elements
|
|
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>
|
|
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
|
+
* @module @sprlab/wccompiler/adapters/angular
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
Directive,
|
|
28
|
+
TemplateRef,
|
|
29
|
+
ElementRef,
|
|
30
|
+
ViewContainerRef,
|
|
31
|
+
ChangeDetectorRef,
|
|
32
|
+
ContentChildren,
|
|
33
|
+
QueryList,
|
|
34
|
+
EmbeddedViewRef,
|
|
35
|
+
AfterContentInit,
|
|
36
|
+
OnDestroy,
|
|
37
|
+
inject,
|
|
38
|
+
Attribute,
|
|
39
|
+
} from '@angular/core';
|
|
40
|
+
|
|
41
|
+
// ─── Interfaces ─────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/** Context object passed to createEmbeddedView for scoped slots */
|
|
44
|
+
export interface SlotContext {
|
|
45
|
+
$implicit: any;
|
|
46
|
+
[key: string]: any;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type SlotType = 'named' | 'scoped';
|
|
50
|
+
|
|
51
|
+
interface SlotState {
|
|
52
|
+
type: SlotType;
|
|
53
|
+
slotDef: WccSlotDef;
|
|
54
|
+
viewRef: EmbeddedViewRef<SlotContext> | null;
|
|
55
|
+
cleanup: (() => void) | null;
|
|
56
|
+
wrapperEl: HTMLElement | null;
|
|
57
|
+
context: SlotContext | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── WccSlotDef — Auxiliary Directive ───────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Auxiliary directive that marks an ng-template as slot content.
|
|
64
|
+
* Captures the TemplateRef and the slot name from the HTML 'slot' attribute.
|
|
65
|
+
*
|
|
66
|
+
* Usage:
|
|
67
|
+
* <ng-template slot="header">...</ng-template>
|
|
68
|
+
* <ng-template slot="stats" let-likes>{{likes}}</ng-template>
|
|
69
|
+
*/
|
|
70
|
+
@Directive({
|
|
71
|
+
selector: 'ng-template[slot]',
|
|
72
|
+
standalone: true,
|
|
73
|
+
})
|
|
74
|
+
export class WccSlotDef {
|
|
75
|
+
public readonly templateRef = inject<TemplateRef<any>>(TemplateRef);
|
|
76
|
+
public readonly slotName: string;
|
|
77
|
+
|
|
78
|
+
constructor(@Attribute('slot') name: string | null) {
|
|
79
|
+
this.slotName = name || '';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── WccSlotsDirective — Main Directive ─────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Exclusion selector: lists all standard HTML elements.
|
|
87
|
+
* Custom elements (which MUST contain a hyphen) are not excluded.
|
|
88
|
+
*/
|
|
89
|
+
const STANDARD_ELEMENTS = 'div,span,p,a,button,input,form,section,article,header,footer,nav,main,ul,ol,li,table,tr,td,th,thead,tbody,tfoot,img,h1,h2,h3,h4,h5,h6,label,select,textarea,option,fieldset,legend,details,summary,dialog,slot,template,canvas,video,audio,source,iframe,pre,code,blockquote,hr,br,strong,em,small,sub,sup,mark,del,ins,figure,figcaption,picture,svg,math,body,html,head,script,style,link,meta,title,base,col,colgroup,caption,abbr,address,area,aside,b,bdi,bdo,cite,data,dd,dfn,dl,dt,i,kbd,map,meter,noscript,output,progress,q,rp,rt,ruby,s,samp,time,u,var,wbr';
|
|
90
|
+
|
|
91
|
+
/** Build the exclusion selector string */
|
|
92
|
+
const EXCLUSION_SELECTOR = STANDARD_ELEMENTS.split(',').map(t => `:not(${t})`).join('');
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Main directive that auto-activates on custom elements (tags with hyphen).
|
|
96
|
+
* Classifies ng-template[slot] children as named or scoped slots and manages
|
|
97
|
+
* their lifecycle.
|
|
98
|
+
*/
|
|
99
|
+
@Directive({
|
|
100
|
+
selector: EXCLUSION_SELECTOR,
|
|
101
|
+
standalone: true,
|
|
102
|
+
})
|
|
103
|
+
export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
104
|
+
@ContentChildren(WccSlotDef) slotDefs!: QueryList<WccSlotDef>;
|
|
105
|
+
|
|
106
|
+
private el = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
107
|
+
private vcr = inject(ViewContainerRef);
|
|
108
|
+
private cdr = inject(ChangeDetectorRef);
|
|
109
|
+
|
|
110
|
+
private slots = new Map<string, SlotState>();
|
|
111
|
+
private eventCleanups: (() => void)[] = [];
|
|
112
|
+
private destroyed = false;
|
|
113
|
+
|
|
114
|
+
ngAfterContentInit(): void {
|
|
115
|
+
// Runtime guard: only proceed for custom elements (tag name contains hyphen)
|
|
116
|
+
if (!this.el.nativeElement.tagName.toLowerCase().includes('-')) return;
|
|
117
|
+
|
|
118
|
+
this.classifyAndInitSlots();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
ngOnDestroy(): void {
|
|
122
|
+
this.destroyed = true;
|
|
123
|
+
this.cleanup();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Classification ─────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/** Classifies slots using __scopedSlots from the host element and initializes them */
|
|
129
|
+
private classifyAndInitSlots(): void {
|
|
130
|
+
const element = this.el.nativeElement as any;
|
|
131
|
+
const scopedNames: string[] = element.__scopedSlots || [];
|
|
132
|
+
|
|
133
|
+
for (const slotDef of this.slotDefs) {
|
|
134
|
+
// Ignore templates with empty slot name
|
|
135
|
+
if (!slotDef.slotName) continue;
|
|
136
|
+
|
|
137
|
+
if (scopedNames.includes(slotDef.slotName)) {
|
|
138
|
+
this.initScopedSlot(slotDef);
|
|
139
|
+
} else {
|
|
140
|
+
this.initNamedSlot(slotDef);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Named Slot ─────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/** Named Slot: immediate static rendering */
|
|
148
|
+
private initNamedSlot(slotDef: WccSlotDef): void {
|
|
149
|
+
const hostEl = this.el.nativeElement;
|
|
150
|
+
const wrapper = document.createElement('div');
|
|
151
|
+
wrapper.setAttribute('slot', slotDef.slotName);
|
|
152
|
+
wrapper.style.display = 'contents';
|
|
153
|
+
|
|
154
|
+
const viewRef = this.vcr.createEmbeddedView(slotDef.templateRef);
|
|
155
|
+
for (const node of viewRef.rootNodes) {
|
|
156
|
+
wrapper.appendChild(node);
|
|
157
|
+
}
|
|
158
|
+
hostEl.appendChild(wrapper);
|
|
159
|
+
|
|
160
|
+
this.slots.set(slotDef.slotName, {
|
|
161
|
+
type: 'named',
|
|
162
|
+
slotDef,
|
|
163
|
+
viewRef,
|
|
164
|
+
cleanup: null,
|
|
165
|
+
wrapperEl: wrapper,
|
|
166
|
+
context: null,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Scoped Slot ────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/** Scoped Slot: async registration + reactive rendering */
|
|
173
|
+
private async initScopedSlot(slotDef: WccSlotDef): Promise<void> {
|
|
174
|
+
const hostEl = this.el.nativeElement;
|
|
175
|
+
const tagName = hostEl.tagName.toLowerCase();
|
|
176
|
+
|
|
177
|
+
// Wait for the custom element to be defined
|
|
178
|
+
await customElements.whenDefined(tagName);
|
|
179
|
+
if (this.destroyed) return;
|
|
180
|
+
|
|
181
|
+
const state: SlotState = {
|
|
182
|
+
type: 'scoped',
|
|
183
|
+
slotDef,
|
|
184
|
+
viewRef: null,
|
|
185
|
+
cleanup: null,
|
|
186
|
+
wrapperEl: null,
|
|
187
|
+
context: null,
|
|
188
|
+
};
|
|
189
|
+
this.slots.set(slotDef.slotName, state);
|
|
190
|
+
|
|
191
|
+
// Register renderer
|
|
192
|
+
const element = hostEl as any;
|
|
193
|
+
if (typeof element.registerSlotRenderer === 'function') {
|
|
194
|
+
state.cleanup = element.registerSlotRenderer(
|
|
195
|
+
slotDef.slotName,
|
|
196
|
+
(props: Record<string, any>) => this.renderSlot(slotDef.slotName, props)
|
|
197
|
+
);
|
|
198
|
+
} else {
|
|
199
|
+
// Fallback: listen for wcc:slot-update event
|
|
200
|
+
const handler = (e: CustomEvent) => {
|
|
201
|
+
if (e.detail?.slot === slotDef.slotName) {
|
|
202
|
+
this.renderSlot(slotDef.slotName, e.detail.props);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
hostEl.addEventListener('wcc:slot-update', handler as EventListener);
|
|
206
|
+
this.eventCleanups.push(() =>
|
|
207
|
+
hostEl.removeEventListener('wcc:slot-update', handler as EventListener)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Context Construction ───────────────────────────────────────────────
|
|
213
|
+
|
|
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: Record<string, any>): SlotContext {
|
|
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
|
+
|
|
233
|
+
// ─── Render Slot ────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
/** Creates or updates the EmbeddedView of a scoped slot */
|
|
236
|
+
private renderSlot(slotName: string, props: Record<string, any> | null): void {
|
|
237
|
+
const state = this.slots.get(slotName);
|
|
238
|
+
if (!state || this.destroyed) return;
|
|
239
|
+
|
|
240
|
+
// Props null/undefined: clear the view
|
|
241
|
+
if (props == null) {
|
|
242
|
+
if (state.viewRef) {
|
|
243
|
+
state.viewRef.destroy();
|
|
244
|
+
state.viewRef = null;
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const context = this.buildContext(props);
|
|
250
|
+
state.context = context;
|
|
251
|
+
|
|
252
|
+
if (state.viewRef) {
|
|
253
|
+
// Update existing context
|
|
254
|
+
Object.assign(state.viewRef.context, context);
|
|
255
|
+
state.viewRef.markForCheck();
|
|
256
|
+
} else {
|
|
257
|
+
// Create new view
|
|
258
|
+
state.viewRef = this.vcr.createEmbeddedView(state.slotDef.templateRef, context);
|
|
259
|
+
this.insertView(slotName, state);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.cdr.markForCheck();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── DOM Insertion ──────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
/** Inserts view root nodes into the custom element's DOM via a wrapper div */
|
|
268
|
+
private insertView(slotName: string, state: SlotState): void {
|
|
269
|
+
if (!state.viewRef) return;
|
|
270
|
+
const hostEl = this.el.nativeElement;
|
|
271
|
+
|
|
272
|
+
if (!state.wrapperEl) {
|
|
273
|
+
state.wrapperEl = document.createElement('div');
|
|
274
|
+
state.wrapperEl.setAttribute('slot', slotName);
|
|
275
|
+
state.wrapperEl.style.display = 'contents';
|
|
276
|
+
hostEl.appendChild(state.wrapperEl);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Clear previous wrapper content and append new nodes
|
|
280
|
+
state.wrapperEl.innerHTML = '';
|
|
281
|
+
for (const node of state.viewRef.rootNodes) {
|
|
282
|
+
state.wrapperEl.appendChild(node);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── Cleanup ────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/** Full cleanup on destroy */
|
|
289
|
+
private cleanup(): void {
|
|
290
|
+
// Destroy views, invoke cleanup functions, remove wrappers
|
|
291
|
+
for (const [, state] of this.slots) {
|
|
292
|
+
if (state.viewRef) {
|
|
293
|
+
state.viewRef.destroy();
|
|
294
|
+
}
|
|
295
|
+
if (state.cleanup) {
|
|
296
|
+
state.cleanup();
|
|
297
|
+
}
|
|
298
|
+
if (state.wrapperEl && state.wrapperEl.parentNode) {
|
|
299
|
+
state.wrapperEl.parentNode.removeChild(state.wrapperEl);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
this.slots.clear();
|
|
303
|
+
|
|
304
|
+
// Remove event listeners
|
|
305
|
+
for (const fn of this.eventCleanups) {
|
|
306
|
+
fn();
|
|
307
|
+
}
|
|
308
|
+
this.eventCleanups = [];
|
|
309
|
+
}
|
|
310
|
+
}
|
package/lib/codegen.js
CHANGED
|
@@ -962,10 +962,24 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
962
962
|
lines.push('');
|
|
963
963
|
}
|
|
964
964
|
|
|
965
|
+
// Static __scopedSlots array (lists slot names with reactive props)
|
|
966
|
+
const scopedSlotNames = slots.filter(s => s.name && s.slotProps.length > 0).map(s => s.name);
|
|
967
|
+
if (scopedSlotNames.length > 0) {
|
|
968
|
+
const scopedArr = scopedSlotNames.map(n => `'${n}'`).join(', ');
|
|
969
|
+
lines.push(` static __scopedSlots = [${scopedArr}];`);
|
|
970
|
+
lines.push('');
|
|
971
|
+
}
|
|
972
|
+
|
|
965
973
|
// Constructor — reactive state only (no DOM manipulation per Custom Elements spec)
|
|
966
974
|
lines.push(' constructor() {');
|
|
967
975
|
lines.push(' super();');
|
|
968
976
|
|
|
977
|
+
// Scoped slot storage initialization
|
|
978
|
+
if (scopedSlotNames.length > 0) {
|
|
979
|
+
lines.push(' this.__slotRenderers = {};');
|
|
980
|
+
lines.push(' this.__slotProps = {};');
|
|
981
|
+
}
|
|
982
|
+
|
|
969
983
|
// Prop signal initialization (BEFORE user signals)
|
|
970
984
|
for (const p of propDefs) {
|
|
971
985
|
lines.push(` this._s_${p.name} = __signal(${p.default});`);
|
|
@@ -1277,11 +1291,20 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1277
1291
|
lines.push(` if (this.__slotTpl_${s.name}) {`);
|
|
1278
1292
|
lines.push(' __effect(() => {');
|
|
1279
1293
|
lines.push(` const __props = { ${propsObj} };`);
|
|
1280
|
-
|
|
1281
|
-
lines.push(
|
|
1282
|
-
|
|
1294
|
+
// Task 3.1: Store current props in __slotProps
|
|
1295
|
+
lines.push(` this.__slotProps['${s.name}'] = __props;`);
|
|
1296
|
+
// Task 3.2: Emit wcc:slot-update event
|
|
1297
|
+
lines.push(` this.dispatchEvent(new CustomEvent('wcc:slot-update', { detail: { slot: '${s.name}', props: __props }, bubbles: false }));`);
|
|
1298
|
+
// Task 3.3: Check for registered renderer, skip token replacement if present
|
|
1299
|
+
lines.push(` if (this.__slotRenderers && this.__slotRenderers['${s.name}']) {`);
|
|
1300
|
+
lines.push(` this.__slotRenderers['${s.name}'](__props);`);
|
|
1301
|
+
lines.push(' } else {');
|
|
1302
|
+
lines.push(` let __html = this.__slotTpl_${s.name};`);
|
|
1303
|
+
lines.push(" for (const [k, v] of Object.entries(__props)) {");
|
|
1304
|
+
lines.push(` __html = __html.replace(new RegExp('(?:\\\\{\\\\{|\\\\{%)\\\\s*' + k + '(\\\\(\\\\))?\\\\s*(?:\\\\}\\\\}|%\\\\})', 'g'), v ?? '');`);
|
|
1305
|
+
lines.push(' }');
|
|
1306
|
+
lines.push(` this.${s.varName}.innerHTML = __html;`);
|
|
1283
1307
|
lines.push(' }');
|
|
1284
|
-
lines.push(` this.${s.varName}.innerHTML = __html;`);
|
|
1285
1308
|
lines.push(' });');
|
|
1286
1309
|
lines.push(' }');
|
|
1287
1310
|
}
|
|
@@ -1769,6 +1792,25 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1769
1792
|
lines.push('');
|
|
1770
1793
|
}
|
|
1771
1794
|
|
|
1795
|
+
// __scopedSlots instance getter and registerSlotRenderer (if scoped slots exist)
|
|
1796
|
+
if (scopedSlotNames.length > 0) {
|
|
1797
|
+
lines.push(' get __scopedSlots() { return this.constructor.__scopedSlots || []; }');
|
|
1798
|
+
lines.push('');
|
|
1799
|
+
lines.push(' registerSlotRenderer(slotName, callback) {');
|
|
1800
|
+
lines.push(' if (!this.__slotRenderers) this.__slotRenderers = {};');
|
|
1801
|
+
lines.push(' this.__slotRenderers[slotName] = callback;');
|
|
1802
|
+
lines.push(' if (this.__slotProps && this.__slotProps[slotName]) {');
|
|
1803
|
+
lines.push(' callback(this.__slotProps[slotName]);');
|
|
1804
|
+
lines.push(' }');
|
|
1805
|
+
lines.push(' return () => {');
|
|
1806
|
+
lines.push(' if (this.__slotRenderers) {');
|
|
1807
|
+
lines.push(' delete this.__slotRenderers[slotName];');
|
|
1808
|
+
lines.push(' }');
|
|
1809
|
+
lines.push(' };');
|
|
1810
|
+
lines.push(' }');
|
|
1811
|
+
lines.push('');
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1772
1814
|
// User methods (prefixed with _)
|
|
1773
1815
|
for (const m of methods) {
|
|
1774
1816
|
const body = transformMethodBody(m.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
package/package.json
CHANGED