@sprlab/wccompiler 0.9.2 → 0.9.4

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