Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quantum-kittens
GitHub Repository: quantum-kittens/platypus
Path: blob/main/frontend/vue/wc-wrapper/index.ts
3367 views
1
/**
2
* Port of https://github.com/cloudera/hue/tree/master/desktop/core/src/desktop/js/vue/wrapper
3
* for Vue 3 support of web components
4
* To remove once @vuejs/vue-web-component-wrapper starts supporting Vue 3
5
* https://github.com/vuejs/vue-web-component-wrapper/issues/93
6
*/
7
8
import {
9
Component,
10
CreateAppFunction,
11
ConcreteComponent,
12
App,
13
ComponentPublicInstance,
14
VNode,
15
ComponentOptionsWithObjectProps
16
} from 'vue'
17
18
import { toHandlerKey } from '@vue/shared'
19
20
import {
21
KeyHash,
22
toVNodes,
23
camelize,
24
hyphenate,
25
callHooks,
26
setInitialProps,
27
createCustomEvent,
28
convertAttributeValue
29
} from './utils'
30
31
export interface WebComponentOptions {
32
connectedCallback?(): void;
33
}
34
35
/**
36
* Vue 3 wrapper to convert a Vue component into Web Component. Supports reactive attributes, events & slots.
37
*/
38
export default function wrap (
39
component: Component,
40
createApp: CreateAppFunction<Element>,
41
h: <P>(type: ConcreteComponent<P> | string, props?: KeyHash, children?: () => unknown) => VNode,
42
options?: WebComponentOptions
43
): CustomElementConstructor {
44
const componentObj: ComponentOptionsWithObjectProps = <ComponentOptionsWithObjectProps>component
45
46
let isInitialized = false
47
48
let hyphenatedPropsList: string[]
49
let camelizedPropsList: string[]
50
let camelizedPropsMap: KeyHash
51
52
function initialize () {
53
if (isInitialized) {
54
return
55
}
56
57
// extract props info
58
const propsList: string[] = Array.isArray(componentObj.props)
59
? componentObj.props
60
: Object.keys(componentObj.props || {})
61
hyphenatedPropsList = propsList.map(hyphenate)
62
camelizedPropsList = propsList.map(camelize)
63
64
const originalPropsAsObject = Array.isArray(componentObj.props) ? {} : componentObj.props || {}
65
camelizedPropsMap = camelizedPropsList.reduce((map: KeyHash, key, i) => {
66
map[key] = originalPropsAsObject[propsList[i]]
67
return map
68
}, {})
69
70
isInitialized = true
71
}
72
73
class CustomElement extends HTMLElement {
74
_wrapper: App;
75
_component?: ComponentPublicInstance;
76
77
_props!: KeyHash;
78
_slotChildren!: (VNode | null)[];
79
_mounted = false;
80
81
constructor () {
82
super()
83
84
const eventProxies = this.createEventProxies(<string[]>componentObj.emits)
85
86
this._props = {}
87
this._slotChildren = []
88
89
// eslint-disable-next-line @typescript-eslint/no-this-alias
90
const self = this
91
this._wrapper = createApp({
92
mounted () {
93
self._mounted = true
94
},
95
unmounted () {
96
self._mounted = false
97
},
98
render () {
99
const props = Object.assign({}, self._props, eventProxies)
100
delete props.dataVApp
101
return h(componentObj, props, () => self._slotChildren)
102
}
103
})
104
105
// Use MutationObserver to react to future attribute & slot content change
106
const observer = new MutationObserver((mutations) => {
107
let hasChildrenChange = false
108
109
for (let i = 0; i < mutations.length; i++) {
110
const m = mutations[i]
111
112
if (isInitialized && m.type === 'attributes' && m.target === this) {
113
if (m.attributeName) {
114
this.syncAttribute(m.attributeName)
115
}
116
} else {
117
hasChildrenChange = true
118
}
119
}
120
121
if (hasChildrenChange) {
122
// this.syncSlots(); Commenting as this is causing an infinit $forceUpdate loop, will fix if required!
123
}
124
})
125
126
observer.observe(this, {
127
childList: true,
128
subtree: true,
129
characterData: true,
130
attributes: true
131
})
132
}
133
134
createEventProxies (
135
eventNames: string[] | undefined
136
): { [name: string]: (...args: unknown[]) => void } {
137
const eventProxies: { [name: string]: (...args: unknown[]) => void } = {}
138
139
if (eventNames) {
140
eventNames.forEach((name) => {
141
const handlerName = toHandlerKey(camelize(name))
142
eventProxies[handlerName] = (...args: unknown[]): void => {
143
this.dispatchEvent(createCustomEvent(name, args))
144
}
145
})
146
}
147
148
return eventProxies
149
}
150
151
syncAttribute (key: string): void {
152
const camelized = camelize(key)
153
let value
154
155
// eslint-disable-next-line no-prototype-builtins
156
if (this.hasOwnProperty(key)) {
157
value = (<KeyHash> this)[key]
158
} else if (this.hasAttribute(key)) {
159
value = this.getAttribute(key)
160
}
161
162
this._props[camelized] = convertAttributeValue(value, key, camelizedPropsMap[camelized])
163
164
this._component?.$forceUpdate()
165
}
166
167
syncSlots (): void {
168
this._slotChildren = toVNodes(this.childNodes, h)
169
this._component?.$forceUpdate()
170
}
171
172
syncInitialAttributes (): void {
173
this._props = setInitialProps(camelizedPropsList)
174
175
// parent attributes not being parsed. possibly related:
176
// https://github.com/vuejs/vue-web-component-wrapper/issues/26
177
const elementAttributes = this.getAttributeNames()
178
hyphenatedPropsList = Array.from(new Set(hyphenatedPropsList.concat(elementAttributes)))
179
180
hyphenatedPropsList.forEach((key) => {
181
this.syncAttribute(key)
182
})
183
}
184
185
connectedCallback () {
186
if (!this._component || !this._mounted) {
187
if (isInitialized) {
188
// initialize attributes
189
this.syncInitialAttributes()
190
}
191
192
// initialize children
193
this.syncSlots()
194
195
// Mount the component
196
this._component = this._wrapper.mount(this)
197
} else {
198
// Call mounted on re-insert
199
callHooks(this._component, 'mounted')
200
}
201
if (options?.connectedCallback) {
202
options.connectedCallback.bind(this)()
203
}
204
}
205
206
disconnectedCallback () {
207
callHooks(this._component, 'unmounted')
208
}
209
}
210
211
initialize()
212
213
return CustomElement
214
}
215
216