2021-07-12 19:32:38 +00:00
|
|
|
import {
|
|
|
|
ComponentOptionsMixin,
|
|
|
|
ComponentOptionsWithArrayProps,
|
|
|
|
ComponentOptionsWithObjectProps,
|
|
|
|
ComponentOptionsWithoutProps,
|
|
|
|
ComponentPropsOptions,
|
|
|
|
ComponentPublicInstance,
|
|
|
|
ComputedOptions,
|
|
|
|
EmitsOptions,
|
|
|
|
MethodOptions,
|
|
|
|
RenderFunction,
|
|
|
|
SetupContext,
|
|
|
|
ComponentInternalInstance,
|
|
|
|
VNode,
|
|
|
|
RootHydrateFunction,
|
|
|
|
ExtractPropTypes,
|
|
|
|
createVNode,
|
|
|
|
defineComponent,
|
|
|
|
nextTick,
|
2021-07-22 20:33:32 +00:00
|
|
|
warn,
|
|
|
|
ComponentOptions
|
2021-07-12 19:32:38 +00:00
|
|
|
} from '@vue/runtime-core'
|
2021-07-13 16:23:51 +00:00
|
|
|
import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared'
|
2021-07-12 19:32:38 +00:00
|
|
|
import { hydrate, render } from '.'
|
|
|
|
|
2021-07-22 22:19:54 +00:00
|
|
|
export type VueElementConstructor<P = {}> = {
|
|
|
|
new (initialProps?: Record<string, any>): VueElement & P
|
2021-07-12 19:32:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
>(
|
|
|
|
options: ComponentOptionsWithoutProps<
|
|
|
|
Props,
|
|
|
|
RawBindings,
|
|
|
|
D,
|
|
|
|
C,
|
|
|
|
M,
|
|
|
|
Mixin,
|
|
|
|
Extends,
|
|
|
|
E,
|
|
|
|
EE
|
2021-07-22 20:33:32 +00:00
|
|
|
> & { styles?: string[] }
|
2021-07-12 19:32:38 +00:00
|
|
|
): 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
|
|
|
|
>(
|
|
|
|
options: ComponentOptionsWithArrayProps<
|
|
|
|
PropNames,
|
|
|
|
RawBindings,
|
|
|
|
D,
|
|
|
|
C,
|
|
|
|
M,
|
|
|
|
Mixin,
|
|
|
|
Extends,
|
|
|
|
E,
|
|
|
|
EE
|
2021-07-22 20:33:32 +00:00
|
|
|
> & { styles?: string[] }
|
2021-07-12 19:32:38 +00:00
|
|
|
): 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
|
|
|
|
>(
|
|
|
|
options: ComponentOptionsWithObjectProps<
|
|
|
|
PropsOptions,
|
|
|
|
RawBindings,
|
|
|
|
D,
|
|
|
|
C,
|
|
|
|
M,
|
|
|
|
Mixin,
|
|
|
|
Extends,
|
|
|
|
E,
|
|
|
|
EE
|
2021-07-22 20:33:32 +00:00
|
|
|
> & { styles?: string[] }
|
2021-07-12 19:32:38 +00:00
|
|
|
): 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,
|
|
|
|
hydate?: RootHydrateFunction
|
|
|
|
): VueElementConstructor {
|
|
|
|
const Comp = defineComponent(options as any)
|
|
|
|
const { props } = options
|
|
|
|
const rawKeys = props ? (isArray(props) ? props : Object.keys(props)) : []
|
|
|
|
const attrKeys = rawKeys.map(hyphenate)
|
|
|
|
const propKeys = rawKeys.map(camelize)
|
|
|
|
|
|
|
|
class VueCustomElement extends VueElement {
|
2021-07-22 21:48:15 +00:00
|
|
|
static def = Comp
|
2021-07-12 19:32:38 +00:00
|
|
|
static get observedAttributes() {
|
|
|
|
return attrKeys
|
|
|
|
}
|
2021-07-22 22:19:54 +00:00
|
|
|
constructor(initialProps?: Record<string, any>) {
|
|
|
|
super(Comp, initialProps, attrKeys, propKeys, hydate)
|
2021-07-12 19:32:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const key of propKeys) {
|
|
|
|
Object.defineProperty(VueCustomElement.prototype, key, {
|
|
|
|
get() {
|
|
|
|
return this._getProp(key)
|
|
|
|
},
|
|
|
|
set(val) {
|
|
|
|
this._setProp(key, val)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return VueCustomElement
|
|
|
|
}
|
|
|
|
|
|
|
|
export const defineSSRCustomElement = ((options: any) => {
|
|
|
|
// @ts-ignore
|
|
|
|
return defineCustomElement(options, hydrate)
|
|
|
|
}) as typeof defineCustomElement
|
|
|
|
|
2021-07-19 22:24:18 +00:00
|
|
|
const BaseClass = (
|
|
|
|
typeof HTMLElement !== 'undefined' ? HTMLElement : class {}
|
|
|
|
) as typeof HTMLElement
|
2021-07-16 14:40:06 +00:00
|
|
|
|
|
|
|
export class VueElement extends BaseClass {
|
2021-07-12 19:32:38 +00:00
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
_instance: ComponentInternalInstance | null = null
|
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
_connected = false
|
|
|
|
|
|
|
|
constructor(
|
2021-07-22 20:33:32 +00:00
|
|
|
private _def: ComponentOptions & { styles?: string[] },
|
2021-07-22 22:19:54 +00:00
|
|
|
private _props: Record<string, any> = {},
|
2021-07-13 16:23:51 +00:00
|
|
|
private _attrKeys: string[],
|
|
|
|
private _propKeys: string[],
|
2021-07-12 19:32:38 +00:00
|
|
|
hydrate?: RootHydrateFunction
|
|
|
|
) {
|
|
|
|
super()
|
|
|
|
if (this.shadowRoot && hydrate) {
|
2021-07-13 16:23:51 +00:00
|
|
|
hydrate(this._createVNode(), this.shadowRoot)
|
2021-07-12 19:32:38 +00:00
|
|
|
} 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' })
|
2021-07-22 21:48:15 +00:00
|
|
|
this._applyStyles()
|
2021-07-12 19:32:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
|
2021-07-13 16:23:51 +00:00
|
|
|
if (this._attrKeys.includes(name)) {
|
|
|
|
this._setProp(camelize(name), toNumber(newValue), false)
|
2021-07-12 19:32:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
this._connected = true
|
|
|
|
if (!this._instance) {
|
2021-07-13 16:23:51 +00:00
|
|
|
// check if there are props set pre-upgrade
|
|
|
|
for (const key of this._propKeys) {
|
|
|
|
if (this.hasOwnProperty(key)) {
|
|
|
|
const value = (this as any)[key]
|
|
|
|
delete (this as any)[key]
|
|
|
|
this._setProp(key, value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
render(this._createVNode(), this.shadowRoot!)
|
2021-07-12 19:32:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
disconnectedCallback() {
|
|
|
|
this._connected = false
|
|
|
|
nextTick(() => {
|
|
|
|
if (!this._connected) {
|
|
|
|
render(null, this.shadowRoot!)
|
|
|
|
this._instance = null
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-07-13 16:23:51 +00:00
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
2021-07-12 19:32:38 +00:00
|
|
|
protected _getProp(key: string) {
|
|
|
|
return this._props[key]
|
|
|
|
}
|
|
|
|
|
2021-07-13 16:23:51 +00:00
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
protected _setProp(key: string, val: any, shouldReflect = true) {
|
|
|
|
if (val !== this._props[key]) {
|
|
|
|
this._props[key] = val
|
|
|
|
if (this._instance) {
|
|
|
|
render(this._createVNode(), this.shadowRoot!)
|
|
|
|
}
|
|
|
|
// 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))
|
|
|
|
}
|
|
|
|
}
|
2021-07-12 19:32:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-13 16:23:51 +00:00
|
|
|
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
|
2021-07-22 21:48:15 +00:00
|
|
|
// HMR
|
|
|
|
if (__DEV__) {
|
|
|
|
instance.appContext.reload = () => {
|
|
|
|
render(this._createVNode(), this.shadowRoot!)
|
|
|
|
this.shadowRoot!.querySelectorAll('style').forEach(s => {
|
|
|
|
this.shadowRoot!.removeChild(s)
|
|
|
|
})
|
|
|
|
this._applyStyles()
|
|
|
|
}
|
|
|
|
}
|
2021-07-12 19:32:38 +00:00
|
|
|
|
2021-07-13 16:23:51 +00:00
|
|
|
// intercept emit
|
|
|
|
instance.emit = (event: string, ...args: any[]) => {
|
|
|
|
this.dispatchEvent(
|
|
|
|
new CustomEvent(event, {
|
|
|
|
detail: args
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
2021-07-12 19:32:38 +00:00
|
|
|
|
2021-07-13 16:23:51 +00:00
|
|
|
// 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
|
|
|
|
break
|
|
|
|
}
|
2021-07-12 19:32:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return vnode
|
|
|
|
}
|
2021-07-22 21:48:15 +00:00
|
|
|
|
|
|
|
private _applyStyles() {
|
|
|
|
if (this._def.styles) {
|
|
|
|
this._def.styles.forEach(css => {
|
|
|
|
const s = document.createElement('style')
|
|
|
|
s.textContent = css
|
|
|
|
this.shadowRoot!.appendChild(s)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2021-07-12 19:32:38 +00:00
|
|
|
}
|