import { extend, isPlainObject } from '@vue/shared'
import {
createComponent,
createVaporApp,
createVaporSSRApp,
defineVaporComponent,
} from '.'
import {
type ComponentObjectPropsOptions,
type CreateAppFunction,
type CustomElementOptions,
type EmitFn,
type EmitsOptions,
type EmitsToProps,
type ExtractPropTypes,
VueElementBase,
warn,
} from '@vue/runtime-dom'
import type {
ObjectVaporComponent,
VaporComponent,
VaporComponentInstance,
} from './component'
import type { Block } from './block'
import { withHydration } from './dom/hydration'
import type {
DefineVaporComponent,
DefineVaporSetupFnComponent,
VaporRenderResult,
} from './apiDefineComponent'
import type { StaticSlots } from './componentSlots'
import { isFragment } from './fragment'
export type VaporElementConstructor
= {
new (initialProps?: Record): VaporElement & P
}
// overload 1: direct setup function
export function defineVaporCustomElement(
setup: (
props: Props,
ctx: {
attrs: Record
slots: StaticSlots
emit: EmitFn
expose: (exposed: Record) => void
},
) => RawBindings | VaporRenderResult,
options?: Pick &
CustomElementOptions & {
props?: (keyof Props)[]
},
): VaporElementConstructor
export function defineVaporCustomElement(
setup: (
props: Props,
ctx: {
attrs: Record
slots: StaticSlots
emit: EmitFn
expose: (exposed: Record) => void
},
) => RawBindings | VaporRenderResult,
options?: Pick &
CustomElementOptions & {
props?: ComponentObjectPropsOptions
},
): VaporElementConstructor
// overload 2: defineVaporCustomElement with options object, infer props from options
export function defineVaporCustomElement<
// props
RuntimePropsOptions extends ComponentObjectPropsOptions =
ComponentObjectPropsOptions,
RuntimePropsKeys extends string = string,
// emits
RuntimeEmitsOptions extends EmitsOptions = {},
RuntimeEmitsKeys extends string = string,
Slots extends StaticSlots = StaticSlots,
// resolved types
InferredProps = string extends RuntimePropsKeys
? ComponentObjectPropsOptions extends RuntimePropsOptions
? {}
: ExtractPropTypes
: { [key in RuntimePropsKeys]?: any },
ResolvedProps = InferredProps & EmitsToProps,
>(
options: CustomElementOptions & {
props?: (RuntimePropsOptions & ThisType) | RuntimePropsKeys[]
emits?: RuntimeEmitsOptions | RuntimeEmitsKeys[]
slots?: Slots
setup?: (
props: Readonly,
ctx: {
attrs: Record
slots: Slots
emit: EmitFn
expose: (exposed: Record) => void
},
) => any
} & ThisType,
extraOptions?: CustomElementOptions,
): VaporElementConstructor
// overload 3: defining a custom element from the returned value of
// `defineVaporComponent`
export function defineVaporCustomElement<
T extends
| DefineVaporComponent
| DefineVaporSetupFnComponent,
>(
options: T,
extraOptions?: CustomElementOptions,
): VaporElementConstructor<
T extends DefineVaporComponent<
infer RuntimePropsOptions,
any,
any,
any,
any,
any,
any,
any,
any,
any
>
? ComponentObjectPropsOptions extends RuntimePropsOptions
? {}
: ExtractPropTypes
: T extends DefineVaporSetupFnComponent<
infer P extends Record,
any,
any,
any,
any
>
? P
: unknown
>
/*@__NO_SIDE_EFFECTS__*/
export function defineVaporCustomElement(
options: any,
extraOptions?: Omit & CustomElementOptions,
/**
* @internal
*/
_createApp?: CreateAppFunction,
): VaporElementConstructor {
let Comp = defineVaporComponent(options, extraOptions)
if (isPlainObject(Comp)) Comp = extend({}, Comp, extraOptions)
class VaporCustomElement extends VaporElement {
static def = Comp
constructor(initialProps?: Record) {
super(Comp, initialProps, _createApp)
}
}
return VaporCustomElement
}
/*@__NO_SIDE_EFFECTS__*/
export const defineVaporSSRCustomElement = ((
options: any,
extraOptions?: Omit,
) => {
// @ts-expect-error
return defineVaporCustomElement(options, extraOptions, createVaporSSRApp)
}) as typeof defineVaporCustomElement
type VaporInnerComponentDef = VaporComponent & CustomElementOptions
export class VaporElement extends VueElementBase<
ParentNode,
VaporComponent,
VaporInnerComponentDef
> {
constructor(
def: VaporInnerComponentDef,
props: Record | undefined = {},
createAppFn: CreateAppFunction = createVaporApp,
) {
super(def, props, createAppFn)
}
protected _needsHydration(): boolean {
if (this.shadowRoot && this._createApp !== createVaporApp) {
return true
} else {
if (__DEV__ && this.shadowRoot) {
warn(
`Custom element has pre-rendered declarative shadow root but is not ` +
`defined as hydratable. Use \`defineVaporSSRCustomElement\`.`,
)
}
}
return false
}
protected _mount(def: VaporInnerComponentDef): void {
if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) {
def.name = 'VaporElement'
}
this._app = this._createApp(this._def)
this._inheritParentContext()
if (this._def.configureApp) {
this._def.configureApp(this._app)
}
// create component in hydration context
if (this.shadowRoot && this._createApp === createVaporSSRApp) {
withHydration(this._root, this._createComponent.bind(this))
} else {
this._createComponent()
}
this._app!.mount(this._root)
// Render slots immediately after mount for shadowRoot: false
// This ensures correct lifecycle order for nested custom elements
if (!this.shadowRoot) {
this._renderSlots()
}
}
protected _update(): void {
if (!this._app) return
// update component by re-running all its render effects
const renderEffects = (this._instance! as VaporComponentInstance)
.renderEffects
if (renderEffects) renderEffects.forEach(e => e.run())
}
protected _unmount(): void {
if (__TEST__) {
try {
this._app!.unmount()
} catch (error) {
// In test environment, ignore errors caused by accessing Node
// after the test environment has been torn down
if (
error instanceof ReferenceError &&
error.message.includes('Node is not defined')
) {
// Ignore this error in tests
} else {
throw error
}
}
} else {
this._app!.unmount()
}
if (this._instance && this._instance.ce) {
this._instance.ce = undefined
}
this._app = this._instance = null
}
/**
* Only called when shadowRoot is false
*/
protected _updateSlotNodes(replacements: Map): void {
this._updateFragmentNodes(
(this._instance! as VaporComponentInstance).block,
replacements,
)
}
/**
* Replace slot nodes with their replace content
* @internal
*/
private _updateFragmentNodes(
block: Block,
replacements: Map,
): void {
if (Array.isArray(block)) {
block.forEach(item => this._updateFragmentNodes(item, replacements))
return
}
if (!isFragment(block)) return
const { nodes } = block
if (Array.isArray(nodes)) {
const newNodes: Block[] = []
for (const node of nodes) {
if (node instanceof HTMLSlotElement) {
newNodes.push(...replacements.get(node)!)
} else {
this._updateFragmentNodes(node, replacements)
newNodes.push(node)
}
}
block.nodes = newNodes
} else if (nodes instanceof HTMLSlotElement) {
block.nodes = replacements.get(nodes)!
} else {
this._updateFragmentNodes(nodes, replacements)
}
}
private _createComponent() {
this._def.ce = instance => {
this._app!._ceComponent = this._instance = instance
// For shadowRoot: false, _renderSlots is called synchronously after mount
// in _mount() to ensure correct lifecycle order
if (!this.shadowRoot) {
// Still set updated hooks for subsequent updates
this._instance!.u = [this._renderSlots.bind(this)]
}
this._processInstance()
}
createComponent(
this._def,
this._props,
undefined,
undefined,
undefined,
this._app!._context,
)
}
}