apiCustomElement.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. import {
  2. type App,
  3. type Component,
  4. type ComponentCustomElementInterface,
  5. type ComponentInjectOptions,
  6. type ComponentInternalInstance,
  7. type ComponentObjectPropsOptions,
  8. type ComponentOptions,
  9. type ComponentOptionsBase,
  10. type ComponentOptionsMixin,
  11. type ComponentProvideOptions,
  12. type ComponentPublicInstance,
  13. type ComputedOptions,
  14. type ConcreteComponent,
  15. type CreateAppFunction,
  16. type CreateComponentPublicInstanceWithMixins,
  17. type DefineComponent,
  18. type Directive,
  19. type EmitsOptions,
  20. type EmitsToProps,
  21. type ExtractPropTypes,
  22. type MethodOptions,
  23. type RenderFunction,
  24. type SetupContext,
  25. type SlotsType,
  26. type VNode,
  27. type VNodeProps,
  28. createVNode,
  29. defineComponent,
  30. getCurrentInstance,
  31. nextTick,
  32. unref,
  33. warn,
  34. } from '@vue/runtime-core'
  35. import {
  36. camelize,
  37. extend,
  38. hasOwn,
  39. hyphenate,
  40. isArray,
  41. isPlainObject,
  42. toNumber,
  43. } from '@vue/shared'
  44. import { createApp, createSSRApp, render } from '.'
  45. // marker for attr removal
  46. const REMOVAL = {}
  47. export type VueElementConstructor<P = {}> = {
  48. new (initialProps?: Record<string, any>): VueElement & P
  49. }
  50. export interface CustomElementOptions {
  51. styles?: string[]
  52. shadowRoot?: boolean
  53. nonce?: string
  54. configureApp?: (app: App) => void
  55. }
  56. // defineCustomElement provides the same type inference as defineComponent
  57. // so most of the following overloads should be kept in sync w/ defineComponent.
  58. // overload 1: direct setup function
  59. export function defineCustomElement<Props, RawBindings = object>(
  60. setup: (props: Props, ctx: SetupContext) => RawBindings | RenderFunction,
  61. options?: Pick<ComponentOptions, 'name' | 'inheritAttrs' | 'emits'> &
  62. CustomElementOptions & {
  63. props?: (keyof Props)[]
  64. },
  65. ): VueElementConstructor<Props>
  66. export function defineCustomElement<Props, RawBindings = object>(
  67. setup: (props: Props, ctx: SetupContext) => RawBindings | RenderFunction,
  68. options?: Pick<ComponentOptions, 'name' | 'inheritAttrs' | 'emits'> &
  69. CustomElementOptions & {
  70. props?: ComponentObjectPropsOptions<Props>
  71. },
  72. ): VueElementConstructor<Props>
  73. // overload 2: defineCustomElement with options object, infer props from options
  74. export function defineCustomElement<
  75. // props
  76. RuntimePropsOptions extends
  77. ComponentObjectPropsOptions = ComponentObjectPropsOptions,
  78. PropsKeys extends string = string,
  79. // emits
  80. RuntimeEmitsOptions extends EmitsOptions = {},
  81. EmitsKeys extends string = string,
  82. // other options
  83. Data = {},
  84. SetupBindings = {},
  85. Computed extends ComputedOptions = {},
  86. Methods extends MethodOptions = {},
  87. Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  88. Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  89. InjectOptions extends ComponentInjectOptions = {},
  90. InjectKeys extends string = string,
  91. Slots extends SlotsType = {},
  92. LocalComponents extends Record<string, Component> = {},
  93. Directives extends Record<string, Directive> = {},
  94. Exposed extends string = string,
  95. Provide extends ComponentProvideOptions = ComponentProvideOptions,
  96. // resolved types
  97. InferredProps = string extends PropsKeys
  98. ? ComponentObjectPropsOptions extends RuntimePropsOptions
  99. ? {}
  100. : ExtractPropTypes<RuntimePropsOptions>
  101. : { [key in PropsKeys]?: any },
  102. ResolvedProps = InferredProps & EmitsToProps<RuntimeEmitsOptions>,
  103. >(
  104. options: CustomElementOptions & {
  105. props?: (RuntimePropsOptions & ThisType<void>) | PropsKeys[]
  106. } & ComponentOptionsBase<
  107. ResolvedProps,
  108. SetupBindings,
  109. Data,
  110. Computed,
  111. Methods,
  112. Mixin,
  113. Extends,
  114. RuntimeEmitsOptions,
  115. EmitsKeys,
  116. {}, // Defaults
  117. InjectOptions,
  118. InjectKeys,
  119. Slots,
  120. LocalComponents,
  121. Directives,
  122. Exposed,
  123. Provide
  124. > &
  125. ThisType<
  126. CreateComponentPublicInstanceWithMixins<
  127. Readonly<ResolvedProps>,
  128. SetupBindings,
  129. Data,
  130. Computed,
  131. Methods,
  132. Mixin,
  133. Extends,
  134. RuntimeEmitsOptions,
  135. EmitsKeys,
  136. {},
  137. false,
  138. InjectOptions,
  139. Slots,
  140. LocalComponents,
  141. Directives,
  142. Exposed
  143. >
  144. >,
  145. extraOptions?: CustomElementOptions,
  146. ): VueElementConstructor<ResolvedProps>
  147. // overload 3: defining a custom element from the returned value of
  148. // `defineComponent`
  149. export function defineCustomElement<
  150. // this should be `ComponentPublicInstanceConstructor` but that type is not exported
  151. T extends { new (...args: any[]): ComponentPublicInstance<any> },
  152. >(
  153. options: T,
  154. extraOptions?: CustomElementOptions,
  155. ): VueElementConstructor<
  156. T extends DefineComponent<infer P, any, any, any> ? P : unknown
  157. >
  158. /*@__NO_SIDE_EFFECTS__*/
  159. export function defineCustomElement(
  160. options: any,
  161. extraOptions?: ComponentOptions,
  162. /**
  163. * @internal
  164. */
  165. _createApp?: CreateAppFunction<Element>,
  166. ): VueElementConstructor {
  167. let Comp = defineComponent(options, extraOptions) as any
  168. if (isPlainObject(Comp)) Comp = extend({}, Comp, extraOptions)
  169. class VueCustomElement extends VueElement {
  170. static def = Comp
  171. constructor(initialProps?: Record<string, any>) {
  172. super(Comp, initialProps, _createApp)
  173. }
  174. }
  175. return VueCustomElement
  176. }
  177. /*@__NO_SIDE_EFFECTS__*/
  178. export const defineSSRCustomElement = ((
  179. options: any,
  180. extraOptions?: ComponentOptions,
  181. ) => {
  182. // @ts-expect-error
  183. return defineCustomElement(options, extraOptions, createSSRApp)
  184. }) as typeof defineCustomElement
  185. const BaseClass = (
  186. typeof HTMLElement !== 'undefined' ? HTMLElement : class {}
  187. ) as typeof HTMLElement
  188. type InnerComponentDef = ConcreteComponent & CustomElementOptions
  189. export class VueElement
  190. extends BaseClass
  191. implements ComponentCustomElementInterface
  192. {
  193. _isVueCE = true
  194. /**
  195. * @internal
  196. */
  197. _instance: ComponentInternalInstance | null = null
  198. /**
  199. * @internal
  200. */
  201. _app: App | null = null
  202. /**
  203. * @internal
  204. */
  205. _root: Element | ShadowRoot
  206. /**
  207. * @internal
  208. */
  209. _nonce: string | undefined = this._def.nonce
  210. /**
  211. * @internal
  212. */
  213. _teleportTargets?: Set<Element>
  214. private _connected = false
  215. private _resolved = false
  216. private _numberProps: Record<string, true> | null = null
  217. private _styleChildren = new WeakSet()
  218. private _pendingResolve: Promise<void> | undefined
  219. private _parent: VueElement | undefined
  220. /**
  221. * dev only
  222. */
  223. private _styles?: HTMLStyleElement[]
  224. /**
  225. * dev only
  226. */
  227. private _childStyles?: Map<string, HTMLStyleElement[]>
  228. private _ob?: MutationObserver | null = null
  229. private _slots?: Record<string, Node[]>
  230. constructor(
  231. /**
  232. * Component def - note this may be an AsyncWrapper, and this._def will
  233. * be overwritten by the inner component when resolved.
  234. */
  235. private _def: InnerComponentDef,
  236. private _props: Record<string, any> = {},
  237. private _createApp: CreateAppFunction<Element> = createApp,
  238. ) {
  239. super()
  240. if (this.shadowRoot && _createApp !== createApp) {
  241. this._root = this.shadowRoot
  242. } else {
  243. if (__DEV__ && this.shadowRoot) {
  244. warn(
  245. `Custom element has pre-rendered declarative shadow root but is not ` +
  246. `defined as hydratable. Use \`defineSSRCustomElement\`.`,
  247. )
  248. }
  249. if (_def.shadowRoot !== false) {
  250. this.attachShadow({ mode: 'open' })
  251. this._root = this.shadowRoot!
  252. } else {
  253. this._root = this
  254. }
  255. }
  256. }
  257. connectedCallback(): void {
  258. // avoid resolving component if it's not connected
  259. if (!this.isConnected) return
  260. // avoid re-parsing slots if already resolved
  261. if (!this.shadowRoot && !this._resolved) {
  262. this._parseSlots()
  263. }
  264. this._connected = true
  265. // locate nearest Vue custom element parent for provide/inject
  266. let parent: Node | null = this
  267. while (
  268. (parent = parent && (parent.parentNode || (parent as ShadowRoot).host))
  269. ) {
  270. if (parent instanceof VueElement) {
  271. this._parent = parent
  272. break
  273. }
  274. }
  275. if (!this._instance) {
  276. if (this._resolved) {
  277. this._mount(this._def)
  278. } else {
  279. if (parent && parent._pendingResolve) {
  280. this._pendingResolve = parent._pendingResolve.then(() => {
  281. this._pendingResolve = undefined
  282. this._resolveDef()
  283. })
  284. } else {
  285. this._resolveDef()
  286. }
  287. }
  288. }
  289. }
  290. private _setParent(parent = this._parent) {
  291. if (parent) {
  292. this._instance!.parent = parent._instance
  293. this._inheritParentContext(parent)
  294. }
  295. }
  296. private _inheritParentContext(parent = this._parent) {
  297. // #13212, the provides object of the app context must inherit the provides
  298. // object from the parent element so we can inject values from both places
  299. if (parent && this._app) {
  300. Object.setPrototypeOf(
  301. this._app._context.provides,
  302. parent._instance!.provides,
  303. )
  304. }
  305. }
  306. disconnectedCallback(): void {
  307. this._connected = false
  308. nextTick(() => {
  309. if (!this._connected) {
  310. if (this._ob) {
  311. this._ob.disconnect()
  312. this._ob = null
  313. }
  314. // unmount
  315. this._app && this._app.unmount()
  316. if (this._instance) this._instance.ce = undefined
  317. this._app = this._instance = null
  318. if (this._teleportTargets) {
  319. this._teleportTargets.clear()
  320. this._teleportTargets = undefined
  321. }
  322. }
  323. })
  324. }
  325. private _processMutations(mutations: MutationRecord[]) {
  326. for (const m of mutations) {
  327. this._setAttr(m.attributeName!)
  328. }
  329. }
  330. /**
  331. * resolve inner component definition (handle possible async component)
  332. */
  333. private _resolveDef() {
  334. if (this._pendingResolve) {
  335. return
  336. }
  337. // set initial attrs
  338. for (let i = 0; i < this.attributes.length; i++) {
  339. this._setAttr(this.attributes[i].name)
  340. }
  341. // watch future attr changes
  342. this._ob = new MutationObserver(this._processMutations.bind(this))
  343. this._ob.observe(this, { attributes: true })
  344. const resolve = (def: InnerComponentDef, isAsync = false) => {
  345. this._resolved = true
  346. this._pendingResolve = undefined
  347. const { props, styles } = def
  348. // cast Number-type props set before resolve
  349. let numberProps
  350. if (props && !isArray(props)) {
  351. for (const key in props) {
  352. const opt = props[key]
  353. if (opt === Number || (opt && opt.type === Number)) {
  354. if (key in this._props) {
  355. this._props[key] = toNumber(this._props[key])
  356. }
  357. ;(numberProps || (numberProps = Object.create(null)))[
  358. camelize(key)
  359. ] = true
  360. }
  361. }
  362. }
  363. this._numberProps = numberProps
  364. this._resolveProps(def)
  365. // apply CSS
  366. if (this.shadowRoot) {
  367. this._applyStyles(styles)
  368. } else if (__DEV__ && styles) {
  369. warn(
  370. 'Custom element style injection is not supported when using ' +
  371. 'shadowRoot: false',
  372. )
  373. }
  374. // initial mount
  375. this._mount(def)
  376. }
  377. const asyncDef = (this._def as ComponentOptions).__asyncLoader
  378. if (asyncDef) {
  379. this._pendingResolve = asyncDef().then((def: InnerComponentDef) => {
  380. def.configureApp = this._def.configureApp
  381. resolve((this._def = def), true)
  382. })
  383. } else {
  384. resolve(this._def)
  385. }
  386. }
  387. private _mount(def: InnerComponentDef) {
  388. if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) {
  389. // @ts-expect-error
  390. def.name = 'VueElement'
  391. }
  392. this._app = this._createApp(def)
  393. // inherit before configureApp to detect context overwrites
  394. this._inheritParentContext()
  395. if (def.configureApp) {
  396. def.configureApp(this._app)
  397. }
  398. this._app._ceVNode = this._createVNode()
  399. this._app.mount(this._root)
  400. // apply expose after mount
  401. const exposed = this._instance && this._instance.exposed
  402. if (!exposed) return
  403. for (const key in exposed) {
  404. if (!hasOwn(this, key)) {
  405. // exposed properties are readonly
  406. Object.defineProperty(this, key, {
  407. // unwrap ref to be consistent with public instance behavior
  408. get: () => unref(exposed[key]),
  409. })
  410. } else if (__DEV__) {
  411. warn(`Exposed property "${key}" already exists on custom element.`)
  412. }
  413. }
  414. }
  415. private _resolveProps(def: InnerComponentDef) {
  416. const { props } = def
  417. const declaredPropKeys = isArray(props) ? props : Object.keys(props || {})
  418. // check if there are props set pre-upgrade or connect
  419. for (const key of Object.keys(this)) {
  420. if (key[0] !== '_' && declaredPropKeys.includes(key)) {
  421. this._setProp(key, this[key as keyof this])
  422. }
  423. }
  424. // defining getter/setters on prototype
  425. for (const key of declaredPropKeys.map(camelize)) {
  426. Object.defineProperty(this, key, {
  427. get() {
  428. return this._getProp(key)
  429. },
  430. set(val) {
  431. this._setProp(key, val, true, true)
  432. },
  433. })
  434. }
  435. }
  436. protected _setAttr(key: string): void {
  437. if (key.startsWith('data-v-')) return
  438. const has = this.hasAttribute(key)
  439. let value = has ? this.getAttribute(key) : REMOVAL
  440. const camelKey = camelize(key)
  441. if (has && this._numberProps && this._numberProps[camelKey]) {
  442. value = toNumber(value)
  443. }
  444. this._setProp(camelKey, value, false, true)
  445. }
  446. /**
  447. * @internal
  448. */
  449. protected _getProp(key: string): any {
  450. return this._props[key]
  451. }
  452. /**
  453. * @internal
  454. */
  455. _setProp(
  456. key: string,
  457. val: any,
  458. shouldReflect = true,
  459. shouldUpdate = false,
  460. ): void {
  461. if (val !== this._props[key]) {
  462. if (val === REMOVAL) {
  463. delete this._props[key]
  464. } else {
  465. this._props[key] = val
  466. // support set key on ceVNode
  467. if (key === 'key' && this._app) {
  468. this._app._ceVNode!.key = val
  469. }
  470. }
  471. if (shouldUpdate && this._instance) {
  472. this._update()
  473. }
  474. // reflect
  475. if (shouldReflect) {
  476. const ob = this._ob
  477. if (ob) {
  478. this._processMutations(ob.takeRecords())
  479. ob.disconnect()
  480. }
  481. if (val === true) {
  482. this.setAttribute(hyphenate(key), '')
  483. } else if (typeof val === 'string' || typeof val === 'number') {
  484. this.setAttribute(hyphenate(key), val + '')
  485. } else if (!val) {
  486. this.removeAttribute(hyphenate(key))
  487. }
  488. ob && ob.observe(this, { attributes: true })
  489. }
  490. }
  491. }
  492. private _update() {
  493. const vnode = this._createVNode()
  494. if (this._app) vnode.appContext = this._app._context
  495. render(vnode, this._root)
  496. }
  497. private _createVNode(): VNode<any, any> {
  498. const baseProps: VNodeProps = {}
  499. if (!this.shadowRoot) {
  500. baseProps.onVnodeMounted = baseProps.onVnodeUpdated =
  501. this._renderSlots.bind(this)
  502. }
  503. const vnode = createVNode(this._def, extend(baseProps, this._props))
  504. if (!this._instance) {
  505. vnode.ce = instance => {
  506. this._instance = instance
  507. instance.ce = this
  508. instance.isCE = true // for vue-i18n backwards compat
  509. // HMR
  510. if (__DEV__) {
  511. instance.ceReload = newStyles => {
  512. // always reset styles
  513. if (this._styles) {
  514. this._styles.forEach(s => this._root.removeChild(s))
  515. this._styles.length = 0
  516. }
  517. this._applyStyles(newStyles)
  518. this._instance = null
  519. this._update()
  520. }
  521. }
  522. const dispatch = (event: string, args: any[]) => {
  523. this.dispatchEvent(
  524. new CustomEvent(
  525. event,
  526. isPlainObject(args[0])
  527. ? extend({ detail: args }, args[0])
  528. : { detail: args },
  529. ),
  530. )
  531. }
  532. // intercept emit
  533. instance.emit = (event: string, ...args: any[]) => {
  534. // dispatch both the raw and hyphenated versions of an event
  535. // to match Vue behavior
  536. dispatch(event, args)
  537. if (hyphenate(event) !== event) {
  538. dispatch(hyphenate(event), args)
  539. }
  540. }
  541. this._setParent()
  542. }
  543. }
  544. return vnode
  545. }
  546. private _applyStyles(
  547. styles: string[] | undefined,
  548. owner?: ConcreteComponent,
  549. ) {
  550. if (!styles) return
  551. if (owner) {
  552. if (owner === this._def || this._styleChildren.has(owner)) {
  553. return
  554. }
  555. this._styleChildren.add(owner)
  556. }
  557. const nonce = this._nonce
  558. for (let i = styles.length - 1; i >= 0; i--) {
  559. const s = document.createElement('style')
  560. if (nonce) s.setAttribute('nonce', nonce)
  561. s.textContent = styles[i]
  562. this.shadowRoot!.prepend(s)
  563. // record for HMR
  564. if (__DEV__) {
  565. if (owner) {
  566. if (owner.__hmrId) {
  567. if (!this._childStyles) this._childStyles = new Map()
  568. let entry = this._childStyles.get(owner.__hmrId)
  569. if (!entry) {
  570. this._childStyles.set(owner.__hmrId, (entry = []))
  571. }
  572. entry.push(s)
  573. }
  574. } else {
  575. ;(this._styles || (this._styles = [])).push(s)
  576. }
  577. }
  578. }
  579. }
  580. /**
  581. * Only called when shadowRoot is false
  582. */
  583. private _parseSlots() {
  584. const slots: VueElement['_slots'] = (this._slots = {})
  585. let n
  586. while ((n = this.firstChild)) {
  587. const slotName =
  588. (n.nodeType === 1 && (n as Element).getAttribute('slot')) || 'default'
  589. ;(slots[slotName] || (slots[slotName] = [])).push(n)
  590. this.removeChild(n)
  591. }
  592. }
  593. /**
  594. * Only called when shadowRoot is false
  595. */
  596. private _renderSlots() {
  597. const outlets = this._getSlots()
  598. const scopeId = this._instance!.type.__scopeId
  599. for (let i = 0; i < outlets.length; i++) {
  600. const o = outlets[i] as HTMLSlotElement
  601. const slotName = o.getAttribute('name') || 'default'
  602. const content = this._slots![slotName]
  603. const parent = o.parentNode!
  604. if (content) {
  605. for (const n of content) {
  606. // for :slotted css
  607. if (scopeId && n.nodeType === 1) {
  608. const id = scopeId + '-s'
  609. const walker = document.createTreeWalker(n, 1)
  610. ;(n as Element).setAttribute(id, '')
  611. let child
  612. while ((child = walker.nextNode())) {
  613. ;(child as Element).setAttribute(id, '')
  614. }
  615. }
  616. parent.insertBefore(n, o)
  617. }
  618. } else {
  619. while (o.firstChild) parent.insertBefore(o.firstChild, o)
  620. }
  621. parent.removeChild(o)
  622. }
  623. }
  624. /**
  625. * @internal
  626. */
  627. private _getSlots(): HTMLSlotElement[] {
  628. const roots: Element[] = [this]
  629. if (this._teleportTargets) {
  630. roots.push(...this._teleportTargets)
  631. }
  632. return roots.reduce<HTMLSlotElement[]>((res, i) => {
  633. res.push(...Array.from(i.querySelectorAll('slot')))
  634. return res
  635. }, [])
  636. }
  637. /**
  638. * @internal
  639. */
  640. _injectChildStyle(comp: ConcreteComponent & CustomElementOptions): void {
  641. this._applyStyles(comp.styles, comp)
  642. }
  643. /**
  644. * @internal
  645. */
  646. _removeChildStyle(comp: ConcreteComponent): void {
  647. if (__DEV__) {
  648. this._styleChildren.delete(comp)
  649. if (this._childStyles && comp.__hmrId) {
  650. // clear old styles
  651. const oldStyles = this._childStyles.get(comp.__hmrId)
  652. if (oldStyles) {
  653. oldStyles.forEach(s => this._root.removeChild(s))
  654. oldStyles.length = 0
  655. }
  656. }
  657. }
  658. }
  659. }
  660. export function useHost(caller?: string): VueElement | null {
  661. const instance = getCurrentInstance()
  662. const el = instance && (instance.ce as VueElement)
  663. if (el) {
  664. return el
  665. } else if (__DEV__) {
  666. if (!instance) {
  667. warn(
  668. `${caller || 'useHost'} called without an active component instance.`,
  669. )
  670. } else {
  671. warn(
  672. `${caller || 'useHost'} can only be used in components defined via ` +
  673. `defineCustomElement.`,
  674. )
  675. }
  676. }
  677. return null
  678. }
  679. /**
  680. * Retrieve the shadowRoot of the current custom element. Only usable in setup()
  681. * of a `defineCustomElement` component.
  682. */
  683. export function useShadowRoot(): ShadowRoot | null {
  684. const el = __DEV__ ? useHost('useShadowRoot') : useHost()
  685. return el && el.shadowRoot
  686. }