-
Notifications
You must be signed in to change notification settings - Fork 26
/
svelte-react.ts
156 lines (138 loc) · 4.39 KB
/
svelte-react.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import {
createElement,
useRef,
useEffect,
forwardRef,
useCallback,
type ForwardedRef,
type PropsWithChildren,
useState,
type PropsWithoutRef
} from 'react'
import type { SvelteComponent } from 'svelte'
const eventRegex = /^on([A-Z]{1,}[a-zA-Z]*)$/
export type IntrinsicProps =
| 'className'
| 'id'
| 'hidden'
| 'role'
| 'style'
| 'tabIndex'
export type SvelteProps<T> =
T extends SvelteComponent<infer Props, any, any> ? Props : {}
// We introduce a custom ref type as some of the Svelte type definitions don't
// play nice with React.ForwardRef.
type Ref<T> =
| ((ref: (Omit<HTMLElement, keyof T> & T) | null) => void)
| {
current:
| HTMLElement
| Partial<T>
| (Omit<HTMLElement, keyof T> & T)
| undefined
| null
}
export type ReactProps<Props> = Props & {
ref?: Ref<Props>
slot?: string
} & {
// Note: The div here isn't important because all props in intrinsicProps are
// available on all elements. We just want to make sure we have the correct
// React name/value for them.
[P in IntrinsicProps]?: Omit<JSX.IntrinsicElements, 'ref'>['div'][P]
}
const useEventHandlers = (props: any) => {
const [el, setEl] = useState<HTMLElement>()
const lastValue = useRef<{ [key: string]: (...args: any[]) => any }>({})
// Handle updating event listeners when props change
useEffect(() => {
if (!el) return
for (const [key, listener] of Object.entries(props) as [
string,
(...args: any[]) => any
][]) {
const match = eventRegex.exec(key)
if (!match) continue
const event = match[1].toLowerCase()
const oldListener = lastValue.current[event]
if (listener === oldListener) continue
if (oldListener) el.removeEventListener(event, oldListener)
el.addEventListener(event, listener)
// Keep track of the last value, so we're able to add/remove it.
lastValue.current[event] = listener
}
// Remove any listeners which are no longer present
for (const removed of Object.keys(lastValue).filter(
(k) => !props[`on${k}`]
)) {
el.removeEventListener(removed, lastValue.current[removed])
delete lastValue.current[removed]
}
}, [props, el])
const setElement = useCallback((el: HTMLElement | undefined) => {
lastValue.current = {}
setEl((oldValue) => {
if (oldValue) {
// Cleanup
for (const [event, listener] of Object.entries(lastValue.current)) {
oldValue.removeEventListener(event, listener)
delete lastValue.current[event]
}
}
return el
})
}, [])
return {
setElement
}
}
/**
*
* @param tag custom element tag name for svelte component
* @param component The imported svelte component itself. This is not used, but ensures that the component's code has been included in the bundle.
* @returns A react component
*/
export default function SvelteWebComponentToReact<
T extends Record<string, any>
>(tag: string, component: typeof HTMLElement) {
return forwardRef(
(
props: PropsWithoutRef<PropsWithChildren<T>>,
forwardedRef: ForwardedRef<HTMLElement>
) => {
const component = useRef<HTMLElement>()
const { setElement } = useEventHandlers(props)
const setRef = useCallback(
(ref: HTMLElement) => {
setElement(ref)
if (!ref) {
// Note: This will happen when the component unmounts.
return
}
component.current = ref
if (forwardedRef) {
if (typeof forwardedRef === 'function') forwardedRef(ref)
else forwardedRef.current = ref
}
},
[setElement]
)
useEffect(() => {
if (!component.current) return
// Create a dictionary of all our properties without events. If we pass an
// onClick prop through to Svelte, we could inadvertently set it on the
// HTMLElement if we use <el {...$restProps}/>, which causes a Svelte to
// setAttribute('onClick', props['onClick']). This can lead to unexpected
// behavior, and triggers a TrustedTypes error.
for (const [key, value] of Object.entries(props)) {
if (eventRegex.test(key) || key === 'children') continue
;(component.current as any)[key] = value
}
}, [props])
return createElement(tag, {
ref: setRef,
children: props.children
})
}
)
}