Skip to content

Commit d7f2715

Browse files
Fix method transforms when shadow DOM is used (#231)
* Fix method transforms when shadow DOM is used
1 parent 7c6bec4 commit d7f2715

4 files changed

Lines changed: 129 additions & 101 deletions

File tree

packages/core/src/core.ts

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import transforms, { R2WCType } from "./transforms"
2-
import { toDashedCase, toCamelCase } from "./utils"
2+
import { toDashedCase } from "./utils"
3+
4+
export type { R2WCElement } from "./transforms"
35

46
type PropName<Props> = Exclude<Extract<keyof Props, string>, "container">
57
type PropNames<Props> = Array<PropName<Props>>
@@ -106,26 +108,7 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
106108
const type = propTypes[prop]
107109
const transform = type ? transforms[type] : null
108110

109-
if (type === "method") {
110-
const methodName = toCamelCase(attribute)
111-
112-
Object.defineProperty(this[propsSymbol].container, methodName, {
113-
enumerable: true,
114-
configurable: true,
115-
get() {
116-
return this[propsSymbol][methodName]
117-
},
118-
set(value) {
119-
this[propsSymbol][methodName] = value
120-
this[renderSymbol]()
121-
},
122-
})
123-
124-
//@ts-ignore
125-
this[propsSymbol][prop] = transform.parse(value, attribute, this)
126-
}
127-
128-
if (transform?.parse && value) {
111+
if (transform?.parse && (value || type === "method")) {
129112
//@ts-ignore
130113
this[propsSymbol][prop] = transform.parse(value, attribute, this)
131114
}
@@ -164,7 +147,11 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
164147
const type = propTypes[prop]
165148
const transform = type ? transforms[type] : null
166149

167-
if (prop in propTypes && transform?.parse && value) {
150+
if (
151+
prop in propTypes &&
152+
transform?.parse &&
153+
(value || type === "method")
154+
) {
168155
//@ts-ignore
169156
this[propsSymbol][prop] = transform.parse(value, attribute, this)
170157

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

209196
if (oldAttributeValue !== attributeValue) {
210-
this.setAttribute(attribute, attributeValue)
197+
if (attributeValue == null) {
198+
this.removeAttribute(attribute)
199+
} else {
200+
this.setAttribute(attribute, attributeValue)
201+
}
211202
}
212203
} else {
204+
if (
205+
prop in propTypes &&
206+
transform?.parse &&
207+
(value || type === "method")
208+
) {
209+
//@ts-ignore
210+
this[propsSymbol][prop] = transform.parse(value, attribute, this)
211+
}
213212
this[renderSymbol]()
214213
}
215214
},

packages/core/src/transforms/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ import method_ from "./method"
55
import number from "./number"
66
import string from "./string"
77

8+
export type R2WCElement = HTMLElement & {
9+
container: R2WCElement
10+
}
11+
812
export interface Transform<Type> {
9-
stringify?: (value: Type, attribute: string, element: HTMLElement) => string
10-
parse: (value: string, attribute: string, element: HTMLElement) => Type
13+
stringify?: (value: Type, attribute: string, element: R2WCElement) => string
14+
parse: (
15+
value: string,
16+
attribute: string,
17+
element: R2WCElement,
18+
) => Type | undefined
1119
}
1220

1321
const transforms = {

packages/core/src/transforms/method.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,32 @@ import { toCamelCase } from "../utils"
22

33
import { Transform } from "./index"
44

5+
const boundSymbol = Symbol.for("r2wc.bound")
6+
57
const method_: Transform<(...args: unknown[]) => unknown> = {
6-
stringify: (value) => value.name,
78
parse: (value, attribute, element) => {
8-
const fn = (() => {
9-
const functionName = toCamelCase(attribute)
9+
const functionName = toCamelCase(attribute)
1010

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

18-
return typeof fn === "function" ? fn.bind(element) : undefined
17+
if (
18+
typeof r2wcElement !== "undefined" &&
19+
functionName in r2wcElement &&
20+
typeof r2wcElement[functionName] !== "undefined"
21+
) {
22+
let fn = r2wcElement[functionName]
23+
if (!(boundSymbol in r2wcElement[functionName])) {
24+
fn = fn.bind(r2wcElement)
25+
Object.defineProperty(fn, boundSymbol, { value: true })
26+
}
27+
return fn
28+
} else {
29+
return undefined
30+
}
1931
},
2032
}
2133

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

Lines changed: 75 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import PropTypes from "prop-types"
44
import React from "react"
55
import { describe, it, expect, assert } from "vitest"
66

7+
import { R2WCElement } from "@r2wc/core"
8+
79
import r2wc from "./react-to-web-component"
810

911
expect.extend(matchers)
@@ -364,90 +366,97 @@ describe("react-to-web-component 1", () => {
364366
})
365367
})
366368

367-
it("Supports class function to react props using method transform", async () => {
368-
const ClassGreeting: React.FC<{ name: string; sayHello: () => void }> = ({
369-
name,
370-
sayHello,
371-
}) => (
372-
<div>
373-
<h1>Hello, {name}</h1>
374-
<button onClick={sayHello}>Click me</button>
375-
</div>
376-
)
369+
it.each([[undefined], ["open"], ["closed"]])(
370+
`Supports class function to react props using method transform: (shadow: %s)`,
371+
async (shadow) => {
372+
const ClassGreeting: React.FC<{ name: string; sayHello: () => void }> = ({
373+
name,
374+
sayHello,
375+
}) => (
376+
<div>
377+
<h1>Hello, {name}</h1>
378+
<button onClick={sayHello}>Click me</button>
379+
</div>
380+
)
377381

378-
const WebClassGreeting = r2wc(ClassGreeting, {
379-
props: {
380-
name: "string",
381-
sayHello: "method",
382-
},
383-
})
382+
const WebClassGreeting = r2wc(ClassGreeting, {
383+
props: {
384+
name: "string",
385+
sayHello: "method",
386+
},
387+
shadow: shadow as unknown as Exclude<
388+
Parameters<typeof r2wc>[1],
389+
undefined
390+
>["shadow"],
391+
})
384392

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

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

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

393-
if (!el) {
394-
throw new Error("Element not found")
395-
}
399+
const el = document.querySelector<
400+
R2WCElement & { sayHello?: () => void }
401+
>(tagName)
396402

397-
const sayHello = function (this: HTMLElement) {
398-
const nameElement = this.querySelector("h1")
399-
if (nameElement) {
400-
nameElement.textContent = "Hello, again"
403+
if (!el) {
404+
throw new Error("Element not found")
401405
}
402-
}
403406

404-
el.sayHello = sayHello.bind(el)
407+
const sayHello = function (this: R2WCElement) {
408+
const nameElement = this.container.querySelector("h1")
409+
if (nameElement) {
410+
nameElement.textContent = "Hello, again"
411+
}
412+
}
405413

406-
await new Promise((resolve, reject) => {
407-
const failIfNotClicked = setTimeout(() => {
408-
reject()
409-
}, 1000)
414+
el.sayHello = sayHello
410415

411-
setTimeout(() => {
412-
document
413-
.querySelector<HTMLButtonElement>("class-greeting button")
414-
?.click()
416+
const docRoot = el.container.getRootNode() as Document | DocumentFragment
417+
418+
await new Promise((resolve, reject) => {
419+
const failIfNotClicked = setTimeout(() => {
420+
reject()
421+
}, 1000)
415422

416423
setTimeout(() => {
417-
const element = document.querySelector("h1")
418-
expect(element?.textContent).toEqual("Hello, again")
419-
clearTimeout(failIfNotClicked)
420-
resolve(true)
424+
docRoot.querySelector<HTMLButtonElement>(`button`)?.click()
425+
426+
setTimeout(() => {
427+
const element = docRoot.querySelector("h1")
428+
expect(element?.textContent).toEqual("Hello, again")
429+
clearTimeout(failIfNotClicked)
430+
resolve(true)
431+
}, 0)
421432
}, 0)
422-
}, 0)
423-
})
433+
})
424434

425-
const sayHelloRerendered = function (this: HTMLElement) {
426-
const nameElement = this.querySelector("h1")
427-
if (nameElement) {
428-
nameElement.textContent = "Hello, again rerendered"
435+
const sayHelloRerendered = function (this: R2WCElement) {
436+
const nameElement = this.container.querySelector("h1")
437+
if (nameElement) {
438+
nameElement.textContent = "Hello, again rerendered"
439+
}
429440
}
430-
}
431-
432-
el.sayHello = sayHelloRerendered.bind(el)
433441

434-
await new Promise((resolve, reject) => {
435-
const failIfNotClicked = setTimeout(() => {
436-
reject()
437-
}, 1000)
442+
el.sayHello = sayHelloRerendered
438443

439-
setTimeout(() => {
440-
document
441-
.querySelector<HTMLButtonElement>("class-greeting button")
442-
?.click()
444+
await new Promise((resolve, reject) => {
445+
const failIfNotClicked = setTimeout(() => {
446+
reject()
447+
}, 1000)
443448

444449
setTimeout(() => {
445-
const element = document.querySelector<HTMLHeadingElement>("h1")
446-
expect(element?.textContent).toEqual("Hello, again rerendered")
447-
clearTimeout(failIfNotClicked)
448-
resolve(true)
450+
docRoot.querySelector<HTMLButtonElement>(`button`)?.click()
451+
452+
setTimeout(() => {
453+
const element = docRoot.querySelector<HTMLHeadingElement>("h1")
454+
expect(element?.textContent).toEqual("Hello, again rerendered")
455+
clearTimeout(failIfNotClicked)
456+
resolve(true)
457+
}, 0)
449458
}, 0)
450-
}, 0)
451-
})
452-
})
459+
})
460+
},
461+
)
453462
})

0 commit comments

Comments
 (0)