| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- import {
- ComponentOptionsMixin,
- ComponentOptionsWithArrayProps,
- ComponentOptionsWithObjectProps,
- ComponentOptionsWithoutProps,
- ComponentPropsOptions,
- ComponentPublicInstance,
- ComputedOptions,
- EmitsOptions,
- MethodOptions,
- RenderFunction,
- SetupContext,
- ComponentInternalInstance,
- VNode,
- RootHydrateFunction,
- ExtractPropTypes,
- createVNode,
- defineComponent,
- nextTick,
- warn,
- ConcreteComponent,
- ComponentOptions,
- ComponentInjectOptions,
- SlotsType
- } from '@vue/runtime-core'
- import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared'
- import { hydrate, render } from '.'
- export type VueElementConstructor<P = {}> = {
- new (initialProps?: Record<string, any>): VueElement & P
- }
- // defineCustomElement provides the same type inference as defineComponent
- // so most of the following overloads should be kept in sync w/ defineComponent.
- // overload 1: direct setup function
- export function defineCustomElement<Props, RawBindings = object>(
- setup: (
- props: Readonly<Props>,
- ctx: SetupContext
- ) => RawBindings | RenderFunction
- ): VueElementConstructor<Props>
- // overload 2: object format with no props
- export function defineCustomElement<
- Props = {},
- RawBindings = {},
- D = {},
- C extends ComputedOptions = {},
- M extends MethodOptions = {},
- Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
- Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
- E extends EmitsOptions = EmitsOptions,
- EE extends string = string,
- I extends ComponentInjectOptions = {},
- II extends string = string,
- S extends SlotsType = {}
- >(
- options: ComponentOptionsWithoutProps<
- Props,
- RawBindings,
- D,
- C,
- M,
- Mixin,
- Extends,
- E,
- EE,
- I,
- II,
- S
- > & { styles?: string[] }
- ): VueElementConstructor<Props>
- // overload 3: object format with array props declaration
- export function defineCustomElement<
- PropNames extends string,
- RawBindings,
- D,
- C extends ComputedOptions = {},
- M extends MethodOptions = {},
- Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
- Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
- E extends EmitsOptions = Record<string, any>,
- EE extends string = string,
- I extends ComponentInjectOptions = {},
- II extends string = string,
- S extends SlotsType = {}
- >(
- options: ComponentOptionsWithArrayProps<
- PropNames,
- RawBindings,
- D,
- C,
- M,
- Mixin,
- Extends,
- E,
- EE,
- I,
- II,
- S
- > & { styles?: string[] }
- ): VueElementConstructor<{ [K in PropNames]: any }>
- // overload 4: object format with object props declaration
- export function defineCustomElement<
- PropsOptions extends Readonly<ComponentPropsOptions>,
- RawBindings,
- D,
- C extends ComputedOptions = {},
- M extends MethodOptions = {},
- Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
- Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
- E extends EmitsOptions = Record<string, any>,
- EE extends string = string,
- I extends ComponentInjectOptions = {},
- II extends string = string,
- S extends SlotsType = {}
- >(
- options: ComponentOptionsWithObjectProps<
- PropsOptions,
- RawBindings,
- D,
- C,
- M,
- Mixin,
- Extends,
- E,
- EE,
- I,
- II,
- S
- > & { styles?: string[] }
- ): VueElementConstructor<ExtractPropTypes<PropsOptions>>
- // overload 5: defining a custom element from the returned value of
- // `defineComponent`
- export function defineCustomElement(options: {
- new (...args: any[]): ComponentPublicInstance
- }): VueElementConstructor
- export function defineCustomElement(
- options: any,
- hydrate?: RootHydrateFunction
- ): VueElementConstructor {
- const Comp = defineComponent(options) as any
- class VueCustomElement extends VueElement {
- static def = Comp
- constructor(initialProps?: Record<string, any>) {
- super(Comp, initialProps, hydrate)
- }
- }
- return VueCustomElement
- }
- export const defineSSRCustomElement = ((options: any) => {
- // @ts-ignore
- return defineCustomElement(options, hydrate)
- }) as typeof defineCustomElement
- const BaseClass = (
- typeof HTMLElement !== 'undefined' ? HTMLElement : class {}
- ) as typeof HTMLElement
- type InnerComponentDef = ConcreteComponent & { styles?: string[] }
- export class VueElement extends BaseClass {
- /**
- * @internal
- */
- _instance: ComponentInternalInstance | null = null
- private _connected = false
- private _resolved = false
- private _numberProps: Record<string, true> | null = null
- private _styles?: HTMLStyleElement[]
- constructor(
- private _def: InnerComponentDef,
- private _props: Record<string, any> = {},
- hydrate?: RootHydrateFunction
- ) {
- super()
- if (this.shadowRoot && hydrate) {
- hydrate(this._createVNode(), this.shadowRoot)
- } else {
- if (__DEV__ && this.shadowRoot) {
- warn(
- `Custom element has pre-rendered declarative shadow root but is not ` +
- `defined as hydratable. Use \`defineSSRCustomElement\`.`
- )
- }
- this.attachShadow({ mode: 'open' })
- if (!(this._def as ComponentOptions).__asyncLoader) {
- // for sync component defs we can immediately resolve props
- this._resolveProps(this._def)
- }
- }
- }
- connectedCallback() {
- this._connected = true
- if (!this._instance) {
- if (this._resolved) {
- this._update()
- } else {
- this._resolveDef()
- }
- }
- }
- disconnectedCallback() {
- this._connected = false
- nextTick(() => {
- if (!this._connected) {
- render(null, this.shadowRoot!)
- this._instance = null
- }
- })
- }
- /**
- * resolve inner component definition (handle possible async component)
- */
- private _resolveDef() {
- this._resolved = true
- // set initial attrs
- for (let i = 0; i < this.attributes.length; i++) {
- this._setAttr(this.attributes[i].name)
- }
- // watch future attr changes
- new MutationObserver(mutations => {
- for (const m of mutations) {
- this._setAttr(m.attributeName!)
- }
- }).observe(this, { attributes: true })
- const resolve = (def: InnerComponentDef, isAsync = false) => {
- const { props, styles } = def
- // cast Number-type props set before resolve
- let numberProps
- if (props && !isArray(props)) {
- for (const key in props) {
- const opt = props[key]
- if (opt === Number || (opt && opt.type === Number)) {
- if (key in this._props) {
- this._props[key] = toNumber(this._props[key])
- }
- ;(numberProps || (numberProps = Object.create(null)))[
- camelize(key)
- ] = true
- }
- }
- }
- this._numberProps = numberProps
- if (isAsync) {
- // defining getter/setters on prototype
- // for sync defs, this already happened in the constructor
- this._resolveProps(def)
- }
- // apply CSS
- this._applyStyles(styles)
- // initial render
- this._update()
- }
- const asyncDef = (this._def as ComponentOptions).__asyncLoader
- if (asyncDef) {
- asyncDef().then(def => resolve(def, true))
- } else {
- resolve(this._def)
- }
- }
- private _resolveProps(def: InnerComponentDef) {
- const { props } = def
- const declaredPropKeys = isArray(props) ? props : Object.keys(props || {})
- // check if there are props set pre-upgrade or connect
- for (const key of Object.keys(this)) {
- if (key[0] !== '_' && declaredPropKeys.includes(key)) {
- this._setProp(key, this[key as keyof this], true, false)
- }
- }
- // defining getter/setters on prototype
- for (const key of declaredPropKeys.map(camelize)) {
- Object.defineProperty(this, key, {
- get() {
- return this._getProp(key)
- },
- set(val) {
- this._setProp(key, val)
- }
- })
- }
- }
- protected _setAttr(key: string) {
- let value = this.getAttribute(key)
- const camelKey = camelize(key)
- if (this._numberProps && this._numberProps[camelKey]) {
- value = toNumber(value)
- }
- this._setProp(camelKey, value, false)
- }
- /**
- * @internal
- */
- protected _getProp(key: string) {
- return this._props[key]
- }
- /**
- * @internal
- */
- protected _setProp(
- key: string,
- val: any,
- shouldReflect = true,
- shouldUpdate = true
- ) {
- if (val !== this._props[key]) {
- this._props[key] = val
- if (shouldUpdate && this._instance) {
- this._update()
- }
- // reflect
- if (shouldReflect) {
- if (val === true) {
- this.setAttribute(hyphenate(key), '')
- } else if (typeof val === 'string' || typeof val === 'number') {
- this.setAttribute(hyphenate(key), val + '')
- } else if (!val) {
- this.removeAttribute(hyphenate(key))
- }
- }
- }
- }
- private _update() {
- render(this._createVNode(), this.shadowRoot!)
- }
- private _createVNode(): VNode<any, any> {
- const vnode = createVNode(this._def, extend({}, this._props))
- if (!this._instance) {
- vnode.ce = instance => {
- this._instance = instance
- instance.isCE = true
- // HMR
- if (__DEV__) {
- instance.ceReload = newStyles => {
- // always reset styles
- if (this._styles) {
- this._styles.forEach(s => this.shadowRoot!.removeChild(s))
- this._styles.length = 0
- }
- this._applyStyles(newStyles)
- this._instance = null
- this._update()
- }
- }
- const dispatch = (event: string, args: any[]) => {
- this.dispatchEvent(
- new CustomEvent(event, {
- detail: args
- })
- )
- }
- // intercept emit
- instance.emit = (event: string, ...args: any[]) => {
- // dispatch both the raw and hyphenated versions of an event
- // to match Vue behavior
- dispatch(event, args)
- if (hyphenate(event) !== event) {
- dispatch(hyphenate(event), args)
- }
- }
- // locate nearest Vue custom element parent for provide/inject
- let parent: Node | null = this
- while (
- (parent =
- parent && (parent.parentNode || (parent as ShadowRoot).host))
- ) {
- if (parent instanceof VueElement) {
- instance.parent = parent._instance
- instance.provides = parent._instance!.provides
- break
- }
- }
- }
- }
- return vnode
- }
- private _applyStyles(styles: string[] | undefined) {
- if (styles) {
- styles.forEach(css => {
- const s = document.createElement('style')
- s.textContent = css
- this.shadowRoot!.appendChild(s)
- // record for HMR
- if (__DEV__) {
- ;(this._styles || (this._styles = [])).push(s)
- }
- })
- }
- }
- }
|