diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 36938ce..c776d24 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -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 = Exclude, "container"> type PropNames = Array> @@ -106,26 +108,7 @@ 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) { + if (transform?.parse && (value || type === "method")) { //@ts-ignore this[propsSymbol][prop] = transform.parse(value, attribute, this) } @@ -164,7 +147,11 @@ export default function r2wc( 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) @@ -207,9 +194,21 @@ export default function r2wc( 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]() } }, diff --git a/packages/core/src/transforms/index.ts b/packages/core/src/transforms/index.ts index 3bb4e9c..27f41d1 100644 --- a/packages/core/src/transforms/index.ts +++ b/packages/core/src/transforms/index.ts @@ -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 { - 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 = { diff --git a/packages/core/src/transforms/method.ts b/packages/core/src/transforms/method.ts index 326209a..e31c5ff 100644 --- a/packages/core/src/transforms/method.ts +++ b/packages/core/src/transforms/method.ts @@ -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 + } }, } 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 f8e5248..bef0391 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 @@ -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) @@ -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, - }) => ( -
-

Hello, {name}

- -
- ) + 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, + }) => ( +
+

Hello, {name}

+ +
+ ) - 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[1], + undefined + >["shadow"], + }) - customElements.define("class-greeting", WebClassGreeting) + const tagName = `class-greeting${shadow ? `-${shadow}` : ""}` - document.body.innerHTML = `` + customElements.define(tagName, WebClassGreeting) - const el = document.querySelector void }>( - "class-greeting", - ) + document.body.innerHTML = `<${tagName} name='Christopher'>` - 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("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(`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("class-greeting button") - ?.click() + await new Promise((resolve, reject) => { + const failIfNotClicked = setTimeout(() => { + reject() + }, 1000) setTimeout(() => { - const element = document.querySelector("h1") - expect(element?.textContent).toEqual("Hello, again rerendered") - clearTimeout(failIfNotClicked) - resolve(true) + docRoot.querySelector(`button`)?.click() + + setTimeout(() => { + const element = docRoot.querySelector("h1") + expect(element?.textContent).toEqual("Hello, again rerendered") + clearTimeout(failIfNotClicked) + resolve(true) + }, 0) }, 0) - }, 0) - }) - }) + }) + }, + ) })