apiCustomElement.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import {
  2. Component,
  3. ComponentOptionsMixin,
  4. ComponentOptionsWithArrayProps,
  5. ComponentOptionsWithObjectProps,
  6. ComponentOptionsWithoutProps,
  7. ComponentPropsOptions,
  8. ComponentPublicInstance,
  9. ComputedOptions,
  10. EmitsOptions,
  11. MethodOptions,
  12. RenderFunction,
  13. SetupContext,
  14. ComponentInternalInstance,
  15. VNode,
  16. RootHydrateFunction,
  17. ExtractPropTypes,
  18. createVNode,
  19. defineComponent,
  20. nextTick,
  21. warn
  22. } from '@vue/runtime-core'
  23. import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared'
  24. import { hydrate, render } from '.'
  25. type VueElementConstructor<P = {}> = {
  26. new (): VueElement & P
  27. }
  28. // defineCustomElement provides the same type inference as defineComponent
  29. // so most of the following overloads should be kept in sync w/ defineComponent.
  30. // overload 1: direct setup function
  31. export function defineCustomElement<Props, RawBindings = object>(
  32. setup: (
  33. props: Readonly<Props>,
  34. ctx: SetupContext
  35. ) => RawBindings | RenderFunction
  36. ): VueElementConstructor<Props>
  37. // overload 2: object format with no props
  38. export function defineCustomElement<
  39. Props = {},
  40. RawBindings = {},
  41. D = {},
  42. C extends ComputedOptions = {},
  43. M extends MethodOptions = {},
  44. Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  45. Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  46. E extends EmitsOptions = EmitsOptions,
  47. EE extends string = string
  48. >(
  49. options: ComponentOptionsWithoutProps<
  50. Props,
  51. RawBindings,
  52. D,
  53. C,
  54. M,
  55. Mixin,
  56. Extends,
  57. E,
  58. EE
  59. >
  60. ): VueElementConstructor<Props>
  61. // overload 3: object format with array props declaration
  62. export function defineCustomElement<
  63. PropNames extends string,
  64. RawBindings,
  65. D,
  66. C extends ComputedOptions = {},
  67. M extends MethodOptions = {},
  68. Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  69. Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  70. E extends EmitsOptions = Record<string, any>,
  71. EE extends string = string
  72. >(
  73. options: ComponentOptionsWithArrayProps<
  74. PropNames,
  75. RawBindings,
  76. D,
  77. C,
  78. M,
  79. Mixin,
  80. Extends,
  81. E,
  82. EE
  83. >
  84. ): VueElementConstructor<{ [K in PropNames]: any }>
  85. // overload 4: object format with object props declaration
  86. export function defineCustomElement<
  87. PropsOptions extends Readonly<ComponentPropsOptions>,
  88. RawBindings,
  89. D,
  90. C extends ComputedOptions = {},
  91. M extends MethodOptions = {},
  92. Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  93. Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  94. E extends EmitsOptions = Record<string, any>,
  95. EE extends string = string
  96. >(
  97. options: ComponentOptionsWithObjectProps<
  98. PropsOptions,
  99. RawBindings,
  100. D,
  101. C,
  102. M,
  103. Mixin,
  104. Extends,
  105. E,
  106. EE
  107. >
  108. ): VueElementConstructor<ExtractPropTypes<PropsOptions>>
  109. // overload 5: defining a custom element from the returned value of
  110. // `defineComponent`
  111. export function defineCustomElement(options: {
  112. new (...args: any[]): ComponentPublicInstance
  113. }): VueElementConstructor
  114. export function defineCustomElement(
  115. options: any,
  116. hydate?: RootHydrateFunction
  117. ): VueElementConstructor {
  118. const Comp = defineComponent(options as any)
  119. const { props } = options
  120. const rawKeys = props ? (isArray(props) ? props : Object.keys(props)) : []
  121. const attrKeys = rawKeys.map(hyphenate)
  122. const propKeys = rawKeys.map(camelize)
  123. class VueCustomElement extends VueElement {
  124. static get observedAttributes() {
  125. return attrKeys
  126. }
  127. constructor() {
  128. super(Comp, attrKeys, propKeys, hydate)
  129. }
  130. }
  131. for (const key of propKeys) {
  132. Object.defineProperty(VueCustomElement.prototype, key, {
  133. get() {
  134. return this._getProp(key)
  135. },
  136. set(val) {
  137. this._setProp(key, val)
  138. }
  139. })
  140. }
  141. return VueCustomElement
  142. }
  143. export const defineSSRCustomElement = ((options: any) => {
  144. // @ts-ignore
  145. return defineCustomElement(options, hydrate)
  146. }) as typeof defineCustomElement
  147. export class VueElement extends HTMLElement {
  148. /**
  149. * @internal
  150. */
  151. _props: Record<string, any> = {}
  152. /**
  153. * @internal
  154. */
  155. _instance: ComponentInternalInstance | null = null
  156. /**
  157. * @internal
  158. */
  159. _connected = false
  160. constructor(
  161. private _def: Component,
  162. private _attrKeys: string[],
  163. private _propKeys: string[],
  164. hydrate?: RootHydrateFunction
  165. ) {
  166. super()
  167. if (this.shadowRoot && hydrate) {
  168. hydrate(this._createVNode(), this.shadowRoot)
  169. } else {
  170. if (__DEV__ && this.shadowRoot) {
  171. warn(
  172. `Custom element has pre-rendered declarative shadow root but is not ` +
  173. `defined as hydratable. Use \`defineSSRCustomElement\`.`
  174. )
  175. }
  176. this.attachShadow({ mode: 'open' })
  177. }
  178. }
  179. attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
  180. if (this._attrKeys.includes(name)) {
  181. this._setProp(camelize(name), toNumber(newValue), false)
  182. }
  183. }
  184. connectedCallback() {
  185. this._connected = true
  186. if (!this._instance) {
  187. // check if there are props set pre-upgrade
  188. for (const key of this._propKeys) {
  189. if (this.hasOwnProperty(key)) {
  190. const value = (this as any)[key]
  191. delete (this as any)[key]
  192. this._setProp(key, value)
  193. }
  194. }
  195. render(this._createVNode(), this.shadowRoot!)
  196. }
  197. }
  198. disconnectedCallback() {
  199. this._connected = false
  200. nextTick(() => {
  201. if (!this._connected) {
  202. render(null, this.shadowRoot!)
  203. this._instance = null
  204. }
  205. })
  206. }
  207. /**
  208. * @internal
  209. */
  210. protected _getProp(key: string) {
  211. return this._props[key]
  212. }
  213. /**
  214. * @internal
  215. */
  216. protected _setProp(key: string, val: any, shouldReflect = true) {
  217. if (val !== this._props[key]) {
  218. this._props[key] = val
  219. if (this._instance) {
  220. render(this._createVNode(), this.shadowRoot!)
  221. }
  222. // reflect
  223. if (shouldReflect) {
  224. if (val === true) {
  225. this.setAttribute(hyphenate(key), '')
  226. } else if (typeof val === 'string' || typeof val === 'number') {
  227. this.setAttribute(hyphenate(key), val + '')
  228. } else if (!val) {
  229. this.removeAttribute(hyphenate(key))
  230. }
  231. }
  232. }
  233. }
  234. private _createVNode(): VNode<any, any> {
  235. const vnode = createVNode(this._def, extend({}, this._props))
  236. if (!this._instance) {
  237. vnode.ce = instance => {
  238. this._instance = instance
  239. instance.isCE = true
  240. // intercept emit
  241. instance.emit = (event: string, ...args: any[]) => {
  242. this.dispatchEvent(
  243. new CustomEvent(event, {
  244. detail: args
  245. })
  246. )
  247. }
  248. // locate nearest Vue custom element parent for provide/inject
  249. let parent: Node | null = this
  250. while (
  251. (parent =
  252. parent && (parent.parentNode || (parent as ShadowRoot).host))
  253. ) {
  254. if (parent instanceof VueElement) {
  255. instance.parent = parent._instance
  256. break
  257. }
  258. }
  259. }
  260. }
  261. return vnode
  262. }
  263. }