apiCustomElement.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  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. const Comp = defineComponent(options, extraOptions) as any
  168. if (isPlainObject(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. _teleportTarget?: HTMLElement
  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. }
  319. })
  320. }
  321. /**
  322. * resolve inner component definition (handle possible async component)
  323. */
  324. private _resolveDef() {
  325. if (this._pendingResolve) {
  326. return
  327. }
  328. // set initial attrs
  329. for (let i = 0; i < this.attributes.length; i++) {
  330. this._setAttr(this.attributes[i].name)
  331. }
  332. // watch future attr changes
  333. this._ob = new MutationObserver(mutations => {
  334. for (const m of mutations) {
  335. this._setAttr(m.attributeName!)
  336. }
  337. })
  338. this._ob.observe(this, { attributes: true })
  339. const resolve = (def: InnerComponentDef, isAsync = false) => {
  340. this._resolved = true
  341. this._pendingResolve = undefined
  342. const { props, styles } = def
  343. // cast Number-type props set before resolve
  344. let numberProps
  345. if (props && !isArray(props)) {
  346. for (const key in props) {
  347. const opt = props[key]
  348. if (opt === Number || (opt && opt.type === Number)) {
  349. if (key in this._props) {
  350. this._props[key] = toNumber(this._props[key])
  351. }
  352. ;(numberProps || (numberProps = Object.create(null)))[
  353. camelize(key)
  354. ] = true
  355. }
  356. }
  357. }
  358. this._numberProps = numberProps
  359. this._resolveProps(def)
  360. // apply CSS
  361. if (this.shadowRoot) {
  362. this._applyStyles(styles)
  363. } else if (__DEV__ && styles) {
  364. warn(
  365. 'Custom element style injection is not supported when using ' +
  366. 'shadowRoot: false',
  367. )
  368. }
  369. // initial mount
  370. this._mount(def)
  371. }
  372. const asyncDef = (this._def as ComponentOptions).__asyncLoader
  373. if (asyncDef) {
  374. this._pendingResolve = asyncDef().then((def: InnerComponentDef) => {
  375. def.configureApp = this._def.configureApp
  376. resolve((this._def = def), true)
  377. })
  378. } else {
  379. resolve(this._def)
  380. }
  381. }
  382. private _mount(def: InnerComponentDef) {
  383. if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && !def.name) {
  384. // @ts-expect-error
  385. def.name = 'VueElement'
  386. }
  387. this._app = this._createApp(def)
  388. // inherit before configureApp to detect context overwrites
  389. this._inheritParentContext()
  390. if (def.configureApp) {
  391. def.configureApp(this._app)
  392. }
  393. this._app._ceVNode = this._createVNode()
  394. this._app.mount(this._root)
  395. // apply expose after mount
  396. const exposed = this._instance && this._instance.exposed
  397. if (!exposed) return
  398. for (const key in exposed) {
  399. if (!hasOwn(this, key)) {
  400. // exposed properties are readonly
  401. Object.defineProperty(this, key, {
  402. // unwrap ref to be consistent with public instance behavior
  403. get: () => unref(exposed[key]),
  404. })
  405. } else if (__DEV__) {
  406. warn(`Exposed property "${key}" already exists on custom element.`)
  407. }
  408. }
  409. }
  410. private _resolveProps(def: InnerComponentDef) {
  411. const { props } = def
  412. const declaredPropKeys = isArray(props) ? props : Object.keys(props || {})
  413. // check if there are props set pre-upgrade or connect
  414. for (const key of Object.keys(this)) {
  415. if (key[0] !== '_' && declaredPropKeys.includes(key)) {
  416. this._setProp(key, this[key as keyof this])
  417. }
  418. }
  419. // defining getter/setters on prototype
  420. for (const key of declaredPropKeys.map(camelize)) {
  421. Object.defineProperty(this, key, {
  422. get() {
  423. return this._getProp(key)
  424. },
  425. set(val) {
  426. this._setProp(key, val, true, true)
  427. },
  428. })
  429. }
  430. }
  431. protected _setAttr(key: string): void {
  432. if (key.startsWith('data-v-')) return
  433. const has = this.hasAttribute(key)
  434. let value = has ? this.getAttribute(key) : REMOVAL
  435. const camelKey = camelize(key)
  436. if (has && this._numberProps && this._numberProps[camelKey]) {
  437. value = toNumber(value)
  438. }
  439. this._setProp(camelKey, value, false, true)
  440. }
  441. /**
  442. * @internal
  443. */
  444. protected _getProp(key: string): any {
  445. return this._props[key]
  446. }
  447. /**
  448. * @internal
  449. */
  450. _setProp(
  451. key: string,
  452. val: any,
  453. shouldReflect = true,
  454. shouldUpdate = false,
  455. ): void {
  456. if (val !== this._props[key]) {
  457. if (val === REMOVAL) {
  458. delete this._props[key]
  459. } else {
  460. this._props[key] = val
  461. // support set key on ceVNode
  462. if (key === 'key' && this._app) {
  463. this._app._ceVNode!.key = val
  464. }
  465. }
  466. if (shouldUpdate && this._instance) {
  467. this._update()
  468. }
  469. // reflect
  470. if (shouldReflect) {
  471. const ob = this._ob
  472. ob && ob.disconnect()
  473. if (val === true) {
  474. this.setAttribute(hyphenate(key), '')
  475. } else if (typeof val === 'string' || typeof val === 'number') {
  476. this.setAttribute(hyphenate(key), val + '')
  477. } else if (!val) {
  478. this.removeAttribute(hyphenate(key))
  479. }
  480. ob && ob.observe(this, { attributes: true })
  481. }
  482. }
  483. }
  484. private _update() {
  485. const vnode = this._createVNode()
  486. if (this._app) vnode.appContext = this._app._context
  487. render(vnode, this._root)
  488. }
  489. private _createVNode(): VNode<any, any> {
  490. const baseProps: VNodeProps = {}
  491. if (!this.shadowRoot) {
  492. baseProps.onVnodeMounted = baseProps.onVnodeUpdated =
  493. this._renderSlots.bind(this)
  494. }
  495. const vnode = createVNode(this._def, extend(baseProps, this._props))
  496. if (!this._instance) {
  497. vnode.ce = instance => {
  498. this._instance = instance
  499. instance.ce = this
  500. instance.isCE = true // for vue-i18n backwards compat
  501. // HMR
  502. if (__DEV__) {
  503. instance.ceReload = newStyles => {
  504. // always reset styles
  505. if (this._styles) {
  506. this._styles.forEach(s => this._root.removeChild(s))
  507. this._styles.length = 0
  508. }
  509. this._applyStyles(newStyles)
  510. this._instance = null
  511. this._update()
  512. }
  513. }
  514. const dispatch = (event: string, args: any[]) => {
  515. this.dispatchEvent(
  516. new CustomEvent(
  517. event,
  518. isPlainObject(args[0])
  519. ? extend({ detail: args }, args[0])
  520. : { detail: args },
  521. ),
  522. )
  523. }
  524. // intercept emit
  525. instance.emit = (event: string, ...args: any[]) => {
  526. // dispatch both the raw and hyphenated versions of an event
  527. // to match Vue behavior
  528. dispatch(event, args)
  529. if (hyphenate(event) !== event) {
  530. dispatch(hyphenate(event), args)
  531. }
  532. }
  533. this._setParent()
  534. }
  535. }
  536. return vnode
  537. }
  538. private _applyStyles(
  539. styles: string[] | undefined,
  540. owner?: ConcreteComponent,
  541. ) {
  542. if (!styles) return
  543. if (owner) {
  544. if (owner === this._def || this._styleChildren.has(owner)) {
  545. return
  546. }
  547. this._styleChildren.add(owner)
  548. }
  549. const nonce = this._nonce
  550. for (let i = styles.length - 1; i >= 0; i--) {
  551. const s = document.createElement('style')
  552. if (nonce) s.setAttribute('nonce', nonce)
  553. s.textContent = styles[i]
  554. this.shadowRoot!.prepend(s)
  555. // record for HMR
  556. if (__DEV__) {
  557. if (owner) {
  558. if (owner.__hmrId) {
  559. if (!this._childStyles) this._childStyles = new Map()
  560. let entry = this._childStyles.get(owner.__hmrId)
  561. if (!entry) {
  562. this._childStyles.set(owner.__hmrId, (entry = []))
  563. }
  564. entry.push(s)
  565. }
  566. } else {
  567. ;(this._styles || (this._styles = [])).push(s)
  568. }
  569. }
  570. }
  571. }
  572. /**
  573. * Only called when shadowRoot is false
  574. */
  575. private _parseSlots() {
  576. const slots: VueElement['_slots'] = (this._slots = {})
  577. let n
  578. while ((n = this.firstChild)) {
  579. const slotName =
  580. (n.nodeType === 1 && (n as Element).getAttribute('slot')) || 'default'
  581. ;(slots[slotName] || (slots[slotName] = [])).push(n)
  582. this.removeChild(n)
  583. }
  584. }
  585. /**
  586. * Only called when shadowRoot is false
  587. */
  588. private _renderSlots() {
  589. const outlets = (this._teleportTarget || this).querySelectorAll('slot')
  590. const scopeId = this._instance!.type.__scopeId
  591. for (let i = 0; i < outlets.length; i++) {
  592. const o = outlets[i] as HTMLSlotElement
  593. const slotName = o.getAttribute('name') || 'default'
  594. const content = this._slots![slotName]
  595. const parent = o.parentNode!
  596. if (content) {
  597. for (const n of content) {
  598. // for :slotted css
  599. if (scopeId && n.nodeType === 1) {
  600. const id = scopeId + '-s'
  601. const walker = document.createTreeWalker(n, 1)
  602. ;(n as Element).setAttribute(id, '')
  603. let child
  604. while ((child = walker.nextNode())) {
  605. ;(child as Element).setAttribute(id, '')
  606. }
  607. }
  608. parent.insertBefore(n, o)
  609. }
  610. } else {
  611. while (o.firstChild) parent.insertBefore(o.firstChild, o)
  612. }
  613. parent.removeChild(o)
  614. }
  615. }
  616. /**
  617. * @internal
  618. */
  619. _injectChildStyle(comp: ConcreteComponent & CustomElementOptions): void {
  620. this._applyStyles(comp.styles, comp)
  621. }
  622. /**
  623. * @internal
  624. */
  625. _removeChildStyle(comp: ConcreteComponent): void {
  626. if (__DEV__) {
  627. this._styleChildren.delete(comp)
  628. if (this._childStyles && comp.__hmrId) {
  629. // clear old styles
  630. const oldStyles = this._childStyles.get(comp.__hmrId)
  631. if (oldStyles) {
  632. oldStyles.forEach(s => this._root.removeChild(s))
  633. oldStyles.length = 0
  634. }
  635. }
  636. }
  637. }
  638. }
  639. export function useHost(caller?: string): VueElement | null {
  640. const instance = getCurrentInstance()
  641. const el = instance && (instance.ce as VueElement)
  642. if (el) {
  643. return el
  644. } else if (__DEV__) {
  645. if (!instance) {
  646. warn(
  647. `${caller || 'useHost'} called without an active component instance.`,
  648. )
  649. } else {
  650. warn(
  651. `${caller || 'useHost'} can only be used in components defined via ` +
  652. `defineCustomElement.`,
  653. )
  654. }
  655. }
  656. return null
  657. }
  658. /**
  659. * Retrieve the shadowRoot of the current custom element. Only usable in setup()
  660. * of a `defineCustomElement` component.
  661. */
  662. export function useShadowRoot(): ShadowRoot | null {
  663. const el = __DEV__ ? useHost('useShadowRoot') : useHost()
  664. return el && el.shadowRoot
  665. }