@sprlab/wccompiler 0.9.4 → 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.
- package/adapters/angular-compiled/angular.d.ts +86 -0
- package/adapters/angular.js +50 -142
- package/adapters/angular.ts +81 -38
- package/bin/wcc.js +0 -0
- package/lib/codegen.js +7 -6
- package/package.json +5 -2
|
@@ -0,0 +1,86 @@
|
|
|
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 { QueryList, AfterContentInit, OnDestroy } from '@angular/core';
|
|
29
|
+
/** Context object passed to createEmbeddedView for scoped slots */
|
|
30
|
+
export interface SlotContext {
|
|
31
|
+
$implicit: any;
|
|
32
|
+
[key: string]: any;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Auxiliary directive that marks an ng-template as slot content.
|
|
36
|
+
* Captures the TemplateRef and the slot name from the HTML 'slot' attribute.
|
|
37
|
+
*
|
|
38
|
+
* Usage:
|
|
39
|
+
* <ng-template slot="header">...</ng-template>
|
|
40
|
+
* <ng-template slot="stats" let-likes>{{likes}}</ng-template>
|
|
41
|
+
*/
|
|
42
|
+
export declare class WccSlotDef {
|
|
43
|
+
readonly templateRef: any;
|
|
44
|
+
readonly slotName: string;
|
|
45
|
+
constructor(name: string | null);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Main directive that activates on elements with the [wccSlots] attribute.
|
|
49
|
+
* Classifies ng-template[slot] children as named or scoped slots and manages
|
|
50
|
+
* their lifecycle.
|
|
51
|
+
*
|
|
52
|
+
* Uses a simple attribute selector `[wccSlots]` instead of a dynamic exclusion
|
|
53
|
+
* selector, because Angular AOT cannot evaluate computed selector expressions.
|
|
54
|
+
*/
|
|
55
|
+
export declare class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
56
|
+
slotDefs: QueryList<WccSlotDef>;
|
|
57
|
+
private el;
|
|
58
|
+
private vcr;
|
|
59
|
+
private cdr;
|
|
60
|
+
private slots;
|
|
61
|
+
private eventCleanups;
|
|
62
|
+
private destroyed;
|
|
63
|
+
ngAfterContentInit(): void;
|
|
64
|
+
ngOnDestroy(): void;
|
|
65
|
+
/** Classifies slots using __scopedSlots from the host element and initializes them */
|
|
66
|
+
private classifyAndInitSlots;
|
|
67
|
+
/** Named Slot: immediate static rendering */
|
|
68
|
+
private initNamedSlot;
|
|
69
|
+
/** Scoped Slot: registration + reactive rendering */
|
|
70
|
+
private initScopedSlot;
|
|
71
|
+
/**
|
|
72
|
+
* Builds the Angular context for createEmbeddedView.
|
|
73
|
+
*
|
|
74
|
+
* Rules:
|
|
75
|
+
* - 0 props: $implicit = undefined
|
|
76
|
+
* - 1 prop: $implicit = that single value, plus the named prop key
|
|
77
|
+
* - N props (N > 1): $implicit = full props object, plus all named props
|
|
78
|
+
*/
|
|
79
|
+
buildContext(props: Record<string, any>): SlotContext;
|
|
80
|
+
/** Creates or updates the EmbeddedView of a scoped slot */
|
|
81
|
+
private renderSlot;
|
|
82
|
+
/** Inserts view root nodes into the custom element's DOM via a wrapper div */
|
|
83
|
+
private insertView;
|
|
84
|
+
/** Full cleanup on destroy */
|
|
85
|
+
private cleanup;
|
|
86
|
+
}
|
package/adapters/angular.js
CHANGED
|
@@ -1,146 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Angular adapter for WCC
|
|
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
|
-
* ═══════════════════════════════════════════════════════════════════════════════
|
|
71
|
-
*
|
|
72
|
-
* The WCC component already emits `propNameChange` directly from _modelSet,
|
|
73
|
-
* so Angular's [(prop)] banana-box syntax works WITHOUT this adapter.
|
|
74
|
-
*
|
|
75
|
-
* This file is kept for:
|
|
76
|
-
* 1. Documentation of the Angular integration approach
|
|
77
|
-
* 2. The ControlValueAccessor guide for ngModel support
|
|
78
|
-
*
|
|
79
|
-
* Setup (Angular):
|
|
80
|
-
* // No adapter import needed for [(prop)]! Just use CUSTOM_ELEMENTS_SCHEMA:
|
|
81
|
-
* import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
|
|
82
|
-
* @Component({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })
|
|
83
|
-
*
|
|
84
|
-
* Usage:
|
|
85
|
-
* <wcc-input [(value)]="text"></wcc-input>
|
|
86
|
-
* <wcc-counter [(count)]="myCount"></wcc-counter>
|
|
87
|
-
*
|
|
88
|
-
* How it works:
|
|
89
|
-
* Angular's [(prop)] expands to [prop]="value" (propChange)="value = $event.detail"
|
|
90
|
-
* WCC _modelSet emits propNameChange CustomEvent with detail=newValue
|
|
91
|
-
* Angular picks it up automatically — no adapter needed.
|
|
2
|
+
* Angular adapter for WCC custom elements.
|
|
92
3
|
*
|
|
93
4
|
* @module @sprlab/wccompiler/adapters/angular
|
|
5
|
+
*
|
|
6
|
+
* ANGULAR INTEGRATION:
|
|
7
|
+
*
|
|
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.
|
|
94
54
|
*/
|
|
95
|
-
|
|
96
|
-
// ── ControlValueAccessor for ngModel/ReactiveForms ──────────────────
|
|
97
|
-
// Angular's ngModel requires a ControlValueAccessor to bridge form controls.
|
|
98
|
-
// Copy this into your Angular project as a .ts file:
|
|
99
|
-
//
|
|
100
|
-
// ```ts
|
|
101
|
-
// import { Directive, ElementRef, forwardRef, HostListener } from '@angular/core';
|
|
102
|
-
// import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
103
|
-
//
|
|
104
|
-
// @Directive({
|
|
105
|
-
// selector: '[wccModel]',
|
|
106
|
-
// providers: [{
|
|
107
|
-
// provide: NG_VALUE_ACCESSOR,
|
|
108
|
-
// useExisting: forwardRef(() => WccValueAccessor),
|
|
109
|
-
// multi: true
|
|
110
|
-
// }]
|
|
111
|
-
// })
|
|
112
|
-
// export class WccValueAccessor implements ControlValueAccessor {
|
|
113
|
-
// private onChange: (value: any) => void = () => {};
|
|
114
|
-
// private onTouched: () => void = () => {};
|
|
115
|
-
//
|
|
116
|
-
// constructor(private el: ElementRef<HTMLElement>) {}
|
|
117
|
-
//
|
|
118
|
-
// writeValue(value: any): void {
|
|
119
|
-
// if (value != null) {
|
|
120
|
-
// this.el.nativeElement.setAttribute('value', String(value));
|
|
121
|
-
// } else {
|
|
122
|
-
// this.el.nativeElement.removeAttribute('value');
|
|
123
|
-
// }
|
|
124
|
-
// }
|
|
125
|
-
//
|
|
126
|
-
// registerOnChange(fn: (value: any) => void): void {
|
|
127
|
-
// this.onChange = fn;
|
|
128
|
-
// }
|
|
129
|
-
//
|
|
130
|
-
// registerOnTouched(fn: () => void): void {
|
|
131
|
-
// this.onTouched = fn;
|
|
132
|
-
// }
|
|
133
|
-
//
|
|
134
|
-
// @HostListener('wcc:model', ['$event'])
|
|
135
|
-
// onModelChange(event: CustomEvent): void {
|
|
136
|
-
// if (event.detail && event.detail.prop === 'value') {
|
|
137
|
-
// this.onChange(event.detail.value);
|
|
138
|
-
// }
|
|
139
|
-
// }
|
|
140
|
-
//
|
|
141
|
-
// @HostListener('blur')
|
|
142
|
-
// onBlur(): void {
|
|
143
|
-
// this.onTouched();
|
|
144
|
-
// }
|
|
145
|
-
// }
|
|
146
|
-
// ```
|
package/adapters/angular.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Exports:
|
|
5
5
|
* - WccSlotDef: Auxiliary directive for ng-template[slot]
|
|
6
|
-
* - WccSlotsDirective: Main directive
|
|
6
|
+
* - WccSlotsDirective: Main directive activated via [wccSlots] attribute
|
|
7
7
|
* - SlotContext: Interface for template context typing
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
@@ -13,13 +13,16 @@
|
|
|
13
13
|
* imports: [WccSlotsDirective, WccSlotDef],
|
|
14
14
|
* schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
15
15
|
* template: `
|
|
16
|
-
* <wcc-card>
|
|
16
|
+
* <wcc-card wccSlots>
|
|
17
17
|
* <ng-template slot="header"><strong>Header</strong></ng-template>
|
|
18
18
|
* <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
|
|
19
19
|
* </wcc-card>
|
|
20
20
|
* `
|
|
21
21
|
* })
|
|
22
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
|
+
*
|
|
23
26
|
* @module @sprlab/wccompiler/adapters/angular
|
|
24
27
|
*/
|
|
25
28
|
|
|
@@ -83,21 +86,15 @@ export class WccSlotDef {
|
|
|
83
86
|
// ─── WccSlotsDirective — Main Directive ─────────────────────────────────────
|
|
84
87
|
|
|
85
88
|
/**
|
|
86
|
-
*
|
|
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).
|
|
89
|
+
* Main directive that activates on elements with the [wccSlots] attribute.
|
|
96
90
|
* Classifies ng-template[slot] children as named or scoped slots and manages
|
|
97
91
|
* their lifecycle.
|
|
92
|
+
*
|
|
93
|
+
* Uses a simple attribute selector `[wccSlots]` instead of a dynamic exclusion
|
|
94
|
+
* selector, because Angular AOT cannot evaluate computed selector expressions.
|
|
98
95
|
*/
|
|
99
96
|
@Directive({
|
|
100
|
-
selector:
|
|
97
|
+
selector: '[wccSlots]',
|
|
101
98
|
standalone: true,
|
|
102
99
|
})
|
|
103
100
|
export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
@@ -126,12 +123,21 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
126
123
|
// ─── Classification ─────────────────────────────────────────────────────
|
|
127
124
|
|
|
128
125
|
/** Classifies slots using __scopedSlots from the host element and initializes them */
|
|
129
|
-
private classifyAndInitSlots(): void {
|
|
130
|
-
const
|
|
131
|
-
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
|
+
|| [];
|
|
132
139
|
|
|
133
140
|
for (const slotDef of this.slotDefs) {
|
|
134
|
-
// Ignore templates with empty slot name
|
|
135
141
|
if (!slotDef.slotName) continue;
|
|
136
142
|
|
|
137
143
|
if (scopedNames.includes(slotDef.slotName)) {
|
|
@@ -147,15 +153,27 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
147
153
|
/** Named Slot: immediate static rendering */
|
|
148
154
|
private initNamedSlot(slotDef: WccSlotDef): void {
|
|
149
155
|
const hostEl = this.el.nativeElement;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
}
|
|
153
172
|
|
|
154
173
|
const viewRef = this.vcr.createEmbeddedView(slotDef.templateRef);
|
|
155
174
|
for (const node of viewRef.rootNodes) {
|
|
156
175
|
wrapper.appendChild(node);
|
|
157
176
|
}
|
|
158
|
-
hostEl.appendChild(wrapper);
|
|
159
177
|
|
|
160
178
|
this.slots.set(slotDef.slotName, {
|
|
161
179
|
type: 'named',
|
|
@@ -165,18 +183,15 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
165
183
|
wrapperEl: wrapper,
|
|
166
184
|
context: null,
|
|
167
185
|
});
|
|
186
|
+
|
|
187
|
+
this.cdr.detectChanges();
|
|
168
188
|
}
|
|
169
189
|
|
|
170
190
|
// ─── Scoped Slot ────────────────────────────────────────────────────────
|
|
171
191
|
|
|
172
|
-
/** Scoped Slot:
|
|
173
|
-
private
|
|
192
|
+
/** Scoped Slot: registration + reactive rendering */
|
|
193
|
+
private initScopedSlot(slotDef: WccSlotDef): void {
|
|
174
194
|
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
195
|
|
|
181
196
|
const state: SlotState = {
|
|
182
197
|
type: 'scoped',
|
|
@@ -237,7 +252,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
237
252
|
const state = this.slots.get(slotName);
|
|
238
253
|
if (!state || this.destroyed) return;
|
|
239
254
|
|
|
240
|
-
// Props null/undefined: clear the view
|
|
241
255
|
if (props == null) {
|
|
242
256
|
if (state.viewRef) {
|
|
243
257
|
state.viewRef.destroy();
|
|
@@ -250,25 +264,51 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
250
264
|
state.context = context;
|
|
251
265
|
|
|
252
266
|
if (state.viewRef) {
|
|
253
|
-
// Update existing context
|
|
267
|
+
// Update existing view context
|
|
254
268
|
Object.assign(state.viewRef.context, context);
|
|
255
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
|
+
}
|
|
256
277
|
} else {
|
|
257
|
-
// Create new view
|
|
258
278
|
state.viewRef = this.vcr.createEmbeddedView(state.slotDef.templateRef, context);
|
|
259
279
|
this.insertView(slotName, state);
|
|
260
280
|
}
|
|
261
281
|
|
|
262
|
-
this.cdr.
|
|
282
|
+
this.cdr.detectChanges();
|
|
263
283
|
}
|
|
264
284
|
|
|
265
285
|
// ─── DOM Insertion ──────────────────────────────────────────────────────
|
|
266
286
|
|
|
267
|
-
/**
|
|
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
|
+
*/
|
|
268
295
|
private insertView(slotName: string, state: SlotState): void {
|
|
269
296
|
if (!state.viewRef) return;
|
|
270
297
|
const hostEl = this.el.nativeElement;
|
|
271
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
|
|
272
312
|
if (!state.wrapperEl) {
|
|
273
313
|
state.wrapperEl = document.createElement('div');
|
|
274
314
|
state.wrapperEl.setAttribute('slot', slotName);
|
|
@@ -276,7 +316,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
276
316
|
hostEl.appendChild(state.wrapperEl);
|
|
277
317
|
}
|
|
278
318
|
|
|
279
|
-
// Clear previous wrapper content and append new nodes
|
|
280
319
|
state.wrapperEl.innerHTML = '';
|
|
281
320
|
for (const node of state.viewRef.rootNodes) {
|
|
282
321
|
state.wrapperEl.appendChild(node);
|
|
@@ -287,7 +326,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
287
326
|
|
|
288
327
|
/** Full cleanup on destroy */
|
|
289
328
|
private cleanup(): void {
|
|
290
|
-
// Destroy views, invoke cleanup functions, remove wrappers
|
|
291
329
|
for (const [, state] of this.slots) {
|
|
292
330
|
if (state.viewRef) {
|
|
293
331
|
state.viewRef.destroy();
|
|
@@ -295,13 +333,18 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
295
333
|
if (state.cleanup) {
|
|
296
334
|
state.cleanup();
|
|
297
335
|
}
|
|
298
|
-
if (state.wrapperEl
|
|
299
|
-
|
|
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
|
+
}
|
|
300
344
|
}
|
|
301
345
|
}
|
|
302
346
|
this.slots.clear();
|
|
303
347
|
|
|
304
|
-
// Remove event listeners
|
|
305
348
|
for (const fn of this.eventCleanups) {
|
|
306
349
|
fn();
|
|
307
350
|
}
|
package/bin/wcc.js
CHANGED
|
File without changes
|
package/lib/codegen.js
CHANGED
|
@@ -1288,17 +1288,19 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1288
1288
|
const ref = slotPropRef(sp.source, signalNames, computedNames, propNames);
|
|
1289
1289
|
return `${sp.prop}: ${ref}`;
|
|
1290
1290
|
}).join(', ');
|
|
1291
|
-
|
|
1291
|
+
// Scoped slot effect: always compute props and notify renderers
|
|
1292
|
+
// The effect runs regardless of whether a template was provided (Angular uses registerSlotRenderer)
|
|
1292
1293
|
lines.push(' __effect(() => {');
|
|
1293
1294
|
lines.push(` const __props = { ${propsObj} };`);
|
|
1294
|
-
//
|
|
1295
|
+
// Store current props for late-registering renderers
|
|
1295
1296
|
lines.push(` this.__slotProps['${s.name}'] = __props;`);
|
|
1296
|
-
//
|
|
1297
|
+
// Emit wcc:slot-update event
|
|
1297
1298
|
lines.push(` this.dispatchEvent(new CustomEvent('wcc:slot-update', { detail: { slot: '${s.name}', props: __props }, bubbles: false }));`);
|
|
1298
|
-
//
|
|
1299
|
+
// Check for registered renderer (Angular directive)
|
|
1299
1300
|
lines.push(` if (this.__slotRenderers && this.__slotRenderers['${s.name}']) {`);
|
|
1300
1301
|
lines.push(` this.__slotRenderers['${s.name}'](__props);`);
|
|
1301
|
-
lines.push(
|
|
1302
|
+
lines.push(` } else if (this.__slotTpl_${s.name}) {`);
|
|
1303
|
+
// Fallback: template-based token replacement (WCC-to-WCC, Vue, React)
|
|
1302
1304
|
lines.push(` let __html = this.__slotTpl_${s.name};`);
|
|
1303
1305
|
lines.push(" for (const [k, v] of Object.entries(__props)) {");
|
|
1304
1306
|
lines.push(` __html = __html.replace(new RegExp('(?:\\\\{\\\\{|\\\\{%)\\\\s*' + k + '(\\\\(\\\\))?\\\\s*(?:\\\\}\\\\}|%\\\\})', 'g'), v ?? '');`);
|
|
@@ -1306,7 +1308,6 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1306
1308
|
lines.push(` this.${s.varName}.innerHTML = __html;`);
|
|
1307
1309
|
lines.push(' }');
|
|
1308
1310
|
lines.push(' });');
|
|
1309
|
-
lines.push(' }');
|
|
1310
1311
|
}
|
|
1311
1312
|
}
|
|
1312
1313
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sprlab/wccompiler",
|
|
3
|
-
"version": "0.9.
|
|
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":
|
|
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": {
|