diff --git a/docs/api.md b/docs/api.md
index 3fd50de..ae2fcdc 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -9,7 +9,7 @@
- `options.shadow` - ("open", "closed", or undefined) Use the specified shadow DOM mode rather than light DOM.
- `options.events` - Array of camelCasedProps to dispatch as custom events or a Record of event names to their associated [Event constructor options](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event#options).
- When dispatching events from named properties, "on" is stripped from the beginning of the property name if present, and the result is lowercased: the property `onMyCustomEvent` dispatches as "mycustomevent".
- - `options.props` - Array of camelCasedProps to watch as String values or { [camelCasedProps]: "string" | "number" | "boolean" | "function" | "json" }
+ - `options.props` - Array of camelCasedProps to watch as String values or { [camelCasedProps]: "string" | "number" | "boolean" | "function" | "method" | "json" }
- When specifying Array or Object as the type, the string passed into the attribute must pass `JSON.parse()` requirements.
- When specifying Boolean as the type, "true", "1", "yes", "TRUE", and "t" are mapped to `true`. All strings NOT begining with t, T, 1, y, or Y will be `false`.
@@ -109,11 +109,11 @@ console.log(document.body.firstElementChild.innerHTML) // "
Hello, Jane
"
If `options.props` is an object, the keys are the camelCased React props and the values are any one of the following built in javascript types.
This is the recommended way of passing props to r2wc.
-`"string" | "number" | "boolean" | "function" | "json"`
+`"string" | "number" | "boolean" | "function" | "method" | "json"`
"json" can be an array or object. The string passed into the attribute must pass `JSON.parse()` requirements.
-### "string" | "number" | "boolean" | "function" | "json" props
+### "string" | "number" | "boolean" | "function" | "method" | "json" props
```js
function AttrPropTypeCasting(props) {
@@ -203,6 +203,49 @@ setTimeout(
// ^ calls globalFn, logs: true, "Jane"
```
+
+### Method props
+
+When `method` is specified as the type, the prop will be bound to a method that can be defined directly on the custom element instance. Unlike `function` props that reference global functions, `method` props allow you to define class methods directly on the web component element, providing better encapsulation and avoiding global namespace pollution.
+
+This is particularly useful when you want to pass functions from parent components or when you need to define behavior specific to each web component instance.
+
+```js
+function ClassGreeting({ name, sayHello }) {
+ return (
+
+
Hello, {name}
+
+
+ )
+}
+
+const WebClassGreeting = reactToWebComponent(ClassGreeting, {
+ props: {
+ name: "string",
+ sayHello: "method",
+ },
+})
+
+customElements.define("class-greeting", WebClassGreeting)
+
+
+document.body.innerHTML = ''
+
+const element = document.querySelector("class-greeting")
+
+const myMethod = function(this: HTMLElement) {
+ const nameElement = this.querySelector("h1") as HTMLElement;
+ nameElement.textContent = "Hello, again rerendered";
+}
+
+element.sayHello = myMethod.bind(element)
+
+setTimeout(() => {
+ document.querySelector("class-greeting button").click()
+}, 0)
+```
+
### Event dispatching
As an alternative to using function props, the `events` object insructs r2wc to dispatch a corresponding DOM event that can be listened to on the custom element itself, on ancestor elements using `bubbles`, and outside of any containing shadow DOM using `composed`.
diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts
index 8f144af..36938ce 100644
--- a/packages/core/src/core.ts
+++ b/packages/core/src/core.ts
@@ -1,5 +1,5 @@
import transforms, { R2WCType } from "./transforms"
-import { toDashedCase } from "./utils"
+import { toDashedCase, toCamelCase } from "./utils"
type PropName = Exclude, "container">
type PropNames = Array>
@@ -34,7 +34,7 @@ const propsSymbol = Symbol.for("r2wc.props")
* @param {ReactComponent}
* @param {Object} options - Optional parameters
* @param {String?} options.shadow - Shadow DOM mode as either open or closed.
- * @param {Object|Array?} options.props - Array of camelCasedProps to watch as Strings or { [camelCasedProp]: "string" | "number" | "boolean" | "function" | "json" }
+ * @param {Object|Array?} options.props - Array of camelCasedProps to watch as Strings or { [camelCasedProp]: "string" | "number" | "boolean" | "function" | "method" | "json" }
*/
export default function r2wc(
ReactComponent: React.ComponentType,
@@ -106,6 +106,25 @@ export default function r2wc(
const type = propTypes[prop]
const transform = type ? transforms[type] : null
+ if (type === "method") {
+ const methodName = toCamelCase(attribute)
+
+ Object.defineProperty(this[propsSymbol].container, methodName, {
+ enumerable: true,
+ configurable: true,
+ get() {
+ return this[propsSymbol][methodName]
+ },
+ set(value) {
+ this[propsSymbol][methodName] = value
+ this[renderSymbol]()
+ },
+ })
+
+ //@ts-ignore
+ this[propsSymbol][prop] = transform.parse(value, attribute, this)
+ }
+
if (transform?.parse && value) {
//@ts-ignore
this[propsSymbol][prop] = transform.parse(value, attribute, this)
diff --git a/packages/core/src/transforms/index.ts b/packages/core/src/transforms/index.ts
index 4804692..3bb4e9c 100644
--- a/packages/core/src/transforms/index.ts
+++ b/packages/core/src/transforms/index.ts
@@ -1,6 +1,7 @@
import boolean from "./boolean"
import function_ from "./function"
import json from "./json"
+import method_ from "./method"
import number from "./number"
import string from "./string"
@@ -14,6 +15,7 @@ const transforms = {
number,
boolean,
function: function_,
+ method: method_,
json,
}
diff --git a/packages/core/src/transforms/method.ts b/packages/core/src/transforms/method.ts
new file mode 100644
index 0000000..326209a
--- /dev/null
+++ b/packages/core/src/transforms/method.ts
@@ -0,0 +1,22 @@
+import { toCamelCase } from "../utils"
+
+import { Transform } from "./index"
+
+const method_: Transform<(...args: unknown[]) => unknown> = {
+ stringify: (value) => value.name,
+ parse: (value, attribute, element) => {
+ const fn = (() => {
+ const functionName = toCamelCase(attribute)
+
+ //@ts-expect-error
+ if (typeof element !== "undefined" && functionName in element.container) {
+ // @ts-expect-error
+ return element.container[functionName]
+ }
+ })()
+
+ return typeof fn === "function" ? fn.bind(element) : undefined
+ },
+}
+
+export default method_
diff --git a/packages/react-to-web-component/src/react-to-web-component.test.tsx b/packages/react-to-web-component/src/react-to-web-component.test.tsx
index 4d89e7e..f8e5248 100644
--- a/packages/react-to-web-component/src/react-to-web-component.test.tsx
+++ b/packages/react-to-web-component/src/react-to-web-component.test.tsx
@@ -363,4 +363,91 @@ describe("react-to-web-component 1", () => {
button.click()
})
})
+
+ it("Supports class function to react props using method transform", async () => {
+ const ClassGreeting: React.FC<{ name: string; sayHello: () => void }> = ({
+ name,
+ sayHello,
+ }) => (
+