@sprlab/wccompiler 0.9.9 → 0.10.0
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/react.js +155 -7
- package/package.json +1 -1
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