diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts
index d4f86f53cf2..6d210a3125d 100644
--- a/packages/runtime-dom/__tests__/customElement.spec.ts
+++ b/packages/runtime-dom/__tests__/customElement.spec.ts
@@ -99,13 +99,66 @@ describe('defineCustomElement', () => {
container.appendChild(e)
expect(e.shadowRoot!.innerHTML).toBe('
one
two
')
+ // reflect
+ // should reflect primitive value
+ expect(e.getAttribute('foo')).toBe('one')
+ // should not reflect rich data
+ expect(e.hasAttribute('bar')).toBe(false)
+
e.foo = 'three'
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe('three
two
')
+ expect(e.getAttribute('foo')).toBe('three')
+
+ e.foo = null
+ await nextTick()
+ expect(e.shadowRoot!.innerHTML).toBe('two
')
+ expect(e.hasAttribute('foo')).toBe(false)
e.bazQux = 'four'
await nextTick()
- expect(e.shadowRoot!.innerHTML).toBe('three
four
')
+ expect(e.shadowRoot!.innerHTML).toBe('four
')
+ expect(e.getAttribute('baz-qux')).toBe('four')
+ })
+
+ test('attribute -> prop type casting', async () => {
+ const E = defineCustomElement({
+ props: {
+ foo: Number,
+ bar: Boolean
+ },
+ render() {
+ return [this.foo, typeof this.foo, this.bar, typeof this.bar].join(
+ ' '
+ )
+ }
+ })
+ customElements.define('my-el-props-cast', E)
+ container.innerHTML = ``
+ const e = container.childNodes[0] as VueElement
+ expect(e.shadowRoot!.innerHTML).toBe(`1 number false boolean`)
+
+ e.setAttribute('bar', '')
+ await nextTick()
+ expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean`)
+
+ e.setAttribute('foo', '2e1')
+ await nextTick()
+ expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean`)
+ })
+
+ test('handling properties set before upgrading', () => {
+ const E = defineCustomElement({
+ props: ['foo'],
+ render() {
+ return `foo: ${this.foo}`
+ }
+ })
+ const el = document.createElement('my-el-upgrade') as any
+ el.foo = 'hello'
+ container.appendChild(el)
+ customElements.define('my-el-upgrade', E)
+ expect(el.shadowRoot.innerHTML).toBe(`foo: hello`)
})
})
diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts
index 82992ae0e09..dc1f8ed781d 100644
--- a/packages/runtime-dom/src/apiCustomElement.ts
+++ b/packages/runtime-dom/src/apiCustomElement.ts
@@ -20,7 +20,7 @@ import {
nextTick,
warn
} from '@vue/runtime-core'
-import { camelize, hyphenate, isArray } from '@vue/shared'
+import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared'
import { hydrate, render } from '.'
type VueElementConstructor = {
@@ -134,7 +134,7 @@ export function defineCustomElement(
return attrKeys
}
constructor() {
- super(Comp, attrKeys, hydate)
+ super(Comp, attrKeys, propKeys, hydate)
}
}
@@ -173,12 +173,13 @@ export class VueElement extends HTMLElement {
constructor(
private _def: Component,
- private _attrs: string[],
+ private _attrKeys: string[],
+ private _propKeys: string[],
hydrate?: RootHydrateFunction
) {
super()
if (this.shadowRoot && hydrate) {
- hydrate(this._initVNode(), this.shadowRoot)
+ hydrate(this._createVNode(), this.shadowRoot)
} else {
if (__DEV__ && this.shadowRoot) {
warn(
@@ -191,15 +192,23 @@ export class VueElement extends HTMLElement {
}
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
- if (this._attrs.includes(name)) {
- this._setProp(camelize(name), newValue)
+ if (this._attrKeys.includes(name)) {
+ this._setProp(camelize(name), toNumber(newValue), false)
}
}
connectedCallback() {
this._connected = true
if (!this._instance) {
- render(this._initVNode(), this.shadowRoot!)
+ // 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!)
}
}
@@ -213,41 +222,61 @@ export class VueElement extends HTMLElement {
})
}
+ /**
+ * @internal
+ */
protected _getProp(key: string) {
return this._props[key]
}
- protected _setProp(key: string, val: any) {
- const oldValue = this._props[key]
- this._props[key] = val
- if (this._instance && val !== oldValue) {
- this._instance.props[key] = val
+ /**
+ * @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))
+ }
+ }
}
}
- protected _initVNode(): VNode {
- const vnode = createVNode(this._def, this._props)
- vnode.ce = instance => {
- this._instance = instance
- instance.isCE = true
+ private _createVNode(): VNode {
+ const vnode = createVNode(this._def, extend({}, this._props))
+ if (!this._instance) {
+ vnode.ce = instance => {
+ this._instance = instance
+ instance.isCE = true
- // intercept emit
- instance.emit = (event: string, ...args: any[]) => {
- this.dispatchEvent(
- new CustomEvent(event, {
- detail: args
- })
- )
- }
+ // intercept emit
+ instance.emit = (event: string, ...args: any[]) => {
+ this.dispatchEvent(
+ new CustomEvent(event, {
+ detail: 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
- break
+ // 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
+ }
}
}
}