apiCustomElement.ts 19 KB

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