apiCustomElement.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import {
  2. ComponentOptionsMixin,
  3. ComponentOptionsWithArrayProps,
  4. ComponentOptionsWithObjectProps,
  5. ComponentOptionsWithoutProps,
  6. ComponentPropsOptions,
  7. ComponentPublicInstance,
  8. ComputedOptions,
  9. EmitsOptions,
  10. MethodOptions,
  11. RenderFunction,
  12. SetupContext,
  13. ComponentInternalInstance,
  14. VNode,
  15. RootHydrateFunction,
  16. ExtractPropTypes,
  17. createVNode,
  18. defineComponent,
  19. nextTick,
  20. warn,
  21. ComponentOptions
  22. } from '@vue/runtime-core'
  23. import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared'
  24. import { hydrate, render } from '.'
  25. export type VueElementConstructor<P = {}> = {
  26. new (initialProps?: Record<string, any>): 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. > & { styles?: string[] }
  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. > & { styles?: string[] }
  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. > & { styles?: string[] }
  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 def = Comp
  125. static get observedAttributes() {
  126. return attrKeys
  127. }
  128. constructor(initialProps?: Record<string, any>) {
  129. super(Comp, initialProps, attrKeys, propKeys, hydate)
  130. }
  131. }
  132. for (const key of propKeys) {
  133. Object.defineProperty(VueCustomElement.prototype, key, {
  134. get() {
  135. return this._getProp(key)
  136. },
  137. set(val) {
  138. this._setProp(key, val)
  139. }
  140. })
  141. }
  142. return VueCustomElement
  143. }
  144. export const defineSSRCustomElement = ((options: any) => {
  145. // @ts-ignore
  146. return defineCustomElement(options, hydrate)
  147. }) as typeof defineCustomElement
  148. const BaseClass = (
  149. typeof HTMLElement !== 'undefined' ? HTMLElement : class {}
  150. ) as typeof HTMLElement
  151. export class VueElement extends BaseClass {
  152. /**
  153. * @internal
  154. */
  155. _instance: ComponentInternalInstance | null = null
  156. private _connected = false
  157. private _styles?: HTMLStyleElement[]
  158. constructor(
  159. private _def: ComponentOptions & { styles?: string[] },
  160. private _props: Record<string, any> = {},
  161. private _attrKeys: string[],
  162. private _propKeys: string[],
  163. hydrate?: RootHydrateFunction
  164. ) {
  165. super()
  166. if (this.shadowRoot && hydrate) {
  167. hydrate(this._createVNode(), this.shadowRoot)
  168. } else {
  169. if (__DEV__ && this.shadowRoot) {
  170. warn(
  171. `Custom element has pre-rendered declarative shadow root but is not ` +
  172. `defined as hydratable. Use \`defineSSRCustomElement\`.`
  173. )
  174. }
  175. this.attachShadow({ mode: 'open' })
  176. this._applyStyles()
  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. // HMR
  241. if (__DEV__) {
  242. instance.ceReload = () => {
  243. this._instance = null
  244. // reset styles
  245. if (this._styles) {
  246. this._styles.forEach(s => this.shadowRoot!.removeChild(s))
  247. this._styles.length = 0
  248. }
  249. this._applyStyles()
  250. // reload
  251. render(this._createVNode(), this.shadowRoot!)
  252. }
  253. }
  254. // intercept emit
  255. instance.emit = (event: string, ...args: any[]) => {
  256. this.dispatchEvent(
  257. new CustomEvent(event, {
  258. detail: args
  259. })
  260. )
  261. }
  262. // locate nearest Vue custom element parent for provide/inject
  263. let parent: Node | null = this
  264. while (
  265. (parent =
  266. parent && (parent.parentNode || (parent as ShadowRoot).host))
  267. ) {
  268. if (parent instanceof VueElement) {
  269. instance.parent = parent._instance
  270. break
  271. }
  272. }
  273. }
  274. }
  275. return vnode
  276. }
  277. private _applyStyles() {
  278. if (this._def.styles) {
  279. this._def.styles.forEach(css => {
  280. const s = document.createElement('style')
  281. s.textContent = css
  282. this.shadowRoot!.appendChild(s)
  283. // record for HMR
  284. if (__DEV__) {
  285. ;(this._styles || (this._styles = [])).push(s)
  286. }
  287. })
  288. }
  289. }
  290. }