diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index 808ad344..74b3eb7f 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -293,7 +293,7 @@ export interface ComponentInternalInstance {
/**
* custom element specific HMR method
*/
- ceReload?: () => void
+ ceReload?: (newStyles?: string[]) => void
// the rest are only for stateful components ---------------------------------
diff --git a/packages/runtime-core/src/hmr.ts b/packages/runtime-core/src/hmr.ts
index a7ccbe9c..eb6ab808 100644
--- a/packages/runtime-core/src/hmr.ts
+++ b/packages/runtime-core/src/hmr.ts
@@ -136,13 +136,21 @@ function reload(id: string, newComp: ComponentOptions | ClassComponent) {
if (instance.ceReload) {
// custom element
hmrDirtyComponents.add(component)
- instance.ceReload()
+ instance.ceReload((newComp as any).styles)
hmrDirtyComponents.delete(component)
} else if (instance.parent) {
// 4. Force the parent instance to re-render. This will cause all updated
// components to be unmounted and re-mounted. Queue the update so that we
// don't end up forcing the same parent to re-render multiple times.
queueJob(instance.parent.update)
+ // instance is the inner component of an async custom element
+ // invoke to reset styles
+ if (
+ (instance.parent.type as ComponentOptions).__asyncLoader &&
+ instance.parent.ceReload
+ ) {
+ instance.parent.ceReload((newComp as any).styles)
+ }
} else if (instance.appContext.reload) {
// root instance mounted via createApp() has a reload method
instance.appContext.reload()
diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts
index e2e44407..042ac68a 100644
--- a/packages/runtime-dom/__tests__/customElement.spec.ts
+++ b/packages/runtime-dom/__tests__/customElement.spec.ts
@@ -1,4 +1,5 @@
import {
+ defineAsyncComponent,
defineCustomElement,
h,
inject,
@@ -300,4 +301,96 @@ describe('defineCustomElement', () => {
expect(style.textContent).toBe(`div { color: red; }`)
})
})
+
+ describe('async', () => {
+ test('should work', async () => {
+ const loaderSpy = jest.fn()
+ const E = defineCustomElement(
+ defineAsyncComponent(() => {
+ loaderSpy()
+ return Promise.resolve({
+ props: ['msg'],
+ styles: [`div { color: red }`],
+ render(this: any) {
+ return h('div', null, this.msg)
+ }
+ })
+ })
+ )
+ customElements.define('my-el-async', E)
+ container.innerHTML =
+ `` +
+ ``
+
+ await new Promise(r => setTimeout(r))
+
+ // loader should be called only once
+ expect(loaderSpy).toHaveBeenCalledTimes(1)
+
+ const e1 = container.childNodes[0] as VueElement
+ const e2 = container.childNodes[1] as VueElement
+
+ // should inject styles
+ expect(e1.shadowRoot!.innerHTML).toBe(
+ `
hello
`
+ )
+ expect(e2.shadowRoot!.innerHTML).toBe(
+ `world
`
+ )
+
+ // attr
+ e1.setAttribute('msg', 'attr')
+ await nextTick()
+ expect((e1 as any).msg).toBe('attr')
+ expect(e1.shadowRoot!.innerHTML).toBe(
+ `attr
`
+ )
+
+ // props
+ expect(`msg` in e1).toBe(true)
+ ;(e1 as any).msg = 'prop'
+ expect(e1.getAttribute('msg')).toBe('prop')
+ expect(e1.shadowRoot!.innerHTML).toBe(
+ `prop
`
+ )
+ })
+
+ test('set DOM property before resolve', async () => {
+ const E = defineCustomElement(
+ defineAsyncComponent(() => {
+ return Promise.resolve({
+ props: ['msg'],
+ render(this: any) {
+ return h('div', this.msg)
+ }
+ })
+ })
+ )
+ customElements.define('my-el-async-2', E)
+
+ const e1 = new E()
+
+ // set property before connect
+ e1.msg = 'hello'
+
+ const e2 = new E()
+
+ container.appendChild(e1)
+ container.appendChild(e2)
+
+ // set property after connect but before resolve
+ e2.msg = 'world'
+
+ await new Promise(r => setTimeout(r))
+
+ expect(e1.shadowRoot!.innerHTML).toBe(`hello
`)
+ expect(e2.shadowRoot!.innerHTML).toBe(`world
`)
+
+ e1.msg = 'world'
+ expect(e1.shadowRoot!.innerHTML).toBe(`world
`)
+
+ e2.msg = 'hello'
+ expect(e2.shadowRoot!.innerHTML).toBe(`hello
`)
+ })
+ })
})
diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts
index b054ccff..ca29a436 100644
--- a/packages/runtime-dom/src/apiCustomElement.ts
+++ b/packages/runtime-dom/src/apiCustomElement.ts
@@ -18,6 +18,7 @@ import {
defineComponent,
nextTick,
warn,
+ ConcreteComponent,
ComponentOptions
} from '@vue/runtime-core'
import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared'
@@ -124,32 +125,13 @@ export function defineCustomElement(
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 {
static def = Comp
- static get observedAttributes() {
- return attrKeys
- }
constructor(initialProps?: Record) {
- super(Comp, initialProps, attrKeys, propKeys, hydate)
+ super(Comp, initialProps, hydate)
}
}
- for (const key of propKeys) {
- Object.defineProperty(VueCustomElement.prototype, key, {
- get() {
- return this._getProp(key)
- },
- set(val) {
- this._setProp(key, val)
- }
- })
- }
-
return VueCustomElement
}
@@ -162,6 +144,8 @@ const BaseClass = (
typeof HTMLElement !== 'undefined' ? HTMLElement : class {}
) as typeof HTMLElement
+type InnerComponentDef = ConcreteComponent & { styles?: string[] }
+
export class VueElement extends BaseClass {
/**
* @internal
@@ -169,13 +153,12 @@ export class VueElement extends BaseClass {
_instance: ComponentInternalInstance | null = null
private _connected = false
+ private _resolved = false
private _styles?: HTMLStyleElement[]
constructor(
- private _def: ComponentOptions & { styles?: string[] },
+ private _def: InnerComponentDef,
private _props: Record = {},
- private _attrKeys: string[],
- private _propKeys: string[],
hydrate?: RootHydrateFunction
) {
super()
@@ -189,27 +172,25 @@ export class VueElement extends BaseClass {
)
}
this.attachShadow({ mode: 'open' })
- this._applyStyles()
}
- }
- attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
- if (this._attrKeys.includes(name)) {
- this._setProp(camelize(name), toNumber(newValue), false)
+ // set initial attrs
+ for (let i = 0; i < this.attributes.length; i++) {
+ this._setAttr(this.attributes[i].name)
}
+ // watch future attr changes
+ const observer = new MutationObserver(mutations => {
+ for (const m of mutations) {
+ this._setAttr(m.attributeName!)
+ }
+ })
+ observer.observe(this, { attributes: true })
}
connectedCallback() {
this._connected = true
if (!this._instance) {
- // 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)
- }
- }
+ this._resolveDef()
render(this._createVNode(), this.shadowRoot!)
}
}
@@ -224,6 +205,50 @@ export class VueElement extends BaseClass {
})
}
+ /**
+ * resolve inner component definition (handle possible async component)
+ */
+ private _resolveDef() {
+ if (this._resolved) {
+ return
+ }
+
+ const resolve = (def: InnerComponentDef) => {
+ this._resolved = true
+ // check if there are props set pre-upgrade or connect
+ for (const key of Object.keys(this)) {
+ if (key[0] !== '_') {
+ this._setProp(key, this[key as keyof this])
+ }
+ }
+ const { props, styles } = def
+ // defining getter/setters on prototype
+ const rawKeys = props ? (isArray(props) ? props : Object.keys(props)) : []
+ for (const key of rawKeys.map(camelize)) {
+ Object.defineProperty(this, key, {
+ get() {
+ return this._getProp(key)
+ },
+ set(val) {
+ this._setProp(key, val)
+ }
+ })
+ }
+ this._applyStyles(styles)
+ }
+
+ const asyncDef = (this._def as ComponentOptions).__asyncLoader
+ if (asyncDef) {
+ asyncDef().then(resolve)
+ } else {
+ resolve(this._def)
+ }
+ }
+
+ protected _setAttr(key: string) {
+ this._setProp(camelize(key), toNumber(this.getAttribute(key)), false)
+ }
+
/**
* @internal
*/
@@ -261,16 +286,20 @@ export class VueElement extends BaseClass {
instance.isCE = true
// HMR
if (__DEV__) {
- instance.ceReload = () => {
- this._instance = null
- // reset styles
+ instance.ceReload = newStyles => {
+ // alawys reset styles
if (this._styles) {
this._styles.forEach(s => this.shadowRoot!.removeChild(s))
this._styles.length = 0
}
- this._applyStyles()
- // reload
- render(this._createVNode(), this.shadowRoot!)
+ this._applyStyles(newStyles)
+ // if this is an async component, ceReload is called from the inner
+ // component so no need to reload the async wrapper
+ if (!(this._def as ComponentOptions).__asyncLoader) {
+ // reload
+ this._instance = null
+ render(this._createVNode(), this.shadowRoot!)
+ }
}
}
@@ -299,9 +328,9 @@ export class VueElement extends BaseClass {
return vnode
}
- private _applyStyles() {
- if (this._def.styles) {
- this._def.styles.forEach(css => {
+ private _applyStyles(styles: string[] | undefined) {
+ if (styles) {
+ styles.forEach(css => {
const s = document.createElement('style')
s.textContent = css
this.shadowRoot!.appendChild(s)