import {
+ defineAsyncComponent,
defineCustomElement,
h,
inject,
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 =
+ `<my-el-async msg="hello"></my-el-async>` +
+ `<my-el-async msg="world"></my-el-async>`
+
+ 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(
+ `<div>hello</div><style>div { color: red }</style>`
+ )
+ expect(e2.shadowRoot!.innerHTML).toBe(
+ `<div>world</div><style>div { color: red }</style>`
+ )
+
+ // attr
+ e1.setAttribute('msg', 'attr')
+ await nextTick()
+ expect((e1 as any).msg).toBe('attr')
+ expect(e1.shadowRoot!.innerHTML).toBe(
+ `<div>attr</div><style>div { color: red }</style>`
+ )
+
+ // props
+ expect(`msg` in e1).toBe(true)
+ ;(e1 as any).msg = 'prop'
+ expect(e1.getAttribute('msg')).toBe('prop')
+ expect(e1.shadowRoot!.innerHTML).toBe(
+ `<div>prop</div><style>div { color: red }</style>`
+ )
+ })
+
+ 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(`<div>hello</div>`)
+ expect(e2.shadowRoot!.innerHTML).toBe(`<div>world</div>`)
+
+ e1.msg = 'world'
+ expect(e1.shadowRoot!.innerHTML).toBe(`<div>world</div>`)
+
+ e2.msg = 'hello'
+ expect(e2.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
+ })
+ })
})
defineComponent,
nextTick,
warn,
+ ConcreteComponent,
ComponentOptions
} from '@vue/runtime-core'
import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared'
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<string, any>) {
- 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
}
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 _styles?: HTMLStyleElement[]
constructor(
- private _def: ComponentOptions & { styles?: string[] },
+ private _def: InnerComponentDef,
private _props: Record<string, any> = {},
- private _attrKeys: string[],
- private _propKeys: string[],
hydrate?: RootHydrateFunction
) {
super()
)
}
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!)
}
}
})
}
+ /**
+ * 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
*/
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!)
+ }
}
}
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)