apiDefineCustomElement.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import { extend, isPlainObject } from '@vue/shared'
  2. import {
  3. createComponent,
  4. createVaporApp,
  5. createVaporSSRApp,
  6. defineVaporComponent,
  7. } from '.'
  8. import {
  9. type ComponentObjectPropsOptions,
  10. type CreateAppFunction,
  11. type CustomElementOptions,
  12. type EmitFn,
  13. type EmitsOptions,
  14. type EmitsToProps,
  15. type ExtractPropTypes,
  16. VueElementBase,
  17. warn,
  18. } from '@vue/runtime-dom'
  19. import type {
  20. ObjectVaporComponent,
  21. VaporComponent,
  22. VaporComponentInstance,
  23. } from './component'
  24. import type { Block } from './block'
  25. import { withHydration } from './dom/hydration'
  26. import type {
  27. DefineVaporComponent,
  28. DefineVaporSetupFnComponent,
  29. VaporRenderResult,
  30. } from './apiDefineComponent'
  31. import type { StaticSlots } from './componentSlots'
  32. import { isFragment } from './fragment'
  33. export type VaporElementConstructor<P = {}> = {
  34. new (initialProps?: Record<string, any>): VaporElement & P
  35. }
  36. // overload 1: direct setup function
  37. export function defineVaporCustomElement<Props, RawBindings = object>(
  38. setup: (
  39. props: Props,
  40. ctx: {
  41. attrs: Record<string, any>
  42. slots: StaticSlots
  43. emit: EmitFn
  44. expose: (exposed: Record<string, any>) => void
  45. },
  46. ) => RawBindings | VaporRenderResult,
  47. options?: Pick<ObjectVaporComponent, 'name' | 'inheritAttrs' | 'emits'> &
  48. CustomElementOptions & {
  49. props?: (keyof Props)[]
  50. },
  51. ): VaporElementConstructor<Props>
  52. export function defineVaporCustomElement<Props, RawBindings = object>(
  53. setup: (
  54. props: Props,
  55. ctx: {
  56. attrs: Record<string, any>
  57. slots: StaticSlots
  58. emit: EmitFn
  59. expose: (exposed: Record<string, any>) => void
  60. },
  61. ) => RawBindings | VaporRenderResult,
  62. options?: Pick<ObjectVaporComponent, 'name' | 'inheritAttrs' | 'emits'> &
  63. CustomElementOptions & {
  64. props?: ComponentObjectPropsOptions<Props>
  65. },
  66. ): VaporElementConstructor<Props>
  67. // overload 2: defineVaporCustomElement with options object, infer props from options
  68. export function defineVaporCustomElement<
  69. // props
  70. RuntimePropsOptions extends ComponentObjectPropsOptions =
  71. ComponentObjectPropsOptions,
  72. RuntimePropsKeys extends string = string,
  73. // emits
  74. RuntimeEmitsOptions extends EmitsOptions = {},
  75. RuntimeEmitsKeys extends string = string,
  76. Slots extends StaticSlots = StaticSlots,
  77. // resolved types
  78. InferredProps = string extends RuntimePropsKeys
  79. ? ComponentObjectPropsOptions extends RuntimePropsOptions
  80. ? {}
  81. : ExtractPropTypes<RuntimePropsOptions>
  82. : { [key in RuntimePropsKeys]?: any },
  83. ResolvedProps = InferredProps & EmitsToProps<RuntimeEmitsOptions>,
  84. >(
  85. options: CustomElementOptions & {
  86. props?: (RuntimePropsOptions & ThisType<void>) | RuntimePropsKeys[]
  87. emits?: RuntimeEmitsOptions | RuntimeEmitsKeys[]
  88. slots?: Slots
  89. setup?: (
  90. props: Readonly<InferredProps>,
  91. ctx: {
  92. attrs: Record<string, any>
  93. slots: Slots
  94. emit: EmitFn<RuntimeEmitsOptions>
  95. expose: (exposed: Record<string, any>) => void
  96. },
  97. ) => any
  98. } & ThisType<void>,
  99. extraOptions?: CustomElementOptions,
  100. ): VaporElementConstructor<ResolvedProps>
  101. // overload 3: defining a custom element from the returned value of
  102. // `defineVaporComponent`
  103. export function defineVaporCustomElement<
  104. T extends
  105. | DefineVaporComponent<any, any, any, any, any, any, any, any, any, any>
  106. | DefineVaporSetupFnComponent<any, any, any, any, any>,
  107. >(
  108. options: T,
  109. extraOptions?: CustomElementOptions,
  110. ): VaporElementConstructor<
  111. T extends DefineVaporComponent<
  112. infer RuntimePropsOptions,
  113. any,
  114. any,
  115. any,
  116. any,
  117. any,
  118. any,
  119. any,
  120. any,
  121. any
  122. >
  123. ? ComponentObjectPropsOptions extends RuntimePropsOptions
  124. ? {}
  125. : ExtractPropTypes<RuntimePropsOptions>
  126. : T extends DefineVaporSetupFnComponent<
  127. infer P extends Record<string, any>,
  128. any,
  129. any,
  130. any,
  131. any
  132. >
  133. ? P
  134. : unknown
  135. >
  136. /*@__NO_SIDE_EFFECTS__*/
  137. export function defineVaporCustomElement(
  138. options: any,
  139. extraOptions?: Omit<ObjectVaporComponent, 'setup'> & CustomElementOptions,
  140. /**
  141. * @internal
  142. */
  143. _createApp?: CreateAppFunction<ParentNode, VaporComponent>,
  144. ): VaporElementConstructor {
  145. let Comp = defineVaporComponent(options, extraOptions)
  146. if (isPlainObject(Comp)) Comp = extend({}, Comp, extraOptions)
  147. class VaporCustomElement extends VaporElement {
  148. static def = Comp
  149. constructor(initialProps?: Record<string, any>) {
  150. super(Comp, initialProps, _createApp)
  151. }
  152. }
  153. return VaporCustomElement
  154. }
  155. /*@__NO_SIDE_EFFECTS__*/
  156. export const defineVaporSSRCustomElement = ((
  157. options: any,
  158. extraOptions?: Omit<ObjectVaporComponent, 'setup'>,
  159. ) => {
  160. // @ts-expect-error
  161. return defineVaporCustomElement(options, extraOptions, createVaporSSRApp)
  162. }) as typeof defineVaporCustomElement
  163. type VaporInnerComponentDef = VaporComponent & CustomElementOptions
  164. export class VaporElement extends VueElementBase<
  165. ParentNode,
  166. VaporComponent,
  167. VaporInnerComponentDef
  168. > {
  169. constructor(
  170. def: VaporInnerComponentDef,
  171. props: Record<string, any> | undefined = {},
  172. createAppFn: CreateAppFunction<ParentNode, VaporComponent> = createVaporApp,
  173. ) {
  174. super(def, props, createAppFn)
  175. }
  176. protected _needsHydration(): boolean {
  177. if (this.shadowRoot && this._createApp !== createVaporApp) {
  178. return true
  179. } else {
  180. if (__DEV__ && this.shadowRoot) {
  181. warn(
  182. `Custom element has pre-rendered declarative shadow root but is not ` +
  183. `defined as hydratable. Use \`defineVaporSSRCustomElement\`.`,
  184. )
  185. }
  186. }
  187. return false
  188. }
  189. protected _mount(def: VaporInnerComponentDef): void {
  190. if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) {
  191. def.name = 'VaporElement'
  192. }
  193. this._app = this._createApp(this._def)
  194. this._inheritParentContext()
  195. if (this._def.configureApp) {
  196. this._def.configureApp(this._app)
  197. }
  198. // create component in hydration context
  199. if (this.shadowRoot && this._createApp === createVaporSSRApp) {
  200. withHydration(this._root, this._createComponent.bind(this))
  201. } else {
  202. this._createComponent()
  203. }
  204. this._app!.mount(this._root)
  205. // Render slots immediately after mount for shadowRoot: false
  206. // This ensures correct lifecycle order for nested custom elements
  207. if (!this.shadowRoot) {
  208. this._renderSlots()
  209. }
  210. }
  211. protected _update(): void {
  212. if (!this._app) return
  213. // update component by re-running all its render effects
  214. const renderEffects = (this._instance! as VaporComponentInstance)
  215. .renderEffects
  216. if (renderEffects) renderEffects.forEach(e => e.run())
  217. }
  218. protected _unmount(): void {
  219. if (__TEST__) {
  220. try {
  221. this._app!.unmount()
  222. } catch (error) {
  223. // In test environment, ignore errors caused by accessing Node
  224. // after the test environment has been torn down
  225. if (
  226. error instanceof ReferenceError &&
  227. error.message.includes('Node is not defined')
  228. ) {
  229. // Ignore this error in tests
  230. } else {
  231. throw error
  232. }
  233. }
  234. } else {
  235. this._app!.unmount()
  236. }
  237. if (this._instance && this._instance.ce) {
  238. this._instance.ce = undefined
  239. }
  240. this._app = this._instance = null
  241. }
  242. /**
  243. * Only called when shadowRoot is false
  244. */
  245. protected _updateSlotNodes(replacements: Map<Node, Node[]>): void {
  246. this._updateFragmentNodes(
  247. (this._instance! as VaporComponentInstance).block,
  248. replacements,
  249. )
  250. }
  251. /**
  252. * Replace slot nodes with their replace content
  253. * @internal
  254. */
  255. private _updateFragmentNodes(
  256. block: Block,
  257. replacements: Map<Node, Node[]>,
  258. ): void {
  259. if (Array.isArray(block)) {
  260. block.forEach(item => this._updateFragmentNodes(item, replacements))
  261. return
  262. }
  263. if (!isFragment(block)) return
  264. const { nodes } = block
  265. if (Array.isArray(nodes)) {
  266. const newNodes: Block[] = []
  267. for (const node of nodes) {
  268. if (node instanceof HTMLSlotElement) {
  269. newNodes.push(...replacements.get(node)!)
  270. } else {
  271. this._updateFragmentNodes(node, replacements)
  272. newNodes.push(node)
  273. }
  274. }
  275. block.nodes = newNodes
  276. } else if (nodes instanceof HTMLSlotElement) {
  277. block.nodes = replacements.get(nodes)!
  278. } else {
  279. this._updateFragmentNodes(nodes, replacements)
  280. }
  281. }
  282. private _createComponent() {
  283. this._def.ce = instance => {
  284. this._app!._ceComponent = this._instance = instance
  285. // For shadowRoot: false, _renderSlots is called synchronously after mount
  286. // in _mount() to ensure correct lifecycle order
  287. if (!this.shadowRoot) {
  288. // Still set updated hooks for subsequent updates
  289. this._instance!.u = [this._renderSlots.bind(this)]
  290. }
  291. this._processInstance()
  292. }
  293. createComponent(
  294. this._def,
  295. this._props,
  296. undefined,
  297. undefined,
  298. undefined,
  299. this._app!._context,
  300. )
  301. }
  302. }