type Component,
type ComponentCustomElementInterface,
type ComponentInjectOptions,
- type ComponentInternalInstance,
type ComponentObjectPropsOptions,
type ComponentOptions,
type ComponentOptionsBase,
type EmitsOptions,
type EmitsToProps,
type ExtractPropTypes,
+ type GenericComponentInstance,
type MethodOptions,
type RenderFunction,
type SetupContext,
type InnerComponentDef = ConcreteComponent & CustomElementOptions
-export class VueElement
+export abstract class VueElementBase<
+ Def extends CustomElementOptions & { props?: any } = InnerComponentDef,
+ >
extends BaseClass
implements ComponentCustomElementInterface
{
/**
* @internal
*/
- _instance: ComponentInternalInstance | null = null
+ _instance: GenericComponentInstance | null = null
/**
* @internal
*/
/**
* @internal
*/
- _nonce: string | undefined = this._def.nonce
-
+ _nonce: string | undefined
/**
* @internal
*/
_teleportTargets?: Set<Element>
- private _connected = false
- private _resolved = false
- private _numberProps: Record<string, true> | null = null
- private _styleChildren = new WeakSet()
- private _pendingResolve: Promise<void> | undefined
- private _parent: VueElement | undefined
+ protected _def: Def
+ protected _props: Record<string, any>
+ protected _createApp: CreateAppFunction<Element>
+ protected _connected = false
+ protected _resolved = false
+ protected _numberProps: Record<string, true> | null = null
+ protected _styleChildren: WeakSet<object> = new WeakSet()
+ protected _pendingResolve: Promise<void> | undefined
+ protected _parent: VueElementBase | undefined
+
/**
* dev only
*/
- private _styles?: HTMLStyleElement[]
+ protected _styles?: HTMLStyleElement[]
/**
* dev only
*/
- private _childStyles?: Map<string, HTMLStyleElement[]>
- private _ob?: MutationObserver | null = null
- private _slots?: Record<string, Node[]>
+ protected _childStyles?: Map<string, HTMLStyleElement[]>
+ protected _ob?: MutationObserver | null = null
+ protected _slots?: Record<string, Node[]>
constructor(
/**
* Component def - note this may be an AsyncWrapper, and this._def will
* be overwritten by the inner component when resolved.
*/
- private _def: InnerComponentDef,
- private _props: Record<string, any> = {},
- private _createApp: CreateAppFunction<Element> = createApp,
+ def: Def,
+ props: Record<string, any> = {},
+ createAppFn: CreateAppFunction<Element> = createApp,
) {
super()
- if (this.shadowRoot && _createApp !== createApp) {
+ this._def = def
+ this._props = props
+ this._createApp = createAppFn
+ this._nonce = def.nonce
+ if (this.shadowRoot && createAppFn !== createApp) {
this._root = this.shadowRoot
} else {
if (__DEV__ && this.shadowRoot) {
`defined as hydratable. Use \`defineSSRCustomElement\`.`,
)
}
- if (_def.shadowRoot !== false) {
+ if (def.shadowRoot !== false) {
this.attachShadow(
- extend({}, _def.shadowRootOptions, {
+ extend({}, def.shadowRootOptions, {
mode: 'open',
}) as ShadowRootInit,
)
while (
(parent = parent && (parent.parentNode || (parent as ShadowRoot).host))
) {
- if (parent instanceof VueElement) {
+ if (parent instanceof VueElementBase) {
this._parent = parent
break
}
if (!this._instance) {
if (this._resolved) {
- this._mount(this._def)
+ this._mountComponent(this._def)
} else {
if (parent && parent._pendingResolve) {
this._pendingResolve = parent._pendingResolve.then(() => {
}
}
- private _setParent(parent = this._parent) {
- if (parent) {
- this._instance!.parent = parent._instance
+ protected abstract _mountComponent(def: Def): void
+ protected abstract _updateComponent(): void
+ protected abstract _unmountComponent(): void
+
+ protected _setParent(
+ parent: VueElementBase | undefined = this._parent,
+ ): void {
+ if (parent && this._instance) {
+ this._instance.parent = parent._instance
this._inheritParentContext(parent)
}
}
- private _inheritParentContext(parent = this._parent) {
+ protected _inheritParentContext(
+ parent: VueElementBase | undefined = this._parent,
+ ): void {
// #13212, the provides object of the app context must inherit the provides
// object from the parent element so we can inject values from both places
if (parent && this._app) {
this._ob.disconnect()
this._ob = null
}
- // unmount
- this._app && this._app.unmount()
- if (this._instance) this._instance.ce = undefined
- this._app = this._instance = null
+ this._unmountComponent()
if (this._teleportTargets) {
this._teleportTargets.clear()
this._teleportTargets = undefined
this._ob.observe(this, { attributes: true })
- const resolve = (def: InnerComponentDef, isAsync = false) => {
+ const resolve = (def: Def) => {
this._resolved = true
this._pendingResolve = undefined
}
// initial mount
- this._mount(def)
+ this._mountComponent(def)
}
const asyncDef = (this._def as ComponentOptions).__asyncLoader
if (asyncDef) {
- this._pendingResolve = asyncDef().then((def: InnerComponentDef) => {
- def.configureApp = this._def.configureApp
- resolve((this._def = def), true)
+ const { configureApp } = this._def
+ this._pendingResolve = asyncDef().then((def: any) => {
+ def.configureApp = configureApp
+ this._def = def
+ resolve(def)
})
} else {
resolve(this._def)
}
}
- private _mount(def: InnerComponentDef) {
- if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) {
- // @ts-expect-error
- def.name = 'VueElement'
- }
- this._app = this._createApp(def)
- // inherit before configureApp to detect context overwrites
- this._inheritParentContext()
- if (def.configureApp) {
- def.configureApp(this._app)
- }
- this._app._ceVNode = this._createVNode()
- this._app.mount(this._root)
-
- // apply expose after mount
- const exposed = this._instance && this._instance.exposed
- if (!exposed) return
- for (const key in exposed) {
- if (!hasOwn(this, key)) {
- // exposed properties are readonly
- Object.defineProperty(this, key, {
- // unwrap ref to be consistent with public instance behavior
- get: () => unref(exposed[key]),
- })
- } else if (__DEV__) {
- warn(`Exposed property "${key}" already exists on custom element.`)
- }
- }
- }
-
- private _resolveProps(def: InnerComponentDef) {
+ private _resolveProps(def: Def): void {
const { props } = def
const declaredPropKeys = isArray(props) ? props : Object.keys(props || {})
}
}
- protected _setAttr(key: string): void {
+ private _setAttr(key: string): void {
if (key.startsWith('data-v-')) return
const has = this.hasAttribute(key)
let value = has ? this.getAttribute(key) : REMOVAL
} else {
this._props[key] = val
// support set key on ceVNode
- if (key === 'key' && this._app) {
+ if (key === 'key' && this._app && this._app._ceVNode) {
this._app._ceVNode!.key = val
}
}
if (shouldUpdate && this._instance) {
- this._update()
+ this._updateComponent()
}
// reflect
if (shouldReflect) {
}
}
- private _update() {
- const vnode = this._createVNode()
- if (this._app) vnode.appContext = this._app._context
- render(vnode, this._root)
- }
-
- private _createVNode(): VNode<any, any> {
- const baseProps: VNodeProps = {}
- if (!this.shadowRoot) {
- baseProps.onVnodeMounted = baseProps.onVnodeUpdated =
- this._renderSlots.bind(this)
- }
- const vnode = createVNode(this._def, extend(baseProps, this._props))
- if (!this._instance) {
- vnode.ce = instance => {
- this._instance = instance
- instance.ce = this
- instance.isCE = true // for vue-i18n backwards compat
- // HMR
- if (__DEV__) {
- instance.ceReload = newStyles => {
- // always reset styles
- if (this._styles) {
- this._styles.forEach(s => this._root.removeChild(s))
- this._styles.length = 0
- }
- this._applyStyles(newStyles)
- this._instance = null
- this._update()
- }
- }
-
- const dispatch = (event: string, args: any[]) => {
- this.dispatchEvent(
- new CustomEvent(
- event,
- isPlainObject(args[0])
- ? extend({ detail: args }, args[0])
- : { detail: args },
- ),
- )
- }
-
- // intercept emit
- instance.emit = (event: string, ...args: any[]) => {
- // dispatch both the raw and hyphenated versions of an event
- // to match Vue behavior
- dispatch(event, args)
- if (hyphenate(event) !== event) {
- dispatch(hyphenate(event), args)
- }
- }
-
- this._setParent()
- }
- }
- return vnode
- }
-
- private _applyStyles(
+ protected _applyStyles(
styles: string[] | undefined,
owner?: ConcreteComponent,
- ) {
+ ): void {
if (!styles) return
if (owner) {
if (owner === this._def || this._styleChildren.has(owner)) {
* Only called when shadowRoot is false
*/
private _parseSlots() {
- const slots: VueElement['_slots'] = (this._slots = {})
+ const slots: VueElementBase['_slots'] = (this._slots = {})
let n
while ((n = this.firstChild)) {
const slotName =
/**
* Only called when shadowRoot is false
*/
- private _renderSlots() {
+ protected _renderSlots(): void {
const outlets = this._getSlots()
const scopeId = this._instance!.type.__scopeId
for (let i = 0; i < outlets.length; i++) {
return res
}, [])
}
+
/**
* @internal
*/
}
}
-export function useHost(caller?: string): VueElement | null {
+export class VueElement extends VueElementBase<InnerComponentDef> {
+ protected _mountComponent(def: InnerComponentDef): void {
+ if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) {
+ // @ts-expect-error
+ def.name = 'VueElement'
+ }
+ this._app = this._createApp(def)
+ this._inheritParentContext()
+ if (def.configureApp) {
+ def.configureApp(this._app)
+ }
+ this._app._ceVNode = this._createVNode()
+ this._app.mount(this._root)
+
+ const exposed = this._instance && this._instance.exposed
+ if (!exposed) return
+ for (const key in exposed) {
+ if (!hasOwn(this, key)) {
+ Object.defineProperty(this, key, {
+ get: () => unref(exposed[key]),
+ })
+ } else if (__DEV__) {
+ warn(`Exposed property "${key}" already exists on custom element.`)
+ }
+ }
+ }
+
+ protected _updateComponent(): void {
+ if (!this._app) return
+ const vnode = this._createVNode()
+ vnode.appContext = this._app._context
+ render(vnode, this._root)
+ }
+
+ protected _unmountComponent(): void {
+ if (this._app) {
+ this._app.unmount()
+ }
+ if (this._instance && this._instance.ce) {
+ this._instance.ce = undefined
+ }
+ this._app = this._instance = null
+ }
+
+ private _createVNode(): VNode<any, any> {
+ const baseProps: VNodeProps = {}
+ if (!this.shadowRoot) {
+ baseProps.onVnodeMounted = baseProps.onVnodeUpdated =
+ this._renderSlots.bind(this)
+ }
+ const vnode = createVNode(this._def, extend(baseProps, this._props))
+ if (!this._instance) {
+ vnode.ce = instance => {
+ this._instance = instance
+ instance.ce = this
+ instance.isCE = true
+ if (__DEV__) {
+ instance.ceReload = newStyles => {
+ if (this._styles) {
+ this._styles.forEach(s => this._root.removeChild(s))
+ this._styles.length = 0
+ }
+ this._applyStyles(newStyles)
+ this._instance = null
+ this._updateComponent()
+ }
+ }
+
+ const dispatch = (event: string, args: any[]) => {
+ this.dispatchEvent(
+ new CustomEvent(
+ event,
+ isPlainObject(args[0])
+ ? extend({ detail: args }, args[0])
+ : { detail: args },
+ ),
+ )
+ }
+
+ instance.emit = (event: string, ...args: any[]) => {
+ dispatch(event, args)
+ if (hyphenate(event) !== event) {
+ dispatch(hyphenate(event), args)
+ }
+ }
+
+ this._setParent()
+ }
+ }
+ return vnode
+ }
+}
+
+export function useHost(caller?: string): VueElementBase | null {
const instance = getCurrentInstance()
- const el = instance && (instance.ce as VueElement)
+ const el = instance && (instance.ce as VueElementBase)
if (el) {
return el
} else if (__DEV__) {