@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.
@@ -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
+ }
@@ -1,146 +1,42 @@
1
1
  /**
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
- * ═══════════════════════════════════════════════════════════════════════════════
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
- // ── 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
- // ```
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.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Exports:
5
5
  * - WccSlotDef: Auxiliary directive for ng-template[slot]
6
- * - WccSlotsDirective: Main directive that auto-activates on custom elements
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
- * 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).
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: EXCLUSION_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
+ }
@@ -1,126 +1,30 @@
1
1
  /**
2
- * React hook for WCC custom element events.
3
- * Bridges CustomEvent to React's ref-based event system.
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
- * Usage:
8
- * // Form 1: Pass an existing ref
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
- * // Form 2: Let the hook create the ref
14
- * const ref = useWccEvent('change', (e) => console.log(e.detail))
15
- * <wcc-counter ref={ref}></wcc-counter>
10
+ * @example vite.config.js
11
+ * ```js
12
+ * import { wccReactPlugin } from '@sprlab/wccompiler/integrations/react'
13
+ * export default { plugins: [wccReactPlugin()] }
14
+ * ```
16
15
  *
17
- * // Form 3: Two-way binding with defineModel
18
- * const [value, setValue] = useState('')
19
- * const ref = useWccModel('value', value, setValue)
20
- * <wcc-input ref={ref}></wcc-input>
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
- lines.push(` if (this.__slotTpl_${s.name}) {`);
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
- // Task 3.1: Store current props in __slotProps
1295
+ // Store current props for late-registering renderers
1295
1296
  lines.push(` this.__slotProps['${s.name}'] = __props;`);
1296
- // Task 3.2: Emit wcc:slot-update event
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
- // Task 3.3: Check for registered renderer, skip token replacement if present
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(' } else {');
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",
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"