apiCustomElement.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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. ConcreteComponent,
  22. ComponentOptions,
  23. ComponentInjectOptions,
  24. SlotsType
  25. } from '@vue/runtime-core'
  26. import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared'
  27. import { hydrate, render } from '.'
  28. export type VueElementConstructor<P = {}> = {
  29. new (initialProps?: Record<string, any>): VueElement & P
  30. }
  31. // defineCustomElement provides the same type inference as defineComponent
  32. // so most of the following overloads should be kept in sync w/ defineComponent.
  33. // overload 1: direct setup function
  34. export function defineCustomElement<Props, RawBindings = object>(
  35. setup: (
  36. props: Readonly<Props>,
  37. ctx: SetupContext
  38. ) => RawBindings | RenderFunction
  39. ): VueElementConstructor<Props>
  40. // overload 2: object format with no props
  41. export function defineCustomElement<
  42. Props = {},
  43. RawBindings = {},
  44. D = {},
  45. C extends ComputedOptions = {},
  46. M extends MethodOptions = {},
  47. Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  48. Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  49. E extends EmitsOptions = EmitsOptions,
  50. EE extends string = string,
  51. I extends ComponentInjectOptions = {},
  52. II extends string = string,
  53. S extends SlotsType = {}
  54. >(
  55. options: ComponentOptionsWithoutProps<
  56. Props,
  57. RawBindings,
  58. D,
  59. C,
  60. M,
  61. Mixin,
  62. Extends,
  63. E,
  64. EE,
  65. I,
  66. II,
  67. S
  68. > & { styles?: string[] }
  69. ): VueElementConstructor<Props>
  70. // overload 3: object format with array props declaration
  71. export function defineCustomElement<
  72. PropNames extends string,
  73. RawBindings,
  74. D,
  75. C extends ComputedOptions = {},
  76. M extends MethodOptions = {},
  77. Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  78. Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  79. E extends EmitsOptions = Record<string, any>,
  80. EE extends string = string,
  81. I extends ComponentInjectOptions = {},
  82. II extends string = string,
  83. S extends SlotsType = {}
  84. >(
  85. options: ComponentOptionsWithArrayProps<
  86. PropNames,
  87. RawBindings,
  88. D,
  89. C,
  90. M,
  91. Mixin,
  92. Extends,
  93. E,
  94. EE,
  95. I,
  96. II,
  97. S
  98. > & { styles?: string[] }
  99. ): VueElementConstructor<{ [K in PropNames]: any }>
  100. // overload 4: object format with object props declaration
  101. export function defineCustomElement<
  102. PropsOptions extends Readonly<ComponentPropsOptions>,
  103. RawBindings,
  104. D,
  105. C extends ComputedOptions = {},
  106. M extends MethodOptions = {},
  107. Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  108. Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  109. E extends EmitsOptions = Record<string, any>,
  110. EE extends string = string,
  111. I extends ComponentInjectOptions = {},
  112. II extends string = string,
  113. S extends SlotsType = {}
  114. >(
  115. options: ComponentOptionsWithObjectProps<
  116. PropsOptions,
  117. RawBindings,
  118. D,
  119. C,
  120. M,
  121. Mixin,
  122. Extends,
  123. E,
  124. EE,
  125. I,
  126. II,
  127. S
  128. > & { styles?: string[] }
  129. ): VueElementConstructor<ExtractPropTypes<PropsOptions>>
  130. // overload 5: defining a custom element from the returned value of
  131. // `defineComponent`
  132. export function defineCustomElement(options: {
  133. new (...args: any[]): ComponentPublicInstance
  134. }): VueElementConstructor
  135. export function defineCustomElement(
  136. options: any,
  137. hydrate?: RootHydrateFunction
  138. ): VueElementConstructor {
  139. const Comp = defineComponent(options) as any
  140. class VueCustomElement extends VueElement {
  141. static def = Comp
  142. constructor(initialProps?: Record<string, any>) {
  143. super(Comp, initialProps, hydrate)
  144. }
  145. }
  146. return VueCustomElement
  147. }
  148. export const defineSSRCustomElement = ((options: any) => {
  149. // @ts-ignore
  150. return defineCustomElement(options, hydrate)
  151. }) as typeof defineCustomElement
  152. const BaseClass = (
  153. typeof HTMLElement !== 'undefined' ? HTMLElement : class {}
  154. ) as typeof HTMLElement
  155. type InnerComponentDef = ConcreteComponent & { styles?: string[] }
  156. export class VueElement extends BaseClass {
  157. /**
  158. * @internal
  159. */
  160. _instance: ComponentInternalInstance | null = null
  161. private _connected = false
  162. private _resolved = false
  163. private _numberProps: Record<string, true> | null = null
  164. private _styles?: HTMLStyleElement[]
  165. constructor(
  166. private _def: InnerComponentDef,
  167. private _props: Record<string, any> = {},
  168. hydrate?: RootHydrateFunction
  169. ) {
  170. super()
  171. if (this.shadowRoot && hydrate) {
  172. hydrate(this._createVNode(), this.shadowRoot)
  173. } else {
  174. if (__DEV__ && this.shadowRoot) {
  175. warn(
  176. `Custom element has pre-rendered declarative shadow root but is not ` +
  177. `defined as hydratable. Use \`defineSSRCustomElement\`.`
  178. )
  179. }
  180. this.attachShadow({ mode: 'open' })
  181. if (!(this._def as ComponentOptions).__asyncLoader) {
  182. // for sync component defs we can immediately resolve props
  183. this._resolveProps(this._def)
  184. }
  185. }
  186. }
  187. connectedCallback() {
  188. this._connected = true
  189. if (!this._instance) {
  190. if (this._resolved) {
  191. this._update()
  192. } else {
  193. this._resolveDef()
  194. }
  195. }
  196. }
  197. disconnectedCallback() {
  198. this._connected = false
  199. nextTick(() => {
  200. if (!this._connected) {
  201. render(null, this.shadowRoot!)
  202. this._instance = null
  203. }
  204. })
  205. }
  206. /**
  207. * resolve inner component definition (handle possible async component)
  208. */
  209. private _resolveDef() {
  210. this._resolved = true
  211. // set initial attrs
  212. for (let i = 0; i < this.attributes.length; i++) {
  213. this._setAttr(this.attributes[i].name)
  214. }
  215. // watch future attr changes
  216. new MutationObserver(mutations => {
  217. for (const m of mutations) {
  218. this._setAttr(m.attributeName!)
  219. }
  220. }).observe(this, { attributes: true })
  221. const resolve = (def: InnerComponentDef, isAsync = false) => {
  222. const { props, styles } = def
  223. // cast Number-type props set before resolve
  224. let numberProps
  225. if (props && !isArray(props)) {
  226. for (const key in props) {
  227. const opt = props[key]
  228. if (opt === Number || (opt && opt.type === Number)) {
  229. if (key in this._props) {
  230. this._props[key] = toNumber(this._props[key])
  231. }
  232. ;(numberProps || (numberProps = Object.create(null)))[
  233. camelize(key)
  234. ] = true
  235. }
  236. }
  237. }
  238. this._numberProps = numberProps
  239. if (isAsync) {
  240. // defining getter/setters on prototype
  241. // for sync defs, this already happened in the constructor
  242. this._resolveProps(def)
  243. }
  244. // apply CSS
  245. this._applyStyles(styles)
  246. // initial render
  247. this._update()
  248. }
  249. const asyncDef = (this._def as ComponentOptions).__asyncLoader
  250. if (asyncDef) {
  251. asyncDef().then(def => resolve(def, true))
  252. } else {
  253. resolve(this._def)
  254. }
  255. }
  256. private _resolveProps(def: InnerComponentDef) {
  257. const { props } = def
  258. const declaredPropKeys = isArray(props) ? props : Object.keys(props || {})
  259. // check if there are props set pre-upgrade or connect
  260. for (const key of Object.keys(this)) {
  261. if (key[0] !== '_' && declaredPropKeys.includes(key)) {
  262. this._setProp(key, this[key as keyof this], true, false)
  263. }
  264. }
  265. // defining getter/setters on prototype
  266. for (const key of declaredPropKeys.map(camelize)) {
  267. Object.defineProperty(this, key, {
  268. get() {
  269. return this._getProp(key)
  270. },
  271. set(val) {
  272. this._setProp(key, val)
  273. }
  274. })
  275. }
  276. }
  277. protected _setAttr(key: string) {
  278. let value = this.getAttribute(key)
  279. const camelKey = camelize(key)
  280. if (this._numberProps && this._numberProps[camelKey]) {
  281. value = toNumber(value)
  282. }
  283. this._setProp(camelKey, value, false)
  284. }
  285. /**
  286. * @internal
  287. */
  288. protected _getProp(key: string) {
  289. return this._props[key]
  290. }
  291. /**
  292. * @internal
  293. */
  294. protected _setProp(
  295. key: string,
  296. val: any,
  297. shouldReflect = true,
  298. shouldUpdate = true
  299. ) {
  300. if (val !== this._props[key]) {
  301. this._props[key] = val
  302. if (shouldUpdate && this._instance) {
  303. this._update()
  304. }
  305. // reflect
  306. if (shouldReflect) {
  307. if (val === true) {
  308. this.setAttribute(hyphenate(key), '')
  309. } else if (typeof val === 'string' || typeof val === 'number') {
  310. this.setAttribute(hyphenate(key), val + '')
  311. } else if (!val) {
  312. this.removeAttribute(hyphenate(key))
  313. }
  314. }
  315. }
  316. }
  317. private _update() {
  318. render(this._createVNode(), this.shadowRoot!)
  319. }
  320. private _createVNode(): VNode<any, any> {
  321. const vnode = createVNode(this._def, extend({}, this._props))
  322. if (!this._instance) {
  323. vnode.ce = instance => {
  324. this._instance = instance
  325. instance.isCE = true
  326. // HMR
  327. if (__DEV__) {
  328. instance.ceReload = newStyles => {
  329. // always reset styles
  330. if (this._styles) {
  331. this._styles.forEach(s => this.shadowRoot!.removeChild(s))
  332. this._styles.length = 0
  333. }
  334. this._applyStyles(newStyles)
  335. this._instance = null
  336. this._update()
  337. }
  338. }
  339. const dispatch = (event: string, args: any[]) => {
  340. this.dispatchEvent(
  341. new CustomEvent(event, {
  342. detail: args
  343. })
  344. )
  345. }
  346. // intercept emit
  347. instance.emit = (event: string, ...args: any[]) => {
  348. // dispatch both the raw and hyphenated versions of an event
  349. // to match Vue behavior
  350. dispatch(event, args)
  351. if (hyphenate(event) !== event) {
  352. dispatch(hyphenate(event), args)
  353. }
  354. }
  355. // locate nearest Vue custom element parent for provide/inject
  356. let parent: Node | null = this
  357. while (
  358. (parent =
  359. parent && (parent.parentNode || (parent as ShadowRoot).host))
  360. ) {
  361. if (parent instanceof VueElement) {
  362. instance.parent = parent._instance
  363. instance.provides = parent._instance!.provides
  364. break
  365. }
  366. }
  367. }
  368. }
  369. return vnode
  370. }
  371. private _applyStyles(styles: string[] | undefined) {
  372. if (styles) {
  373. styles.forEach(css => {
  374. const s = document.createElement('style')
  375. s.textContent = css
  376. this.shadowRoot!.appendChild(s)
  377. // record for HMR
  378. if (__DEV__) {
  379. ;(this._styles || (this._styles = [])).push(s)
  380. }
  381. })
  382. }
  383. }
  384. }