@sprlab/wccompiler 0.9.3 → 0.9.5
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 +37 -141
- package/adapters/angular.ts +10 -20
- package/adapters/react.js +103 -0
- package/integrations/react.js +13 -109
- package/lib/codegen.js +7 -6
- package/package.json +3 -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: async 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,42 @@
|
|
|
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
|
+
* 1. For basic two-way binding with [(prop)]:
|
|
9
|
+
* No adapter needed! WCC components emit propNameChange directly.
|
|
10
|
+
* Just add CUSTOM_ELEMENTS_SCHEMA to your component.
|
|
11
|
+
*
|
|
12
|
+
* 2. For slots (named + scoped):
|
|
13
|
+
* Copy `adapters/angular.ts` to your Angular project's src/ directory.
|
|
14
|
+
* This is the standard pattern for Web Component libraries in Angular
|
|
15
|
+
* (same as Shoelace, Lit, FAST, etc.) because Angular AOT requires
|
|
16
|
+
* directives to be compiled within the project.
|
|
17
|
+
*
|
|
18
|
+
* Steps:
|
|
19
|
+
* a) Copy adapters/angular.ts → src/directives/wcc-slots.directive.ts
|
|
20
|
+
* b) Import in your component:
|
|
21
|
+
* import { WccSlotsDirective, WccSlotDef } from './directives/wcc-slots.directive';
|
|
22
|
+
* c) Add to imports: @Component({ imports: [WccSlotsDirective, WccSlotDef] })
|
|
23
|
+
* d) Add wccSlots attribute to WCC elements that use slots:
|
|
24
|
+
* <wcc-card wccSlots>
|
|
25
|
+
* <ng-template slot="header">...</ng-template>
|
|
26
|
+
* </wcc-card>
|
|
27
|
+
*
|
|
28
|
+
* WHY can't we distribute as a compiled package?
|
|
29
|
+
* Angular AOT requires decorator metadata that can only be generated by
|
|
30
|
+
* ng-packagr or the Angular compiler. Standard tsc output doesn't include
|
|
31
|
+
* the Ivy metadata Angular needs. A future version may provide a separate
|
|
32
|
+
* @sprlab/wccompiler-angular package compiled with ng-packagr.
|
|
33
|
+
*
|
|
34
|
+
* SELECTOR:
|
|
35
|
+
* The directive uses [wccSlots] attribute selector (not a dynamic exclusion
|
|
36
|
+
* selector) because Angular AOT cannot evaluate computed expressions in
|
|
37
|
+
* decorator metadata.
|
|
94
38
|
*/
|
|
95
39
|
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
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
|
-
// ```
|
|
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
|
@@ -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 {
|
|
@@ -131,7 +128,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
131
128
|
const scopedNames: string[] = element.__scopedSlots || [];
|
|
132
129
|
|
|
133
130
|
for (const slotDef of this.slotDefs) {
|
|
134
|
-
// Ignore templates with empty slot name
|
|
135
131
|
if (!slotDef.slotName) continue;
|
|
136
132
|
|
|
137
133
|
if (scopedNames.includes(slotDef.slotName)) {
|
|
@@ -237,7 +233,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
237
233
|
const state = this.slots.get(slotName);
|
|
238
234
|
if (!state || this.destroyed) return;
|
|
239
235
|
|
|
240
|
-
// Props null/undefined: clear the view
|
|
241
236
|
if (props == null) {
|
|
242
237
|
if (state.viewRef) {
|
|
243
238
|
state.viewRef.destroy();
|
|
@@ -250,11 +245,9 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
250
245
|
state.context = context;
|
|
251
246
|
|
|
252
247
|
if (state.viewRef) {
|
|
253
|
-
// Update existing context
|
|
254
248
|
Object.assign(state.viewRef.context, context);
|
|
255
249
|
state.viewRef.markForCheck();
|
|
256
250
|
} else {
|
|
257
|
-
// Create new view
|
|
258
251
|
state.viewRef = this.vcr.createEmbeddedView(state.slotDef.templateRef, context);
|
|
259
252
|
this.insertView(slotName, state);
|
|
260
253
|
}
|
|
@@ -276,7 +269,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
276
269
|
hostEl.appendChild(state.wrapperEl);
|
|
277
270
|
}
|
|
278
271
|
|
|
279
|
-
// Clear previous wrapper content and append new nodes
|
|
280
272
|
state.wrapperEl.innerHTML = '';
|
|
281
273
|
for (const node of state.viewRef.rootNodes) {
|
|
282
274
|
state.wrapperEl.appendChild(node);
|
|
@@ -287,7 +279,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
287
279
|
|
|
288
280
|
/** Full cleanup on destroy */
|
|
289
281
|
private cleanup(): void {
|
|
290
|
-
// Destroy views, invoke cleanup functions, remove wrappers
|
|
291
282
|
for (const [, state] of this.slots) {
|
|
292
283
|
if (state.viewRef) {
|
|
293
284
|
state.viewRef.destroy();
|
|
@@ -301,7 +292,6 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
301
292
|
}
|
|
302
293
|
this.slots.clear();
|
|
303
294
|
|
|
304
|
-
// Remove event listeners
|
|
305
295
|
for (const fn of this.eventCleanups) {
|
|
306
296
|
fn();
|
|
307
297
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React browser-side hooks for WCC custom elements.
|
|
3
|
+
* Bridges CustomEvent to React's ref-based event system.
|
|
4
|
+
*
|
|
5
|
+
* @module @sprlab/wccompiler/adapters/react
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: Import hooks from THIS file (not from integrations/react).
|
|
8
|
+
* The integrations/react file is for vite.config.js only (contains Babel).
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* import { useWccEvent, useWccModel } from '@sprlab/wccompiler/adapters/react'
|
|
12
|
+
*
|
|
13
|
+
* // Listen to custom events
|
|
14
|
+
* const ref = useWccEvent('change', (e) => console.log(e.detail))
|
|
15
|
+
* <wcc-counter ref={ref}></wcc-counter>
|
|
16
|
+
*
|
|
17
|
+
* // Two-way binding with defineModel
|
|
18
|
+
* const [text, setText] = useState('')
|
|
19
|
+
* const inputRef = useWccModel('value', text, setText)
|
|
20
|
+
* <wcc-input ref={inputRef}></wcc-input>
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { useRef, useEffect } from 'react'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hook that attaches a CustomEvent listener to a DOM element via ref.
|
|
27
|
+
*
|
|
28
|
+
* Supports two calling conventions:
|
|
29
|
+
* - useWccEvent(ref, eventName, handler) — uses an existing ref
|
|
30
|
+
* - useWccEvent(eventName, handler) — creates and returns a new ref
|
|
31
|
+
*
|
|
32
|
+
* @param {import('react').RefObject<HTMLElement> | string} refOrEventName
|
|
33
|
+
* @param {string | ((event: CustomEvent) => void)} eventNameOrHandler
|
|
34
|
+
* @param {((event: CustomEvent) => void)} [handler]
|
|
35
|
+
* @returns {import('react').RefObject<HTMLElement> | void}
|
|
36
|
+
*/
|
|
37
|
+
export function useWccEvent(refOrEventName, eventNameOrHandler, handler) {
|
|
38
|
+
const isRefForm = typeof refOrEventName !== 'string'
|
|
39
|
+
const elementRef = isRefForm ? refOrEventName : useRef(null)
|
|
40
|
+
const eventName = isRefForm ? eventNameOrHandler : refOrEventName
|
|
41
|
+
const callback = isRefForm ? handler : eventNameOrHandler
|
|
42
|
+
|
|
43
|
+
const handlerRef = useRef(callback)
|
|
44
|
+
handlerRef.current = callback
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const el = elementRef.current
|
|
48
|
+
if (!el) return
|
|
49
|
+
const listener = (e) => handlerRef.current(e)
|
|
50
|
+
el.addEventListener(eventName, listener)
|
|
51
|
+
return () => el.removeEventListener(eventName, listener)
|
|
52
|
+
}, [eventName])
|
|
53
|
+
|
|
54
|
+
if (!isRefForm) return elementRef
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Hook for two-way binding with WCC defineModel props.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} propName - The model prop name (e.g., 'value', 'count')
|
|
61
|
+
* @param {*} value - Current React state value
|
|
62
|
+
* @param {(newValue: *) => void} setValue - React state setter
|
|
63
|
+
* @param {import('react').RefObject<HTMLElement>} [existingRef] - Optional existing ref
|
|
64
|
+
* @returns {import('react').RefObject<HTMLElement>} Ref to attach to the WCC element
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* const [text, setText] = useState('')
|
|
68
|
+
* const inputRef = useWccModel('value', text, setText)
|
|
69
|
+
* <wcc-input ref={inputRef}></wcc-input>
|
|
70
|
+
*/
|
|
71
|
+
export function useWccModel(propName, value, setValue, existingRef) {
|
|
72
|
+
const internalRef = useRef(null)
|
|
73
|
+
const elementRef = existingRef || internalRef
|
|
74
|
+
|
|
75
|
+
const setValueRef = useRef(setValue)
|
|
76
|
+
setValueRef.current = setValue
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const el = elementRef.current
|
|
80
|
+
if (!el) return
|
|
81
|
+
|
|
82
|
+
const listener = (e) => {
|
|
83
|
+
if (e.detail && e.detail.prop === propName) {
|
|
84
|
+
setValueRef.current(e.detail.value)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
el.addEventListener('wcc:model', listener)
|
|
89
|
+
return () => el.removeEventListener('wcc:model', listener)
|
|
90
|
+
}, [propName])
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const el = elementRef.current
|
|
94
|
+
if (!el) return
|
|
95
|
+
if (value != null) {
|
|
96
|
+
el.setAttribute(propName, String(value))
|
|
97
|
+
} else {
|
|
98
|
+
el.removeAttribute(propName)
|
|
99
|
+
}
|
|
100
|
+
}, [propName, value])
|
|
101
|
+
|
|
102
|
+
return elementRef
|
|
103
|
+
}
|
package/integrations/react.js
CHANGED
|
@@ -1,126 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* React
|
|
3
|
-
*
|
|
2
|
+
* React Vite plugin for WCC custom elements.
|
|
3
|
+
* Transforms idiomatic React JSX slot patterns into WCC-compatible slot markup.
|
|
4
4
|
*
|
|
5
5
|
* @module @sprlab/wccompiler/integrations/react
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* const ref = useRef(null)
|
|
10
|
-
* useWccEvent(ref, 'change', (e) => console.log(e.detail))
|
|
11
|
-
* <wcc-counter ref={ref}></wcc-counter>
|
|
7
|
+
* IMPORTANT: This file is for vite.config.js (Node.js context) ONLY.
|
|
8
|
+
* For browser-side hooks, import from '@sprlab/wccompiler/adapters/react'.
|
|
12
9
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* @example vite.config.js
|
|
11
|
+
* ```js
|
|
12
|
+
* import { wccReactPlugin } from '@sprlab/wccompiler/integrations/react'
|
|
13
|
+
* export default { plugins: [wccReactPlugin()] }
|
|
14
|
+
* ```
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* @example Component (browser — import hooks from adapters)
|
|
17
|
+
* ```jsx
|
|
18
|
+
* import { useWccEvent, useWccModel } from '@sprlab/wccompiler/adapters/react'
|
|
19
|
+
* ```
|
|
21
20
|
*/
|
|
22
21
|
|
|
23
|
-
import { useRef, useEffect, useCallback } from 'react'
|
|
24
22
|
import { parse } from '@babel/parser'
|
|
25
23
|
import _traverse from '@babel/traverse'
|
|
26
24
|
import _generate from '@babel/generator'
|
|
27
25
|
const traverse = _traverse.default || _traverse
|
|
28
26
|
const generate = _generate.default || _generate
|
|
29
27
|
|
|
30
|
-
/**
|
|
31
|
-
* Hook that attaches a CustomEvent listener to a DOM element via ref.
|
|
32
|
-
*
|
|
33
|
-
* Supports two calling conventions:
|
|
34
|
-
* - useWccEvent(ref, eventName, handler) — uses an existing ref
|
|
35
|
-
* - useWccEvent(eventName, handler) — creates and returns a new ref
|
|
36
|
-
*
|
|
37
|
-
* @param {import('react').RefObject<HTMLElement> | string} refOrEventName
|
|
38
|
-
* @param {string | ((event: CustomEvent) => void)} eventNameOrHandler
|
|
39
|
-
* @param {((event: CustomEvent) => void)} [handler]
|
|
40
|
-
* @returns {import('react').RefObject<HTMLElement> | void}
|
|
41
|
-
*/
|
|
42
|
-
export function useWccEvent(refOrEventName, eventNameOrHandler, handler) {
|
|
43
|
-
// Detect calling convention
|
|
44
|
-
const isRefForm = typeof refOrEventName !== 'string'
|
|
45
|
-
const elementRef = isRefForm ? refOrEventName : useRef(null)
|
|
46
|
-
const eventName = isRefForm ? eventNameOrHandler : refOrEventName
|
|
47
|
-
const callback = isRefForm ? handler : eventNameOrHandler
|
|
48
|
-
|
|
49
|
-
const handlerRef = useRef(callback)
|
|
50
|
-
handlerRef.current = callback
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
const el = elementRef.current
|
|
54
|
-
if (!el) return
|
|
55
|
-
const listener = (e) => handlerRef.current(e)
|
|
56
|
-
el.addEventListener(eventName, listener)
|
|
57
|
-
return () => el.removeEventListener(eventName, listener)
|
|
58
|
-
}, [eventName])
|
|
59
|
-
|
|
60
|
-
// Only return ref if we created it (Form 2)
|
|
61
|
-
if (!isRefForm) return elementRef
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Hook for two-way binding with WCC defineModel props.
|
|
67
|
-
*
|
|
68
|
-
* Listens for `wcc:model` events on the element and calls the setter
|
|
69
|
-
* when the matching prop changes internally. Also syncs the React state
|
|
70
|
-
* to the element's attribute when the value changes externally.
|
|
71
|
-
*
|
|
72
|
-
* @param {string} propName - The model prop name (e.g., 'value', 'count')
|
|
73
|
-
* @param {*} value - Current React state value
|
|
74
|
-
* @param {(newValue: *) => void} setValue - React state setter
|
|
75
|
-
* @param {import('react').RefObject<HTMLElement>} [existingRef] - Optional existing ref
|
|
76
|
-
* @returns {import('react').RefObject<HTMLElement>} Ref to attach to the WCC element
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* ```jsx
|
|
80
|
-
* function App() {
|
|
81
|
-
* const [text, setText] = useState('')
|
|
82
|
-
* const inputRef = useWccModel('value', text, setText)
|
|
83
|
-
* return <wcc-input ref={inputRef}></wcc-input>
|
|
84
|
-
* }
|
|
85
|
-
* ```
|
|
86
|
-
*/
|
|
87
|
-
export function useWccModel(propName, value, setValue, existingRef) {
|
|
88
|
-
const internalRef = useRef(null)
|
|
89
|
-
const elementRef = existingRef || internalRef
|
|
90
|
-
|
|
91
|
-
const setValueRef = useRef(setValue)
|
|
92
|
-
setValueRef.current = setValue
|
|
93
|
-
|
|
94
|
-
// Listen for wcc:model events from the component (child → parent)
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
const el = elementRef.current
|
|
97
|
-
if (!el) return
|
|
98
|
-
|
|
99
|
-
const listener = (e) => {
|
|
100
|
-
if (e.detail && e.detail.prop === propName) {
|
|
101
|
-
setValueRef.current(e.detail.value)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
el.addEventListener('wcc:model', listener)
|
|
106
|
-
return () => el.removeEventListener('wcc:model', listener)
|
|
107
|
-
}, [propName])
|
|
108
|
-
|
|
109
|
-
// Sync React state to the element's attribute (parent → child)
|
|
110
|
-
useEffect(() => {
|
|
111
|
-
const el = elementRef.current
|
|
112
|
-
if (!el) return
|
|
113
|
-
if (value != null) {
|
|
114
|
-
el.setAttribute(propName, String(value))
|
|
115
|
-
} else {
|
|
116
|
-
el.removeAttribute(propName)
|
|
117
|
-
}
|
|
118
|
-
}, [propName, value])
|
|
119
|
-
|
|
120
|
-
return elementRef
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
28
|
/**
|
|
125
29
|
* JSX attribute name to HTML attribute name mapping.
|
|
126
30
|
* React uses camelCase for some attributes that HTML uses lowercase.
|
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.5",
|
|
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,8 @@
|
|
|
9
9
|
"./integrations/react": "./integrations/react.js",
|
|
10
10
|
"./integrations/angular": "./integrations/angular.js",
|
|
11
11
|
"./adapters/vue": "./adapters/vue.js",
|
|
12
|
-
"./adapters/angular": "./adapters/angular.js"
|
|
12
|
+
"./adapters/angular": "./adapters/angular.js",
|
|
13
|
+
"./adapters/react": "./adapters/react.js"
|
|
13
14
|
},
|
|
14
15
|
"bin": {
|
|
15
16
|
"wcc": "./bin/wcc.js"
|