Skip to content

Commit

Permalink
Feature: Add option to dispatch events instead of using global functi…
Browse files Browse the repository at this point in the history
…ons (#203)

* Add option to dispatch events instead of using global functions
* adjusted implementation according to code review
  • Loading branch information
shahata authored Nov 12, 2024
1 parent fa9c8b4 commit 9259f99
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 0 deletions.
39 changes: 39 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- `options` - An set of parameters.

- `options.shadow` - Use shadow DOM rather than light DOM.
- `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))
- `options.props` - Array of camelCasedProps to watch as String values or { [camelCasedProps]: "string" | "number" | "boolean" | "function" | "json" }

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

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`).

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.

```js
function ThemeSelect({ handleClick }) {
return (
Expand Down Expand Up @@ -198,3 +201,39 @@ setTimeout(
)
// ^ calls globalFn, logs: true, "Jane"
```

### Event dispatching

When `Function` is specified as the type, instead of passing attribute values referencing global methods, you can simply listen on the DOM event.

```js
function ThemeSelect({ onSelect }) {
return (
<div>
<button onClick={() => onSelect("V")}>V</button>
<button onClick={() => onSelect("Johnny")}>Johnny</button>
<button onClick={() => onSelect("Jane")}>Jane</button>
</div>
)
}

const WebThemeSelect = reactToWebComponent(ThemeSelect, {
props: { onSelect: "function" },
dispatchEvents: { bubbles: true }
})

customElements.define("theme-select", WebThemeSelect)

document.body.innerHTML = "<theme-select></theme-select>"

setTimeout(() => {
const element = document.querySelector("theme-select")
element.addEventListener("select", (event) => {
// "event.target" is the instance of the WebComponent / HTMLElement
const thisIsEl = event.target === element
console.log(thisIsEl, event.detail)
})
document.querySelector("theme-select button:last-child").click()
}, 0)
// ^ calls event listener, logs: true, "Jane"
```
22 changes: 22 additions & 0 deletions packages/core/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type PropNames<Props> = Array<PropName<Props>>
export interface R2WCOptions<Props> {
shadow?: "open" | "closed"
props?: PropNames<Props> | Partial<Record<PropName<Props>, R2WCType>>
events?: PropNames<Props> | Partial<Record<PropName<Props>, EventInit>>
}

export interface R2WCRenderer<Props, Context> {
Expand Down Expand Up @@ -45,12 +46,19 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
? (Object.keys(ReactComponent.propTypes) as PropNames<Props>)
: []
}
if (!options.events) {
options.events = []
}

const propNames = Array.isArray(options.props)
? options.props.slice()
: (Object.keys(options.props) as PropNames<Props>)
const eventNames = Array.isArray(options.events)
? options.events.slice()
: (Object.keys(options.events) as PropNames<Props>)

const propTypes = {} as Partial<Record<PropName<Props>, R2WCType>>
const eventParams = {} as Partial<Record<PropName<Props>, EventInit>>
const mapPropAttribute = {} as Record<PropName<Props>, string>
const mapAttributeProp = {} as Record<string, PropName<Props>>
for (const prop of propNames) {
Expand All @@ -63,6 +71,11 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
mapPropAttribute[prop] = attribute
mapAttributeProp[attribute] = prop
}
for (const event of eventNames) {
eventParams[event] = Array.isArray(options.events)
? {}
: options.events[event]
}

class ReactWebComponent extends HTMLElement {
static get observedAttributes() {
Expand Down Expand Up @@ -98,6 +111,15 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
this[propsSymbol][prop] = transform.parse(value, attribute, this)
}
}
for (const event of eventNames) {
//@ts-ignore
this[propsSymbol][event] = (detail) => {
const name = event.replace(/^on/, "").toLowerCase()
this.dispatchEvent(
new CustomEvent(name, { detail, ...eventParams[event] }),
)
}
}
}

connectedCallback() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,4 +325,42 @@ describe("react-to-web-component 1", () => {
}, 0)
})
})

it("Props typed as Function are dispatching events when events are enables via options", async () => {
expect.assertions(2)

function ThemeSelect({ onSelect }: { onSelect: (arg: string) => void }) {
return (
<div>
<button onClick={() => onSelect("V")}>V</button>
<button onClick={() => onSelect("Johnny")}>Johnny</button>
<button onClick={() => onSelect("Jane")}>Jane</button>
</div>
)
}

const WebThemeSelect = r2wc(ThemeSelect, {
events: { onSelect: { bubbles: true } },
})
customElements.define("theme-select-events", WebThemeSelect)
document.body.innerHTML = "<theme-select-events></theme-select-events>"

await new Promise((resolve, reject) => {
const failUnlessCleared = setTimeout(() => {
reject("event listener was not called to clear the failure timeout")
}, 1000)

const element = document.querySelector("theme-select-events")
element?.addEventListener("select", (event) => {
clearTimeout(failUnlessCleared)
expect((event as CustomEvent).detail).toEqual("Jane")
expect(event.target).toEqual(element)
resolve(true)
})
const button = element?.querySelector(
"button:last-child",
) as HTMLButtonElement
button.click()
})
})
})

0 comments on commit 9259f99

Please sign in to comment.