Skip to content

Commit f9e5239

Browse files
zerico007Christopher J Baker
and
Christopher J Baker
authored
wrap legacy code around core functionality (bitovi#100)
* initial commit * prettier check * misc code improvement * start updating to match monorepo architecture * created legacy package * created github workflow * package-lock update * update legacy yml * misc prettier fix * more fixes * renderer change in core * more updates * added unit tests for react * prettier --------- Co-authored-by: Christopher J Baker <[email protected]>
1 parent c786f79 commit f9e5239

13 files changed

+754
-380
lines changed

.github/workflows/legacy.yml

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: "react-to-webcomponent"
2+
3+
on:
4+
push:
5+
workflow_dispatch:
6+
inputs:
7+
segment:
8+
description: "The version segment to increment: major, minor, patch, or prerelease."
9+
required: true
10+
preId:
11+
description: "Appended to the prerelease segment. (default: \"\")"
12+
13+
jobs:
14+
verify:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v3
20+
21+
- name: Verify
22+
uses: ./.github/actions/job-verify
23+
24+
publish:
25+
if: github.event_name == 'workflow_dispatch'
26+
needs: verify
27+
28+
concurrency:
29+
group: "publish"
30+
31+
runs-on: ubuntu-latest
32+
permissions:
33+
contents: write
34+
packages: write
35+
36+
steps:
37+
- name: Checkout
38+
uses: actions/checkout@v3
39+
40+
- name: Publish
41+
uses: ./.github/actions/job-publish
42+
with:
43+
segment: ${{ github.event.inputs.segment }}
44+
preId: ${{ github.event.inputs.preId }}

package-lock.json

+4-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/src/core.test.tsx

+17-8
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,19 @@ import r2wc from "./core"
66

77
expect.extend(matchers)
88

9-
const mount = vi.fn()
9+
const mountCheck = vi.fn()
1010
const unmount = vi.fn()
11+
const update = vi.fn()
1112
const onUpdated = vi.fn()
1213

14+
const mount = (el: HTMLElement, reactComponent: any, _props: any) => {
15+
mountCheck()
16+
return {
17+
reactContainer: el,
18+
component: reactComponent,
19+
}
20+
}
21+
1322
function flushPromises() {
1423
return new Promise((resolve) => setImmediate(resolve))
1524
}
@@ -24,13 +33,13 @@ describe("core", () => {
2433
return <div>hello</div>
2534
}
2635

27-
const TestElement = r2wc(TestComponent, {}, { mount, unmount })
36+
const TestElement = r2wc(TestComponent, {}, { mount, unmount, update })
2837
customElements.define("test-func-element", TestElement)
2938

3039
const testEl = new TestElement()
3140

3241
document.body.appendChild(testEl)
33-
expect(mount).toBeCalledTimes(1)
42+
expect(mountCheck).toBeCalledTimes(1)
3443

3544
document.body.removeChild(testEl)
3645
expect(unmount).toBeCalledTimes(1)
@@ -43,13 +52,13 @@ describe("core", () => {
4352
}
4453
}
4554

46-
const TestElement = r2wc(TestComponent, {}, { mount, unmount })
55+
const TestElement = r2wc(TestComponent, {}, { mount, unmount, update })
4756
customElements.define("test-element", TestElement)
4857

4958
const testEl = new TestElement()
5059

5160
document.body.appendChild(testEl)
52-
expect(mount).toBeCalledTimes(1)
61+
expect(mountCheck).toBeCalledTimes(1)
5362

5463
document.body.removeChild(testEl)
5564
expect(unmount).toBeCalledTimes(1)
@@ -63,7 +72,7 @@ describe("core", () => {
6372
const ButtonElement = r2wc(
6473
Button,
6574
{ props: ["text"] },
66-
{ mount, unmount, onUpdated },
75+
{ mount, unmount, update, onUpdated },
6776
)
6877

6978
customElements.define("test-button-element-attribute", ButtonElement)
@@ -118,7 +127,7 @@ describe("core", () => {
118127
funcProp: "function",
119128
},
120129
},
121-
{ mount, unmount, onUpdated },
130+
{ mount, unmount, update, onUpdated },
122131
)
123132

124133
//@ts-ignore
@@ -175,7 +184,7 @@ describe("core", () => {
175184
const ButtonElement = r2wc(
176185
Button,
177186
{ props: ["text"] },
178-
{ mount, unmount, onUpdated },
187+
{ mount, unmount, update, onUpdated },
179188
)
180189

181190
customElements.define("test-button-element-non-prop", ButtonElement)

packages/core/src/core.ts

+77-50
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88

99
import React from "react"
1010

11-
export { R2WCOptions }
11+
export * from "./types"
1212

1313
const renderSymbol = Symbol.for("r2wc.reactRender")
1414
const shouldRenderSymbol = Symbol.for("r2wc.shouldRender")
@@ -23,7 +23,7 @@ const shouldRenderSymbol = Symbol.for("r2wc.shouldRender")
2323
export default function r2wc(
2424
ReactComponent: FC<any> | ComponentClass<any>,
2525
config: R2WCOptions = {},
26-
renderer: Renderer<ReturnType<typeof React.createElement>>,
26+
renderer: Renderer,
2727
): CustomElementConstructor {
2828
const propTypes: Record<string, any> = {} // { [camelCasedProp]: String | Number | Boolean | Function | Object | Array }
2929
const propAttrMap: Record<string, any> = {} // @TODO: add option to specify for asymetric mapping (eg "className" from "class")
@@ -49,7 +49,9 @@ export default function r2wc(
4949
}
5050
class WebCompClass extends HTMLElement {
5151
rendering: boolean
52-
mounted: boolean;
52+
mounted: boolean
53+
componentContainer: HTMLElement | null
54+
reactComponent: FC<any> | ComponentClass<any> | null;
5355
[renderSymbol]: () => void
5456
getOwnPropertyDescriptor: (
5557
key: string,
@@ -73,6 +75,10 @@ export default function r2wc(
7375

7476
this.mounted = false
7577

78+
this.componentContainer = null
79+
80+
this.reactComponent = null
81+
7682
// Add custom getter and setter for each prop
7783
for (const key of propKeys) {
7884
if (key in propTypes) {
@@ -110,18 +116,33 @@ export default function r2wc(
110116

111117
this.rendering = true
112118
// Container is either shadow DOM or light DOM depending on `shadow` option.
113-
const container = (this.shadowRoot as unknown as HTMLElement) || this
114-
115-
const children = flattenIfOne(mapChildren(this))
119+
const _container =
120+
this.componentContainer ??
121+
((this.shadowRoot as unknown as HTMLElement) || this)
116122

117-
const element = React.createElement(ReactComponent, data, children)
123+
// const children = flattenIfOne(mapChildren(this))
118124

119125
// Use react to render element in container
120-
renderer.mount(container, element)
121126

122127
if (!this.mounted) {
128+
const { reactContainer, component } = renderer.mount(
129+
_container,
130+
ReactComponent,
131+
data,
132+
)
133+
this.componentContainer = reactContainer
134+
this.reactComponent = component
123135
this.mounted = true
124136
} else {
137+
if (!this.reactComponent)
138+
throw new Error("React component is not mounted")
139+
if (!this.componentContainer)
140+
throw new Error("React component container is not mounted")
141+
const updateContext = {
142+
reactContainer: this.componentContainer,
143+
component: this.reactComponent,
144+
}
145+
renderer.update(updateContext, data)
125146
renderer.onUpdated?.()
126147
}
127148
this.rendering = false
@@ -163,7 +184,13 @@ export default function r2wc(
163184

164185
disconnectedCallback() {
165186
this[shouldRenderSymbol] = false
166-
renderer.unmount(this)
187+
if (this.componentContainer && this.reactComponent) {
188+
const context = {
189+
reactContainer: this.componentContainer,
190+
component: this.reactComponent,
191+
}
192+
renderer.unmount(context)
193+
}
167194
this.mounted = false
168195
}
169196

@@ -221,47 +248,47 @@ function toDashedStyle(camelCase = "") {
221248
return camelCase.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
222249
}
223250

224-
function isAllCaps(word: string) {
225-
return word.split("").every((c: string) => c.toUpperCase() === c)
226-
}
227-
228-
function flattenIfOne(arr: object) {
229-
if (!Array.isArray(arr)) {
230-
return arr
231-
}
232-
if (arr.length === 1) {
233-
return arr[0]
234-
}
235-
return arr
236-
}
237-
238-
function mapChildren(node: Element) {
239-
if (node.nodeType === Node.TEXT_NODE) {
240-
return node.textContent?.toString()
241-
}
242-
243-
const arr = Array.from(node.children).map((element) => {
244-
if (element.nodeType === Node.TEXT_NODE) {
245-
return element.textContent?.toString()
246-
}
247-
// BR = br, ReactElement = ReactElement
248-
const nodeName = isAllCaps(element.nodeName)
249-
? element.nodeName.toLowerCase()
250-
: element.nodeName
251-
const children = flattenIfOne(mapChildren(element))
252-
253-
// we need to format c.attributes before passing it to createElement
254-
const attributes: Record<string, string | null> = {}
255-
for (const attr of element.getAttributeNames()) {
256-
// handleTypeCasting.call(c, attr, c.getAttribute(attr), attributes)
257-
attributes[attr] = element.getAttribute(attr)
258-
}
259-
260-
return React.createElement(nodeName, attributes, children)
261-
})
262-
263-
return flattenIfOne(arr)
264-
}
251+
// function isAllCaps(word: string) {
252+
// return word.split("").every((c: string) => c.toUpperCase() === c)
253+
// }
254+
255+
// function flattenIfOne(arr: object) {
256+
// if (!Array.isArray(arr)) {
257+
// return arr
258+
// }
259+
// if (arr.length === 1) {
260+
// return arr[0]
261+
// }
262+
// return arr
263+
// }
264+
265+
// function mapChildren(node: Element) {
266+
// if (node.nodeType === Node.TEXT_NODE) {
267+
// return node.textContent?.toString()
268+
// }
269+
270+
// const arr = Array.from(node.children).map((element) => {
271+
// if (element.nodeType === Node.TEXT_NODE) {
272+
// return element.textContent?.toString()
273+
// }
274+
// // BR = br, ReactElement = ReactElement
275+
// const nodeName = isAllCaps(element.nodeName)
276+
// ? element.nodeName.toLowerCase()
277+
// : element.nodeName
278+
// const children = flattenIfOne(mapChildren(element))
279+
280+
// // we need to format c.attributes before passing it to createElement
281+
// const attributes: Record<string, string | null> = {}
282+
// for (const attr of element.getAttributeNames()) {
283+
// // handleTypeCasting.call(c, attr, c.getAttribute(attr), attributes)
284+
// attributes[attr] = element.getAttribute(attr)
285+
// }
286+
287+
// return React.createElement(nodeName, attributes, children)
288+
// })
289+
290+
// return flattenIfOne(arr)
291+
// }
265292

266293
function handleTypeCasting(
267294
this: HTMLElement,

packages/core/src/types.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ export interface ComponentClass<P = Record<string, unknown>> {
4747

4848
export type Container = Element | Document | DocumentFragment
4949

50+
export interface Context<ComponentType> {
51+
reactContainer: HTMLElement
52+
component: ComponentType
53+
}
54+
5055
export interface CustomElementConstructor {
5156
new (...params: any[]): HTMLElement
5257
}
@@ -64,8 +69,13 @@ export interface R2WCOptions {
6469
props?: string[] | Record<string, PropOptionTypes>
6570
}
6671

67-
export interface Renderer<T> {
68-
mount: (container: HTMLElement, element: T) => any
69-
unmount: (container: HTMLElement) => any
72+
export interface Renderer {
73+
mount: (
74+
container: HTMLElement,
75+
ReactComponent: FC<any> | ComponentClass<any>,
76+
props: any,
77+
) => Context<any>
78+
unmount: (context: Context<any>, props?: any) => void
79+
update: (context: Context<any>, props: any) => void
7080
onUpdated?: () => void
7181
}

0 commit comments

Comments
 (0)