Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 22 additions & 23 deletions packages/core/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import transforms, { R2WCType } from "./transforms"
import { toDashedCase, toCamelCase } from "./utils"
import { toDashedCase } from "./utils"

export type { R2WCElement } from "./transforms"

type PropName<Props> = Exclude<Extract<keyof Props, string>, "container">
type PropNames<Props> = Array<PropName<Props>>
Expand Down Expand Up @@ -106,26 +108,7 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
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) {
if (transform?.parse && (value || type === "method")) {
//@ts-ignore
this[propsSymbol][prop] = transform.parse(value, attribute, this)
}
Expand Down Expand Up @@ -164,7 +147,11 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
const type = propTypes[prop]
const transform = type ? transforms[type] : null

if (prop in propTypes && transform?.parse && value) {
if (
prop in propTypes &&
transform?.parse &&
(value || type === "method")
) {
//@ts-ignore
this[propsSymbol][prop] = transform.parse(value, attribute, this)

Expand Down Expand Up @@ -207,9 +194,21 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
const oldAttributeValue = this.getAttribute(attribute)

if (oldAttributeValue !== attributeValue) {
this.setAttribute(attribute, attributeValue)
if (attributeValue == null) {
this.removeAttribute(attribute)
} else {
this.setAttribute(attribute, attributeValue)
}
}
} else {
if (
prop in propTypes &&
transform?.parse &&
(value || type === "method")
) {
//@ts-ignore
this[propsSymbol][prop] = transform.parse(value, attribute, this)
}
this[renderSymbol]()
}
},
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/transforms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ import method_ from "./method"
import number from "./number"
import string from "./string"

export type R2WCElement = HTMLElement & {
container: R2WCElement
}

export interface Transform<Type> {
stringify?: (value: Type, attribute: string, element: HTMLElement) => string
parse: (value: string, attribute: string, element: HTMLElement) => Type
stringify?: (value: Type, attribute: string, element: R2WCElement) => string
parse: (
value: string,
attribute: string,
element: R2WCElement,
) => Type | undefined
}

const transforms = {
Expand Down
32 changes: 22 additions & 10 deletions packages/core/src/transforms/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,32 @@ import { toCamelCase } from "../utils"

import { Transform } from "./index"

const boundSymbol = Symbol.for("r2wc.bound")

const method_: Transform<(...args: unknown[]) => unknown> = {
stringify: (value) => value.name,
parse: (value, attribute, element) => {
const fn = (() => {
const functionName = toCamelCase(attribute)
const functionName = toCamelCase(attribute)

//@ts-expect-error
if (typeof element !== "undefined" && functionName in element.container) {
// @ts-expect-error
return element.container[functionName]
}
})()
const r2wcElement = element as typeof element & {
container: typeof r2wcElement
} & {
[k in typeof functionName]: (...args: unknown[]) => unknown
}

return typeof fn === "function" ? fn.bind(element) : undefined
if (
typeof r2wcElement !== "undefined" &&
functionName in r2wcElement &&
typeof r2wcElement[functionName] !== "undefined"
) {
let fn = r2wcElement[functionName]
if (!(boundSymbol in r2wcElement[functionName])) {
fn = fn.bind(r2wcElement)
Object.defineProperty(fn, boundSymbol, { value: true })
}
return fn
} else {
return undefined
}
},
}

Expand Down
141 changes: 75 additions & 66 deletions packages/react-to-web-component/src/react-to-web-component.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import PropTypes from "prop-types"
import React from "react"
import { describe, it, expect, assert } from "vitest"

import { R2WCElement } from "@r2wc/core"

import r2wc from "./react-to-web-component"

expect.extend(matchers)
Expand Down Expand Up @@ -364,90 +366,97 @@ describe("react-to-web-component 1", () => {
})
})

it("Supports class function to react props using method transform", async () => {
const ClassGreeting: React.FC<{ name: string; sayHello: () => void }> = ({
name,
sayHello,
}) => (
<div>
<h1>Hello, {name}</h1>
<button onClick={sayHello}>Click me</button>
</div>
)
it.each([[undefined], ["open"], ["closed"]])(
`Supports class function to react props using method transform: (shadow: %s)`,
async (shadow) => {
const ClassGreeting: React.FC<{ name: string; sayHello: () => void }> = ({
name,
sayHello,
}) => (
<div>
<h1>Hello, {name}</h1>
<button onClick={sayHello}>Click me</button>
</div>
)

const WebClassGreeting = r2wc(ClassGreeting, {
props: {
name: "string",
sayHello: "method",
},
})
const WebClassGreeting = r2wc(ClassGreeting, {
props: {
name: "string",
sayHello: "method",
},
shadow: shadow as unknown as Exclude<
Parameters<typeof r2wc>[1],
undefined
>["shadow"],
})

customElements.define("class-greeting", WebClassGreeting)
const tagName = `class-greeting${shadow ? `-${shadow}` : ""}`

document.body.innerHTML = `<class-greeting name='Christopher'></class-greeting>`
customElements.define(tagName, WebClassGreeting)

const el = document.querySelector<HTMLElement & { sayHello?: () => void }>(
"class-greeting",
)
document.body.innerHTML = `<${tagName} name='Christopher'></class-greeting>`

if (!el) {
throw new Error("Element not found")
}
const el = document.querySelector<
R2WCElement & { sayHello?: () => void }
>(tagName)

const sayHello = function (this: HTMLElement) {
const nameElement = this.querySelector("h1")
if (nameElement) {
nameElement.textContent = "Hello, again"
if (!el) {
throw new Error("Element not found")
}
}

el.sayHello = sayHello.bind(el)
const sayHello = function (this: R2WCElement) {
const nameElement = this.container.querySelector("h1")
if (nameElement) {
nameElement.textContent = "Hello, again"
}
}

await new Promise((resolve, reject) => {
const failIfNotClicked = setTimeout(() => {
reject()
}, 1000)
el.sayHello = sayHello

setTimeout(() => {
document
.querySelector<HTMLButtonElement>("class-greeting button")
?.click()
const docRoot = el.container.getRootNode() as Document | DocumentFragment

await new Promise((resolve, reject) => {
const failIfNotClicked = setTimeout(() => {
reject()
}, 1000)

setTimeout(() => {
const element = document.querySelector("h1")
expect(element?.textContent).toEqual("Hello, again")
clearTimeout(failIfNotClicked)
resolve(true)
docRoot.querySelector<HTMLButtonElement>(`button`)?.click()

setTimeout(() => {
const element = docRoot.querySelector("h1")
expect(element?.textContent).toEqual("Hello, again")
clearTimeout(failIfNotClicked)
resolve(true)
}, 0)
}, 0)
}, 0)
})
})

const sayHelloRerendered = function (this: HTMLElement) {
const nameElement = this.querySelector("h1")
if (nameElement) {
nameElement.textContent = "Hello, again rerendered"
const sayHelloRerendered = function (this: R2WCElement) {
const nameElement = this.container.querySelector("h1")
if (nameElement) {
nameElement.textContent = "Hello, again rerendered"
}
}
}

el.sayHello = sayHelloRerendered.bind(el)

await new Promise((resolve, reject) => {
const failIfNotClicked = setTimeout(() => {
reject()
}, 1000)
el.sayHello = sayHelloRerendered

setTimeout(() => {
document
.querySelector<HTMLButtonElement>("class-greeting button")
?.click()
await new Promise((resolve, reject) => {
const failIfNotClicked = setTimeout(() => {
reject()
}, 1000)

setTimeout(() => {
const element = document.querySelector<HTMLHeadingElement>("h1")
expect(element?.textContent).toEqual("Hello, again rerendered")
clearTimeout(failIfNotClicked)
resolve(true)
docRoot.querySelector<HTMLButtonElement>(`button`)?.click()

setTimeout(() => {
const element = docRoot.querySelector<HTMLHeadingElement>("h1")
expect(element?.textContent).toEqual("Hello, again rerendered")
clearTimeout(failIfNotClicked)
resolve(true)
}, 0)
}, 0)
}, 0)
})
})
})
},
)
})