Skip to content

Commit eac9783

Browse files
Fix method transforms when shadow DOM is used
1 parent 7c6bec4 commit eac9783

4 files changed

Lines changed: 65 additions & 44 deletions

File tree

packages/core/src/core.ts

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import transforms, { R2WCType } from "./transforms"
2-
import { toDashedCase, toCamelCase } from "./utils"
2+
import { toDashedCase } from "./utils"
33

44
type PropName<Props> = Exclude<Extract<keyof Props, string>, "container">
55
type PropNames<Props> = Array<PropName<Props>>
@@ -106,26 +106,7 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
106106
const type = propTypes[prop]
107107
const transform = type ? transforms[type] : null
108108

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) {
109+
if (transform?.parse && (value || type === "method")) {
129110
//@ts-ignore
130111
this[propsSymbol][prop] = transform.parse(value, attribute, this)
131112
}
@@ -164,7 +145,7 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
164145
const type = propTypes[prop]
165146
const transform = type ? transforms[type] : null
166147

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

@@ -207,9 +188,17 @@ export default function r2wc<Props extends R2WCBaseProps, Context>(
207188
const oldAttributeValue = this.getAttribute(attribute)
208189

209190
if (oldAttributeValue !== attributeValue) {
210-
this.setAttribute(attribute, attributeValue)
191+
if (attributeValue == null) {
192+
this.removeAttribute(attribute)
193+
} else {
194+
this.setAttribute(attribute, attributeValue)
195+
}
211196
}
212197
} else {
198+
if (prop in propTypes && transform?.parse && (value || type === "method")) {
199+
//@ts-ignore
200+
this[propsSymbol][prop] = transform.parse(value, attribute, this)
201+
}
213202
this[renderSymbol]()
214203
}
215204
},

packages/core/src/transforms/index.ts

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

8+
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: (value: string, attribute: string, element: R2WCElement) => Type
1115
}
1216

1317
const transforms = {

packages/core/src/transforms/method.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,35 @@ 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) => {
89
const fn = (() => {
910
const functionName = toCamelCase(attribute)
1011

11-
//@ts-expect-error
12-
if (typeof element !== "undefined" && functionName in element.container) {
13-
// @ts-expect-error
14-
return element.container[functionName]
12+
const r2wcElement = element as typeof element & {
13+
container: typeof r2wcElement
14+
} & {
15+
[k in typeof functionName]: Function
16+
}
17+
18+
if (typeof r2wcElement !== "undefined") {
19+
if (
20+
typeof r2wcElement !== "undefined"
21+
&& functionName in r2wcElement && typeof r2wcElement[functionName] !== "undefined"
22+
) {
23+
let fn = r2wcElement[functionName]
24+
if (!(boundSymbol in r2wcElement[functionName])) {
25+
fn = fn.bind(r2wcElement)
26+
Object.defineProperty(fn, boundSymbol, { value: true })
27+
}
28+
return fn
29+
} else {
30+
return null
31+
}
32+
} else {
33+
return null
1534
}
1635
})()
1736

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

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,11 @@ describe("react-to-web-component 1", () => {
364364
})
365365
})
366366

367-
it("Supports class function to react props using method transform", async () => {
367+
it.each(
368+
[[undefined], ["open"], ["closed"]]
369+
)(
370+
`Supports class function to react props using method transform: (shadow: %s)`,
371+
async (shadow) => {
368372
const ClassGreeting: React.FC<{ name: string; sayHello: () => void }> = ({
369373
name,
370374
sayHello,
@@ -380,41 +384,46 @@ describe("react-to-web-component 1", () => {
380384
name: "string",
381385
sayHello: "method",
382386
},
387+
shadow: shadow as unknown as Exclude<Parameters<typeof r2wc>[1], undefined>['shadow'],
383388
})
384389

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

387-
document.body.innerHTML = `<class-greeting name='Christopher'></class-greeting>`
392+
customElements.define(tagName, WebClassGreeting)
393+
394+
document.body.innerHTML = `<${tagName} name='Christopher'></class-greeting>`
388395

389396
const el = document.querySelector<HTMLElement & { sayHello?: () => void }>(
390-
"class-greeting",
397+
tagName,
391398
)
392399

393400
if (!el) {
394401
throw new Error("Element not found")
395402
}
396403

397404
const sayHello = function (this: HTMLElement) {
398-
const nameElement = this.querySelector("h1")
405+
const nameElement = this.container.querySelector("h1")
399406
if (nameElement) {
400407
nameElement.textContent = "Hello, again"
401408
}
402409
}
403410

404-
el.sayHello = sayHello.bind(el)
411+
el.sayHello = sayHello//.bind(el)
412+
413+
const docRoot = el.container.getRootNode()
405414

406415
await new Promise((resolve, reject) => {
407416
const failIfNotClicked = setTimeout(() => {
408417
reject()
409418
}, 1000)
410419

411420
setTimeout(() => {
412-
document
413-
.querySelector<HTMLButtonElement>("class-greeting button")
421+
docRoot
422+
.querySelector<HTMLButtonElement>(`button`)
414423
?.click()
415424

416425
setTimeout(() => {
417-
const element = document.querySelector("h1")
426+
const element = docRoot.querySelector("h1")
418427
expect(element?.textContent).toEqual("Hello, again")
419428
clearTimeout(failIfNotClicked)
420429
resolve(true)
@@ -423,26 +432,26 @@ describe("react-to-web-component 1", () => {
423432
})
424433

425434
const sayHelloRerendered = function (this: HTMLElement) {
426-
const nameElement = this.querySelector("h1")
435+
const nameElement = this.container.querySelector("h1")
427436
if (nameElement) {
428437
nameElement.textContent = "Hello, again rerendered"
429438
}
430439
}
431440

432-
el.sayHello = sayHelloRerendered.bind(el)
441+
el.sayHello = sayHelloRerendered//.bind(el)
433442

434443
await new Promise((resolve, reject) => {
435444
const failIfNotClicked = setTimeout(() => {
436445
reject()
437446
}, 1000)
438447

439448
setTimeout(() => {
440-
document
441-
.querySelector<HTMLButtonElement>("class-greeting button")
449+
docRoot
450+
.querySelector<HTMLButtonElement>(`button`)
442451
?.click()
443452

444453
setTimeout(() => {
445-
const element = document.querySelector<HTMLHeadingElement>("h1")
454+
const element = docRoot.querySelector<HTMLHeadingElement>("h1")
446455
expect(element?.textContent).toEqual("Hello, again rerendered")
447456
clearTimeout(failIfNotClicked)
448457
resolve(true)

0 commit comments

Comments
 (0)