Skip to content

Commit 9259f99

Browse files
authored
Feature: Add option to dispatch events instead of using global functions (#203)
* Add option to dispatch events instead of using global functions * adjusted implementation according to code review
1 parent fa9c8b4 commit 9259f99

File tree

3 files changed

+99
-0
lines changed

3 files changed

+99
-0
lines changed

docs/api.md

+39
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- `options` - An set of parameters.
88

99
- `options.shadow` - Use shadow DOM rather than light DOM.
10+
- `options.dispatchEvents` - Will cause dispatchEvent to be called for functions when attribute is not passed (this object should be same type as passed to [Event constructor options](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event#options))
1011
- `options.props` - Array of camelCasedProps to watch as String values or { [camelCasedProps]: "string" | "number" | "boolean" | "function" | "json" }
1112

1213
- When specifying Array or Object as the type, the string passed into the attribute must pass `JSON.parse()` requirements.
@@ -164,6 +165,8 @@ document.body.innerHTML = `
164165

165166
When `Function` is specified as the type, attribute values on the web component will be converted into function references when passed into the underlying React component. The string value of the attribute must be a valid reference to a function on `window` (or on `global`).
166167

168+
Note: If you want to avoid global functions, instead of passing attribute you can pass `dispatchEvents` object in options and simply listen on events using `addEventListener` on the custom element. See below.
169+
167170
```js
168171
function ThemeSelect({ handleClick }) {
169172
return (
@@ -198,3 +201,39 @@ setTimeout(
198201
)
199202
// ^ calls globalFn, logs: true, "Jane"
200203
```
204+
205+
### Event dispatching
206+
207+
When `Function` is specified as the type, instead of passing attribute values referencing global methods, you can simply listen on the DOM event.
208+
209+
```js
210+
function ThemeSelect({ onSelect }) {
211+
return (
212+
<div>
213+
<button onClick={() => onSelect("V")}>V</button>
214+
<button onClick={() => onSelect("Johnny")}>Johnny</button>
215+
<button onClick={() => onSelect("Jane")}>Jane</button>
216+
</div>
217+
)
218+
}
219+
220+
const WebThemeSelect = reactToWebComponent(ThemeSelect, {
221+
props: { onSelect: "function" },
222+
dispatchEvents: { bubbles: true }
223+
})
224+
225+
customElements.define("theme-select", WebThemeSelect)
226+
227+
document.body.innerHTML = "<theme-select></theme-select>"
228+
229+
setTimeout(() => {
230+
const element = document.querySelector("theme-select")
231+
element.addEventListener("select", (event) => {
232+
// "event.target" is the instance of the WebComponent / HTMLElement
233+
const thisIsEl = event.target === element
234+
console.log(thisIsEl, event.detail)
235+
})
236+
document.querySelector("theme-select button:last-child").click()
237+
}, 0)
238+
// ^ calls event listener, logs: true, "Jane"
239+
```

packages/core/src/core.ts

+22
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type PropNames<Props> = Array<PropName<Props>>
77
export interface R2WCOptions<Props> {
88
shadow?: "open" | "closed"
99
props?: PropNames<Props> | Partial<Record<PropName<Props>, R2WCType>>
10+
events?: PropNames<Props> | Partial<Record<PropName<Props>, EventInit>>
1011
}
1112

1213
export interface R2WCRenderer<Props, Context> {
@@ -45,12 +46,19 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
4546
? (Object.keys(ReactComponent.propTypes) as PropNames<Props>)
4647
: []
4748
}
49+
if (!options.events) {
50+
options.events = []
51+
}
4852

4953
const propNames = Array.isArray(options.props)
5054
? options.props.slice()
5155
: (Object.keys(options.props) as PropNames<Props>)
56+
const eventNames = Array.isArray(options.events)
57+
? options.events.slice()
58+
: (Object.keys(options.events) as PropNames<Props>)
5259

5360
const propTypes = {} as Partial<Record<PropName<Props>, R2WCType>>
61+
const eventParams = {} as Partial<Record<PropName<Props>, EventInit>>
5462
const mapPropAttribute = {} as Record<PropName<Props>, string>
5563
const mapAttributeProp = {} as Record<string, PropName<Props>>
5664
for (const prop of propNames) {
@@ -63,6 +71,11 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
6371
mapPropAttribute[prop] = attribute
6472
mapAttributeProp[attribute] = prop
6573
}
74+
for (const event of eventNames) {
75+
eventParams[event] = Array.isArray(options.events)
76+
? {}
77+
: options.events[event]
78+
}
6679

6780
class ReactWebComponent extends HTMLElement {
6881
static get observedAttributes() {
@@ -98,6 +111,15 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
98111
this[propsSymbol][prop] = transform.parse(value, attribute, this)
99112
}
100113
}
114+
for (const event of eventNames) {
115+
//@ts-ignore
116+
this[propsSymbol][event] = (detail) => {
117+
const name = event.replace(/^on/, "").toLowerCase()
118+
this.dispatchEvent(
119+
new CustomEvent(name, { detail, ...eventParams[event] }),
120+
)
121+
}
122+
}
101123
}
102124

103125
connectedCallback() {

packages/react-to-web-component/src/react-to-web-component.test.tsx

+38
Original file line numberDiff line numberDiff line change
@@ -325,4 +325,42 @@ describe("react-to-web-component 1", () => {
325325
}, 0)
326326
})
327327
})
328+
329+
it("Props typed as Function are dispatching events when events are enables via options", async () => {
330+
expect.assertions(2)
331+
332+
function ThemeSelect({ onSelect }: { onSelect: (arg: string) => void }) {
333+
return (
334+
<div>
335+
<button onClick={() => onSelect("V")}>V</button>
336+
<button onClick={() => onSelect("Johnny")}>Johnny</button>
337+
<button onClick={() => onSelect("Jane")}>Jane</button>
338+
</div>
339+
)
340+
}
341+
342+
const WebThemeSelect = r2wc(ThemeSelect, {
343+
events: { onSelect: { bubbles: true } },
344+
})
345+
customElements.define("theme-select-events", WebThemeSelect)
346+
document.body.innerHTML = "<theme-select-events></theme-select-events>"
347+
348+
await new Promise((resolve, reject) => {
349+
const failUnlessCleared = setTimeout(() => {
350+
reject("event listener was not called to clear the failure timeout")
351+
}, 1000)
352+
353+
const element = document.querySelector("theme-select-events")
354+
element?.addEventListener("select", (event) => {
355+
clearTimeout(failUnlessCleared)
356+
expect((event as CustomEvent).detail).toEqual("Jane")
357+
expect(event.target).toEqual(element)
358+
resolve(true)
359+
})
360+
const button = element?.querySelector(
361+
"button:last-child",
362+
) as HTMLButtonElement
363+
button.click()
364+
})
365+
})
328366
})

0 commit comments

Comments
 (0)