Path: blob/main/frontend/vue/wc-wrapper/index.ts
3367 views
/**1* Port of https://github.com/cloudera/hue/tree/master/desktop/core/src/desktop/js/vue/wrapper2* for Vue 3 support of web components3* To remove once @vuejs/vue-web-component-wrapper starts supporting Vue 34* https://github.com/vuejs/vue-web-component-wrapper/issues/935*/67import {8Component,9CreateAppFunction,10ConcreteComponent,11App,12ComponentPublicInstance,13VNode,14ComponentOptionsWithObjectProps15} from 'vue'1617import { toHandlerKey } from '@vue/shared'1819import {20KeyHash,21toVNodes,22camelize,23hyphenate,24callHooks,25setInitialProps,26createCustomEvent,27convertAttributeValue28} from './utils'2930export interface WebComponentOptions {31connectedCallback?(): void;32}3334/**35* Vue 3 wrapper to convert a Vue component into Web Component. Supports reactive attributes, events & slots.36*/37export default function wrap (38component: Component,39createApp: CreateAppFunction<Element>,40h: <P>(type: ConcreteComponent<P> | string, props?: KeyHash, children?: () => unknown) => VNode,41options?: WebComponentOptions42): CustomElementConstructor {43const componentObj: ComponentOptionsWithObjectProps = <ComponentOptionsWithObjectProps>component4445let isInitialized = false4647let hyphenatedPropsList: string[]48let camelizedPropsList: string[]49let camelizedPropsMap: KeyHash5051function initialize () {52if (isInitialized) {53return54}5556// extract props info57const propsList: string[] = Array.isArray(componentObj.props)58? componentObj.props59: Object.keys(componentObj.props || {})60hyphenatedPropsList = propsList.map(hyphenate)61camelizedPropsList = propsList.map(camelize)6263const originalPropsAsObject = Array.isArray(componentObj.props) ? {} : componentObj.props || {}64camelizedPropsMap = camelizedPropsList.reduce((map: KeyHash, key, i) => {65map[key] = originalPropsAsObject[propsList[i]]66return map67}, {})6869isInitialized = true70}7172class CustomElement extends HTMLElement {73_wrapper: App;74_component?: ComponentPublicInstance;7576_props!: KeyHash;77_slotChildren!: (VNode | null)[];78_mounted = false;7980constructor () {81super()8283const eventProxies = this.createEventProxies(<string[]>componentObj.emits)8485this._props = {}86this._slotChildren = []8788// eslint-disable-next-line @typescript-eslint/no-this-alias89const self = this90this._wrapper = createApp({91mounted () {92self._mounted = true93},94unmounted () {95self._mounted = false96},97render () {98const props = Object.assign({}, self._props, eventProxies)99delete props.dataVApp100return h(componentObj, props, () => self._slotChildren)101}102})103104// Use MutationObserver to react to future attribute & slot content change105const observer = new MutationObserver((mutations) => {106let hasChildrenChange = false107108for (let i = 0; i < mutations.length; i++) {109const m = mutations[i]110111if (isInitialized && m.type === 'attributes' && m.target === this) {112if (m.attributeName) {113this.syncAttribute(m.attributeName)114}115} else {116hasChildrenChange = true117}118}119120if (hasChildrenChange) {121// this.syncSlots(); Commenting as this is causing an infinit $forceUpdate loop, will fix if required!122}123})124125observer.observe(this, {126childList: true,127subtree: true,128characterData: true,129attributes: true130})131}132133createEventProxies (134eventNames: string[] | undefined135): { [name: string]: (...args: unknown[]) => void } {136const eventProxies: { [name: string]: (...args: unknown[]) => void } = {}137138if (eventNames) {139eventNames.forEach((name) => {140const handlerName = toHandlerKey(camelize(name))141eventProxies[handlerName] = (...args: unknown[]): void => {142this.dispatchEvent(createCustomEvent(name, args))143}144})145}146147return eventProxies148}149150syncAttribute (key: string): void {151const camelized = camelize(key)152let value153154// eslint-disable-next-line no-prototype-builtins155if (this.hasOwnProperty(key)) {156value = (<KeyHash> this)[key]157} else if (this.hasAttribute(key)) {158value = this.getAttribute(key)159}160161this._props[camelized] = convertAttributeValue(value, key, camelizedPropsMap[camelized])162163this._component?.$forceUpdate()164}165166syncSlots (): void {167this._slotChildren = toVNodes(this.childNodes, h)168this._component?.$forceUpdate()169}170171syncInitialAttributes (): void {172this._props = setInitialProps(camelizedPropsList)173174// parent attributes not being parsed. possibly related:175// https://github.com/vuejs/vue-web-component-wrapper/issues/26176const elementAttributes = this.getAttributeNames()177hyphenatedPropsList = Array.from(new Set(hyphenatedPropsList.concat(elementAttributes)))178179hyphenatedPropsList.forEach((key) => {180this.syncAttribute(key)181})182}183184connectedCallback () {185if (!this._component || !this._mounted) {186if (isInitialized) {187// initialize attributes188this.syncInitialAttributes()189}190191// initialize children192this.syncSlots()193194// Mount the component195this._component = this._wrapper.mount(this)196} else {197// Call mounted on re-insert198callHooks(this._component, 'mounted')199}200if (options?.connectedCallback) {201options.connectedCallback.bind(this)()202}203}204205disconnectedCallback () {206callHooks(this._component, 'unmounted')207}208}209210initialize()211212return CustomElement213}214215216