@sprlab/wccompiler 0.9.9 → 0.10.1
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 +76 -1
- package/adapters/angular-compiled/angular.mjs +149 -1
- package/adapters/angular.ts +165 -4
- package/adapters/react.js +155 -7
- package/package.json +1 -1
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*
|
|
26
26
|
* @module @sprlab/wccompiler/adapters/angular
|
|
27
27
|
*/
|
|
28
|
-
import { TemplateRef, QueryList, AfterContentInit, OnDestroy } from '@angular/core';
|
|
28
|
+
import { TemplateRef, QueryList, AfterContentInit, OnDestroy, OnInit, EventEmitter } from '@angular/core';
|
|
29
29
|
import * as i0 from "@angular/core";
|
|
30
30
|
/** Context object passed to createEmbeddedView for scoped slots */
|
|
31
31
|
export interface SlotContext {
|
|
@@ -96,3 +96,78 @@ export declare class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
96
96
|
static ɵfac: i0.ɵɵFactoryDeclaration<WccSlotsDirective, never>;
|
|
97
97
|
static ɵdir: i0.ɵɵDirectiveDeclaration<WccSlotsDirective, "[wccSlots]", never, {}, {}, ["slotDefs"], never, true, never>;
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Directive that bridges WCC custom element events to Angular output bindings.
|
|
101
|
+
*
|
|
102
|
+
* Problem: Angular's `(event-name)="handler($event)"` works on custom elements,
|
|
103
|
+
* but `$event` is the raw CustomEvent. The developer must write `$event.detail`
|
|
104
|
+
* to get the payload. This is verbose and error-prone.
|
|
105
|
+
*
|
|
106
|
+
* Solution: This directive listens for CustomEvents on the host element and
|
|
107
|
+
* re-emits them as Angular outputs with `$event = event.detail`.
|
|
108
|
+
*
|
|
109
|
+
* Usage:
|
|
110
|
+
* <wcc-counter wccEvent="count-changed" (wccEmit)="onCount($event)"></wcc-counter>
|
|
111
|
+
*
|
|
112
|
+
* Or for multiple events, use WccEvents (plural) with a comma-separated list:
|
|
113
|
+
* <wcc-counter wccEvents="count-changed, value-changed"
|
|
114
|
+
* (countChanged)="onCount($event)"
|
|
115
|
+
* (valueChanged)="onValue($event)">
|
|
116
|
+
* </wcc-counter>
|
|
117
|
+
*
|
|
118
|
+
* The event name is converted from kebab-case to camelCase for the output:
|
|
119
|
+
* 'count-changed' → (countChanged)
|
|
120
|
+
* 'value-changed' → (valueChanged)
|
|
121
|
+
* 'change' → (change)
|
|
122
|
+
*/
|
|
123
|
+
/**
|
|
124
|
+
* Single-event directive: listens for one CustomEvent and emits its detail.
|
|
125
|
+
*
|
|
126
|
+
* Usage:
|
|
127
|
+
* <wcc-counter wccEvent="count-changed" (wccEmit)="handler($event)"></wcc-counter>
|
|
128
|
+
*/
|
|
129
|
+
export declare class WccEvent implements OnInit, OnDestroy {
|
|
130
|
+
wccEvent: string;
|
|
131
|
+
wccEmit: EventEmitter<any>;
|
|
132
|
+
private el;
|
|
133
|
+
private listener;
|
|
134
|
+
ngOnInit(): void;
|
|
135
|
+
ngOnDestroy(): void;
|
|
136
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<WccEvent, never>;
|
|
137
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<WccEvent, "[wccEvent]", never, { "wccEvent": { "alias": "wccEvent"; "required": false; }; }, { "wccEmit": "wccEmit"; }, never, never, true, never>;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Event bridging directive: allows using camelCase event bindings on WCC elements.
|
|
141
|
+
*
|
|
142
|
+
* Without this directive, Angular devs must use kebab-case event names:
|
|
143
|
+
* <wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
|
|
144
|
+
*
|
|
145
|
+
* With this directive, they can use camelCase (more Angular-idiomatic):
|
|
146
|
+
* <wcc-counter wccEvents (countChanged)="onCount($event.detail)"></wcc-counter>
|
|
147
|
+
*
|
|
148
|
+
* The directive listens for kebab-case CustomEvents from the WCC component
|
|
149
|
+
* and re-dispatches them with camelCase names so Angular's event binding picks them up.
|
|
150
|
+
*
|
|
151
|
+
* Event name conversion:
|
|
152
|
+
* 'count-changed' → dispatches 'countChanged'
|
|
153
|
+
* 'value-changed' → dispatches 'valueChanged'
|
|
154
|
+
* 'change' → dispatches 'change' (no conversion needed)
|
|
155
|
+
*
|
|
156
|
+
* Event discovery:
|
|
157
|
+
* - Auto: reads `static __events` from the WCC component class (set by codegen)
|
|
158
|
+
* - Manual: pass an explicit array via [wccEvents]="['count-changed', 'value-changed']"
|
|
159
|
+
*
|
|
160
|
+
* Note: $event is still the CustomEvent — use $event.detail to get the payload.
|
|
161
|
+
* This is consistent with how Angular handles all DOM events.
|
|
162
|
+
*/
|
|
163
|
+
export declare class WccEvents implements OnInit, OnDestroy {
|
|
164
|
+
/** Optional explicit list of kebab-case event names to bridge */
|
|
165
|
+
wccEvents: string[] | '';
|
|
166
|
+
private el;
|
|
167
|
+
private listeners;
|
|
168
|
+
ngOnInit(): void;
|
|
169
|
+
private setupEvents;
|
|
170
|
+
ngOnDestroy(): void;
|
|
171
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<WccEvents, never>;
|
|
172
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<WccEvents, "[wccEvents]", never, { "wccEvents": { "alias": "wccEvents"; "required": false; }; }, {}, never, never, true, never>;
|
|
173
|
+
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*
|
|
26
26
|
* @module @sprlab/wccompiler/adapters/angular
|
|
27
27
|
*/
|
|
28
|
-
import { Directive, TemplateRef, ElementRef, ViewContainerRef, ChangeDetectorRef, ContentChildren, inject, Attribute, } from '@angular/core';
|
|
28
|
+
import { Directive, TemplateRef, ElementRef, ViewContainerRef, ChangeDetectorRef, ContentChildren, Output, EventEmitter, inject, Attribute, Input, } from '@angular/core';
|
|
29
29
|
import * as i0 from "@angular/core";
|
|
30
30
|
// ─── WccSlotDef — Auxiliary Directive ───────────────────────────────────────
|
|
31
31
|
/**
|
|
@@ -297,3 +297,151 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
|
|
|
297
297
|
type: ContentChildren,
|
|
298
298
|
args: [WccSlotDef]
|
|
299
299
|
}] } });
|
|
300
|
+
// ─── WccEvent — Event Binding Directive ─────────────────────────────────────
|
|
301
|
+
/**
|
|
302
|
+
* Directive that bridges WCC custom element events to Angular output bindings.
|
|
303
|
+
*
|
|
304
|
+
* Problem: Angular's `(event-name)="handler($event)"` works on custom elements,
|
|
305
|
+
* but `$event` is the raw CustomEvent. The developer must write `$event.detail`
|
|
306
|
+
* to get the payload. This is verbose and error-prone.
|
|
307
|
+
*
|
|
308
|
+
* Solution: This directive listens for CustomEvents on the host element and
|
|
309
|
+
* re-emits them as Angular outputs with `$event = event.detail`.
|
|
310
|
+
*
|
|
311
|
+
* Usage:
|
|
312
|
+
* <wcc-counter wccEvent="count-changed" (wccEmit)="onCount($event)"></wcc-counter>
|
|
313
|
+
*
|
|
314
|
+
* Or for multiple events, use WccEvents (plural) with a comma-separated list:
|
|
315
|
+
* <wcc-counter wccEvents="count-changed, value-changed"
|
|
316
|
+
* (countChanged)="onCount($event)"
|
|
317
|
+
* (valueChanged)="onValue($event)">
|
|
318
|
+
* </wcc-counter>
|
|
319
|
+
*
|
|
320
|
+
* The event name is converted from kebab-case to camelCase for the output:
|
|
321
|
+
* 'count-changed' → (countChanged)
|
|
322
|
+
* 'value-changed' → (valueChanged)
|
|
323
|
+
* 'change' → (change)
|
|
324
|
+
*/
|
|
325
|
+
/**
|
|
326
|
+
* Single-event directive: listens for one CustomEvent and emits its detail.
|
|
327
|
+
*
|
|
328
|
+
* Usage:
|
|
329
|
+
* <wcc-counter wccEvent="count-changed" (wccEmit)="handler($event)"></wcc-counter>
|
|
330
|
+
*/
|
|
331
|
+
export class WccEvent {
|
|
332
|
+
wccEvent = '';
|
|
333
|
+
wccEmit = new EventEmitter();
|
|
334
|
+
el = inject(ElementRef);
|
|
335
|
+
listener = null;
|
|
336
|
+
ngOnInit() {
|
|
337
|
+
if (!this.wccEvent)
|
|
338
|
+
return;
|
|
339
|
+
this.listener = (e) => {
|
|
340
|
+
this.wccEmit.emit(e.detail);
|
|
341
|
+
};
|
|
342
|
+
this.el.nativeElement.addEventListener(this.wccEvent, this.listener);
|
|
343
|
+
}
|
|
344
|
+
ngOnDestroy() {
|
|
345
|
+
if (this.listener && this.wccEvent) {
|
|
346
|
+
this.el.nativeElement.removeEventListener(this.wccEvent, this.listener);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccEvent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
350
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.21", type: WccEvent, isStandalone: true, selector: "[wccEvent]", inputs: { wccEvent: "wccEvent" }, outputs: { wccEmit: "wccEmit" }, ngImport: i0 });
|
|
351
|
+
}
|
|
352
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccEvent, decorators: [{
|
|
353
|
+
type: Directive,
|
|
354
|
+
args: [{
|
|
355
|
+
selector: '[wccEvent]',
|
|
356
|
+
standalone: true,
|
|
357
|
+
}]
|
|
358
|
+
}], propDecorators: { wccEvent: [{
|
|
359
|
+
type: Input
|
|
360
|
+
}], wccEmit: [{
|
|
361
|
+
type: Output
|
|
362
|
+
}] } });
|
|
363
|
+
/**
|
|
364
|
+
* Event bridging directive: allows using camelCase event bindings on WCC elements.
|
|
365
|
+
*
|
|
366
|
+
* Without this directive, Angular devs must use kebab-case event names:
|
|
367
|
+
* <wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
|
|
368
|
+
*
|
|
369
|
+
* With this directive, they can use camelCase (more Angular-idiomatic):
|
|
370
|
+
* <wcc-counter wccEvents (countChanged)="onCount($event.detail)"></wcc-counter>
|
|
371
|
+
*
|
|
372
|
+
* The directive listens for kebab-case CustomEvents from the WCC component
|
|
373
|
+
* and re-dispatches them with camelCase names so Angular's event binding picks them up.
|
|
374
|
+
*
|
|
375
|
+
* Event name conversion:
|
|
376
|
+
* 'count-changed' → dispatches 'countChanged'
|
|
377
|
+
* 'value-changed' → dispatches 'valueChanged'
|
|
378
|
+
* 'change' → dispatches 'change' (no conversion needed)
|
|
379
|
+
*
|
|
380
|
+
* Event discovery:
|
|
381
|
+
* - Auto: reads `static __events` from the WCC component class (set by codegen)
|
|
382
|
+
* - Manual: pass an explicit array via [wccEvents]="['count-changed', 'value-changed']"
|
|
383
|
+
*
|
|
384
|
+
* Note: $event is still the CustomEvent — use $event.detail to get the payload.
|
|
385
|
+
* This is consistent with how Angular handles all DOM events.
|
|
386
|
+
*/
|
|
387
|
+
export class WccEvents {
|
|
388
|
+
/** Optional explicit list of kebab-case event names to bridge */
|
|
389
|
+
wccEvents = '';
|
|
390
|
+
el = inject(ElementRef);
|
|
391
|
+
listeners = [];
|
|
392
|
+
ngOnInit() {
|
|
393
|
+
const hostEl = this.el.nativeElement;
|
|
394
|
+
const tagName = hostEl.tagName.toLowerCase();
|
|
395
|
+
if (!tagName.includes('-'))
|
|
396
|
+
return;
|
|
397
|
+
this.setupEvents(hostEl, tagName);
|
|
398
|
+
}
|
|
399
|
+
async setupEvents(hostEl, tagName) {
|
|
400
|
+
let eventNames;
|
|
401
|
+
if (Array.isArray(this.wccEvents) && this.wccEvents.length > 0) {
|
|
402
|
+
eventNames = this.wccEvents;
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
// Auto-discover from component metadata
|
|
406
|
+
await customElements.whenDefined(tagName);
|
|
407
|
+
const ctor = customElements.get(tagName);
|
|
408
|
+
eventNames = ctor?.__events || [];
|
|
409
|
+
}
|
|
410
|
+
if (eventNames.length === 0)
|
|
411
|
+
return;
|
|
412
|
+
for (const eventName of eventNames) {
|
|
413
|
+
// Only bridge events that have hyphens (already camelCase events don't need bridging)
|
|
414
|
+
if (!eventName.includes('-'))
|
|
415
|
+
continue;
|
|
416
|
+
const camelName = eventName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
417
|
+
const listener = (e) => {
|
|
418
|
+
// Re-dispatch with camelCase name — Angular's (camelName) binding will catch it
|
|
419
|
+
hostEl.dispatchEvent(new CustomEvent(camelName, {
|
|
420
|
+
detail: e.detail,
|
|
421
|
+
bubbles: false,
|
|
422
|
+
cancelable: false,
|
|
423
|
+
}));
|
|
424
|
+
};
|
|
425
|
+
hostEl.addEventListener(eventName, listener);
|
|
426
|
+
this.listeners.push([eventName, listener]);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
ngOnDestroy() {
|
|
430
|
+
const hostEl = this.el.nativeElement;
|
|
431
|
+
for (const [name, listener] of this.listeners) {
|
|
432
|
+
hostEl.removeEventListener(name, listener);
|
|
433
|
+
}
|
|
434
|
+
this.listeners = [];
|
|
435
|
+
}
|
|
436
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccEvents, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
437
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.21", type: WccEvents, isStandalone: true, selector: "[wccEvents]", inputs: { wccEvents: "wccEvents" }, ngImport: i0 });
|
|
438
|
+
}
|
|
439
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: WccEvents, decorators: [{
|
|
440
|
+
type: Directive,
|
|
441
|
+
args: [{
|
|
442
|
+
selector: '[wccEvents]',
|
|
443
|
+
standalone: true,
|
|
444
|
+
}]
|
|
445
|
+
}], propDecorators: { wccEvents: [{
|
|
446
|
+
type: Input
|
|
447
|
+
}] } });
|
package/adapters/angular.ts
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Angular adapter for WCC Scoped Slots.
|
|
2
|
+
* Angular adapter for WCC Scoped Slots and Event Binding.
|
|
3
3
|
*
|
|
4
4
|
* Exports:
|
|
5
5
|
* - WccSlotDef: Auxiliary directive for ng-template[slot]
|
|
6
6
|
* - WccSlotsDirective: Main directive activated via [wccSlots] attribute
|
|
7
|
+
* - WccEvent: Single-event directive (wccEvent="name" + wccEmit output)
|
|
8
|
+
* - WccEvents: Multi-event bridging directive (kebab-case → camelCase)
|
|
7
9
|
* - SlotContext: Interface for template context typing
|
|
8
10
|
*
|
|
9
11
|
* Usage:
|
|
10
|
-
* import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular';
|
|
12
|
+
* import { WccSlotsDirective, WccSlotDef, WccEvent, WccEvents } from '@sprlab/wccompiler/adapters/angular';
|
|
11
13
|
*
|
|
12
14
|
* @Component({
|
|
13
|
-
* imports: [WccSlotsDirective, WccSlotDef],
|
|
15
|
+
* imports: [WccSlotsDirective, WccSlotDef, WccEvent, WccEvents],
|
|
14
16
|
* schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
15
17
|
* template: `
|
|
16
18
|
* <wcc-card wccSlots>
|
|
17
19
|
* <ng-template slot="header"><strong>Header</strong></ng-template>
|
|
18
20
|
* <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
|
|
19
21
|
* </wcc-card>
|
|
22
|
+
*
|
|
23
|
+
* <!-- Event binding option 1: single event with unwrapped detail -->
|
|
24
|
+
* <wcc-counter wccEvent="count-changed" (wccEmit)="onCount($event)"></wcc-counter>
|
|
25
|
+
*
|
|
26
|
+
* <!-- Event binding option 2: camelCase event names -->
|
|
27
|
+
* <wcc-counter wccEvents (countChanged)="onCount($event.detail)"></wcc-counter>
|
|
28
|
+
*
|
|
29
|
+
* <!-- Event binding option 3: standard Angular (always works) -->
|
|
30
|
+
* <wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
|
|
20
31
|
* `
|
|
21
32
|
* })
|
|
22
33
|
*
|
|
23
|
-
* Note: Add the `wccSlots` attribute to any WCC
|
|
34
|
+
* Note: Add the `wccSlots` attribute to any WCC element that uses slots.
|
|
24
35
|
* This is required because Angular AOT cannot evaluate dynamic selectors.
|
|
25
36
|
*
|
|
26
37
|
* @module @sprlab/wccompiler/adapters/angular
|
|
@@ -37,8 +48,12 @@ import {
|
|
|
37
48
|
EmbeddedViewRef,
|
|
38
49
|
AfterContentInit,
|
|
39
50
|
OnDestroy,
|
|
51
|
+
OnInit,
|
|
52
|
+
Output,
|
|
53
|
+
EventEmitter,
|
|
40
54
|
inject,
|
|
41
55
|
Attribute,
|
|
56
|
+
Input,
|
|
42
57
|
} from '@angular/core';
|
|
43
58
|
|
|
44
59
|
// ─── Interfaces ─────────────────────────────────────────────────────────────
|
|
@@ -351,3 +366,149 @@ export class WccSlotsDirective implements AfterContentInit, OnDestroy {
|
|
|
351
366
|
this.eventCleanups = [];
|
|
352
367
|
}
|
|
353
368
|
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
// ─── WccEvent — Event Binding Directive ─────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Directive that bridges WCC custom element events to Angular output bindings.
|
|
375
|
+
*
|
|
376
|
+
* Problem: Angular's `(event-name)="handler($event)"` works on custom elements,
|
|
377
|
+
* but `$event` is the raw CustomEvent. The developer must write `$event.detail`
|
|
378
|
+
* to get the payload. This is verbose and error-prone.
|
|
379
|
+
*
|
|
380
|
+
* Solution: This directive listens for CustomEvents on the host element and
|
|
381
|
+
* re-emits them as Angular outputs with `$event = event.detail`.
|
|
382
|
+
*
|
|
383
|
+
* Usage:
|
|
384
|
+
* <wcc-counter wccEvent="count-changed" (wccEmit)="onCount($event)"></wcc-counter>
|
|
385
|
+
*
|
|
386
|
+
* Or for multiple events, use WccEvents (plural) with a comma-separated list:
|
|
387
|
+
* <wcc-counter wccEvents="count-changed, value-changed"
|
|
388
|
+
* (countChanged)="onCount($event)"
|
|
389
|
+
* (valueChanged)="onValue($event)">
|
|
390
|
+
* </wcc-counter>
|
|
391
|
+
*
|
|
392
|
+
* The event name is converted from kebab-case to camelCase for the output:
|
|
393
|
+
* 'count-changed' → (countChanged)
|
|
394
|
+
* 'value-changed' → (valueChanged)
|
|
395
|
+
* 'change' → (change)
|
|
396
|
+
*/
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Single-event directive: listens for one CustomEvent and emits its detail.
|
|
400
|
+
*
|
|
401
|
+
* Usage:
|
|
402
|
+
* <wcc-counter wccEvent="count-changed" (wccEmit)="handler($event)"></wcc-counter>
|
|
403
|
+
*/
|
|
404
|
+
@Directive({
|
|
405
|
+
selector: '[wccEvent]',
|
|
406
|
+
standalone: true,
|
|
407
|
+
})
|
|
408
|
+
export class WccEvent implements OnInit, OnDestroy {
|
|
409
|
+
@Input() wccEvent = '';
|
|
410
|
+
@Output() wccEmit = new EventEmitter<any>();
|
|
411
|
+
|
|
412
|
+
private el = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
413
|
+
private listener: ((e: Event) => void) | null = null;
|
|
414
|
+
|
|
415
|
+
ngOnInit(): void {
|
|
416
|
+
if (!this.wccEvent) return;
|
|
417
|
+
this.listener = (e: Event) => {
|
|
418
|
+
this.wccEmit.emit((e as CustomEvent).detail);
|
|
419
|
+
};
|
|
420
|
+
this.el.nativeElement.addEventListener(this.wccEvent, this.listener);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
ngOnDestroy(): void {
|
|
424
|
+
if (this.listener && this.wccEvent) {
|
|
425
|
+
this.el.nativeElement.removeEventListener(this.wccEvent, this.listener);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Event bridging directive: allows using camelCase event bindings on WCC elements.
|
|
432
|
+
*
|
|
433
|
+
* Without this directive, Angular devs must use kebab-case event names:
|
|
434
|
+
* <wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
|
|
435
|
+
*
|
|
436
|
+
* With this directive, they can use camelCase (more Angular-idiomatic):
|
|
437
|
+
* <wcc-counter wccEvents (countChanged)="onCount($event.detail)"></wcc-counter>
|
|
438
|
+
*
|
|
439
|
+
* The directive listens for kebab-case CustomEvents from the WCC component
|
|
440
|
+
* and re-dispatches them with camelCase names so Angular's event binding picks them up.
|
|
441
|
+
*
|
|
442
|
+
* Event name conversion:
|
|
443
|
+
* 'count-changed' → dispatches 'countChanged'
|
|
444
|
+
* 'value-changed' → dispatches 'valueChanged'
|
|
445
|
+
* 'change' → dispatches 'change' (no conversion needed)
|
|
446
|
+
*
|
|
447
|
+
* Event discovery:
|
|
448
|
+
* - Auto: reads `static __events` from the WCC component class (set by codegen)
|
|
449
|
+
* - Manual: pass an explicit array via [wccEvents]="['count-changed', 'value-changed']"
|
|
450
|
+
*
|
|
451
|
+
* Note: $event is still the CustomEvent — use $event.detail to get the payload.
|
|
452
|
+
* This is consistent with how Angular handles all DOM events.
|
|
453
|
+
*/
|
|
454
|
+
@Directive({
|
|
455
|
+
selector: '[wccEvents]',
|
|
456
|
+
standalone: true,
|
|
457
|
+
})
|
|
458
|
+
export class WccEvents implements OnInit, OnDestroy {
|
|
459
|
+
/** Optional explicit list of kebab-case event names to bridge */
|
|
460
|
+
@Input() wccEvents: string[] | '' = '';
|
|
461
|
+
|
|
462
|
+
private el = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
463
|
+
private listeners: Array<[string, (e: Event) => void]> = [];
|
|
464
|
+
|
|
465
|
+
ngOnInit(): void {
|
|
466
|
+
const hostEl = this.el.nativeElement;
|
|
467
|
+
const tagName = hostEl.tagName.toLowerCase();
|
|
468
|
+
if (!tagName.includes('-')) return;
|
|
469
|
+
|
|
470
|
+
this.setupEvents(hostEl, tagName);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private async setupEvents(hostEl: HTMLElement, tagName: string): Promise<void> {
|
|
474
|
+
let eventNames: string[];
|
|
475
|
+
|
|
476
|
+
if (Array.isArray(this.wccEvents) && this.wccEvents.length > 0) {
|
|
477
|
+
eventNames = this.wccEvents;
|
|
478
|
+
} else {
|
|
479
|
+
// Auto-discover from component metadata
|
|
480
|
+
await customElements.whenDefined(tagName);
|
|
481
|
+
const ctor = customElements.get(tagName) as any;
|
|
482
|
+
eventNames = ctor?.__events || [];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (eventNames.length === 0) return;
|
|
486
|
+
|
|
487
|
+
for (const eventName of eventNames) {
|
|
488
|
+
// Only bridge events that have hyphens (already camelCase events don't need bridging)
|
|
489
|
+
if (!eventName.includes('-')) continue;
|
|
490
|
+
|
|
491
|
+
const camelName = eventName.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
|
|
492
|
+
|
|
493
|
+
const listener = (e: Event) => {
|
|
494
|
+
// Re-dispatch with camelCase name — Angular's (camelName) binding will catch it
|
|
495
|
+
hostEl.dispatchEvent(new CustomEvent(camelName, {
|
|
496
|
+
detail: (e as CustomEvent).detail,
|
|
497
|
+
bubbles: false,
|
|
498
|
+
cancelable: false,
|
|
499
|
+
}));
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
hostEl.addEventListener(eventName, listener);
|
|
503
|
+
this.listeners.push([eventName, listener]);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
ngOnDestroy(): void {
|
|
508
|
+
const hostEl = this.el.nativeElement;
|
|
509
|
+
for (const [name, listener] of this.listeners) {
|
|
510
|
+
hostEl.removeEventListener(name, listener);
|
|
511
|
+
}
|
|
512
|
+
this.listeners = [];
|
|
513
|
+
}
|
|
514
|
+
}
|
package/adapters/react.js
CHANGED
|
@@ -8,19 +8,21 @@
|
|
|
8
8
|
* The integrations/react file is for vite.config.js only (contains Babel).
|
|
9
9
|
*
|
|
10
10
|
* Usage:
|
|
11
|
-
* import { useWccEvent, useWccModel } from '@sprlab/wccompiler/adapters/react'
|
|
11
|
+
* import { useWccEvent, useWccModel, createWccWrapper } from '@sprlab/wccompiler/adapters/react'
|
|
12
12
|
*
|
|
13
|
-
* //
|
|
13
|
+
* // Option A: Low-level hooks (full control)
|
|
14
14
|
* const ref = useWccEvent('change', (e) => console.log(e.detail))
|
|
15
15
|
* <wcc-counter ref={ref}></wcc-counter>
|
|
16
16
|
*
|
|
17
|
-
* //
|
|
18
|
-
* const
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* // Option B: Wrapper components (idiomatic React DX)
|
|
18
|
+
* const WccCounter = createWccWrapper('wcc-counter', {
|
|
19
|
+
* events: ['change'],
|
|
20
|
+
* models: ['count']
|
|
21
|
+
* })
|
|
22
|
+
* <WccCounter onChange={handler} count={count} onCountChanged={setCount} />
|
|
21
23
|
*/
|
|
22
24
|
|
|
23
|
-
import { useRef, useEffect } from 'react'
|
|
25
|
+
import React, { useRef, useEffect } from 'react'
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* Hook that attaches a CustomEvent listener to a DOM element via ref.
|
|
@@ -101,3 +103,149 @@ export function useWccModel(propName, value, setValue, existingRef) {
|
|
|
101
103
|
|
|
102
104
|
return elementRef
|
|
103
105
|
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a React wrapper component for a WCC custom element.
|
|
110
|
+
*
|
|
111
|
+
* The wrapper provides idiomatic React DX:
|
|
112
|
+
* - Event props: `onChange`, `onCountChanged` → automatically wired via addEventListener
|
|
113
|
+
* - Model props: two-way binding via attribute + event listener
|
|
114
|
+
* - Regular props: passed as attributes on the custom element
|
|
115
|
+
* - Children: passed through as-is (use `<div slot="name">` for named slots)
|
|
116
|
+
* - Ref forwarding: supports React refs via forwardRef
|
|
117
|
+
*
|
|
118
|
+
* @param {string} tagName - The custom element tag name (e.g., 'wcc-card')
|
|
119
|
+
* @param {Object} [config] - Configuration for the wrapper
|
|
120
|
+
* @param {string[]} [config.events] - Custom event names to expose as onEventName props
|
|
121
|
+
* Event names are converted: 'count-changed' → onCountChanged prop
|
|
122
|
+
* @param {string[]} [config.models] - Model prop names for two-way binding
|
|
123
|
+
* Each model 'name' creates: `name` prop (sets attribute) + `onNameChanged` event
|
|
124
|
+
* @returns {import('react').ForwardRefExoticComponent} A React component
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* const WccCounter = createWccWrapper('wcc-counter', {
|
|
128
|
+
* events: ['change'],
|
|
129
|
+
* models: ['count']
|
|
130
|
+
* })
|
|
131
|
+
*
|
|
132
|
+
* function App() {
|
|
133
|
+
* const [count, setCount] = useState(0)
|
|
134
|
+
* return (
|
|
135
|
+
* <WccCounter
|
|
136
|
+
* count={count}
|
|
137
|
+
* onCountChanged={(e) => setCount(e.detail)}
|
|
138
|
+
* onChange={(e) => console.log('changed', e.detail)}
|
|
139
|
+
* label="Clicks"
|
|
140
|
+
* >
|
|
141
|
+
* <div slot="footer">Footer content</div>
|
|
142
|
+
* </WccCounter>
|
|
143
|
+
* )
|
|
144
|
+
* }
|
|
145
|
+
*/
|
|
146
|
+
export function createWccWrapper(tagName, config = {}) {
|
|
147
|
+
const { events = [], models = [] } = config
|
|
148
|
+
|
|
149
|
+
// Build a set of event prop names for quick lookup
|
|
150
|
+
// 'count-changed' → 'onCountChanged'
|
|
151
|
+
// 'change' → 'onChange'
|
|
152
|
+
const eventPropMap = new Map()
|
|
153
|
+
for (const eventName of events) {
|
|
154
|
+
const propName = 'on' + eventName.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('')
|
|
155
|
+
eventPropMap.set(propName, eventName)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Model events: 'count' → 'count-changed' → 'onCountChanged'
|
|
159
|
+
const modelEventMap = new Map()
|
|
160
|
+
for (const modelName of models) {
|
|
161
|
+
const eventName = `${modelName}-changed`
|
|
162
|
+
const propName = 'on' + eventName.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('')
|
|
163
|
+
eventPropMap.set(propName, eventName)
|
|
164
|
+
modelEventMap.set(modelName, eventName)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Reserved prop names that should not be passed as attributes
|
|
168
|
+
const SKIP_PROPS = new Set(['children', 'key', 'ref', 'style', 'className', 'dangerouslySetInnerHTML'])
|
|
169
|
+
|
|
170
|
+
const WccWrapper = React.forwardRef(function WccWrapper(props, externalRef) {
|
|
171
|
+
const internalRef = useRef(null)
|
|
172
|
+
const ref = externalRef || internalRef
|
|
173
|
+
|
|
174
|
+
// Store event handlers in a ref to avoid re-subscribing on every render
|
|
175
|
+
const handlersRef = useRef({})
|
|
176
|
+
|
|
177
|
+
// Collect event handlers and regular props
|
|
178
|
+
const regularProps = {}
|
|
179
|
+
const eventHandlers = {}
|
|
180
|
+
|
|
181
|
+
for (const [key, value] of Object.entries(props)) {
|
|
182
|
+
if (SKIP_PROPS.has(key)) continue
|
|
183
|
+
|
|
184
|
+
if (eventPropMap.has(key)) {
|
|
185
|
+
eventHandlers[eventPropMap.get(key)] = value
|
|
186
|
+
} else if (key.startsWith('on') && key.length > 2 && key[2] >= 'A' && key[2] <= 'Z') {
|
|
187
|
+
// Generic React event handler pattern: onClick, onFocus, etc.
|
|
188
|
+
// Convert onSomething → 'something' (lowercase first char)
|
|
189
|
+
const nativeEvent = key[2].toLowerCase() + key.slice(3)
|
|
190
|
+
eventHandlers[nativeEvent] = value
|
|
191
|
+
} else {
|
|
192
|
+
regularProps[key] = value
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Update handlers ref
|
|
197
|
+
handlersRef.current = eventHandlers
|
|
198
|
+
|
|
199
|
+
// Subscribe to custom events
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
const el = typeof ref === 'function' ? null : ref?.current
|
|
202
|
+
if (!el) return
|
|
203
|
+
|
|
204
|
+
const listeners = []
|
|
205
|
+
const allEvents = new Set([...eventPropMap.values(), ...Object.keys(eventHandlers)])
|
|
206
|
+
|
|
207
|
+
for (const eventName of allEvents) {
|
|
208
|
+
const listener = (e) => {
|
|
209
|
+
const handler = handlersRef.current[eventName]
|
|
210
|
+
if (handler) handler(e)
|
|
211
|
+
}
|
|
212
|
+
el.addEventListener(eventName, listener)
|
|
213
|
+
listeners.push([eventName, listener])
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return () => {
|
|
217
|
+
for (const [name, listener] of listeners) {
|
|
218
|
+
el.removeEventListener(name, listener)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
222
|
+
|
|
223
|
+
// Sync regular props as attributes
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
const el = typeof ref === 'function' ? null : ref?.current
|
|
226
|
+
if (!el) return
|
|
227
|
+
|
|
228
|
+
for (const [key, value] of Object.entries(regularProps)) {
|
|
229
|
+
if (value == null || value === false) {
|
|
230
|
+
el.removeAttribute(key)
|
|
231
|
+
} else if (value === true) {
|
|
232
|
+
el.setAttribute(key, '')
|
|
233
|
+
} else {
|
|
234
|
+
el.setAttribute(key, String(value))
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// Build the element props for React's createElement
|
|
240
|
+
const elementProps = { ref }
|
|
241
|
+
if (props.style) elementProps.style = props.style
|
|
242
|
+
if (props.className) elementProps.className = props.className
|
|
243
|
+
|
|
244
|
+
return React.createElement(tagName, elementProps, props.children)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
WccWrapper.displayName = tagName.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('')
|
|
248
|
+
|
|
249
|
+
return WccWrapper
|
|
250
|
+
}
|
|
251
|
+
|
package/package.json
CHANGED