From 0cfa2112ce2210300cf2edf272c8c8d11b9355e4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 16 Sep 2021 19:15:15 -0400 Subject: [PATCH] fix(custom-elements): fix number prop casting fix #4370, close #4393 --- .../__tests__/customElement.spec.ts | 50 ++++++++++++++++--- packages/runtime-dom/src/apiCustomElement.ts | 44 ++++++++++++---- 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index 042ac68a7af..7e69ec28ef2 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -136,26 +136,40 @@ describe('defineCustomElement', () => { const E = defineCustomElement({ props: { foo: Number, - bar: Boolean + bar: Boolean, + baz: String }, render() { - return [this.foo, typeof this.foo, this.bar, typeof this.bar].join( - ' ' - ) + return [ + this.foo, + typeof this.foo, + this.bar, + typeof this.bar, + this.baz, + typeof this.baz + ].join(' ') } }) customElements.define('my-el-props-cast', E) - container.innerHTML = `` + container.innerHTML = `` const e = container.childNodes[0] as VueElement - expect(e.shadowRoot!.innerHTML).toBe(`1 number false boolean`) + expect(e.shadowRoot!.innerHTML).toBe( + `1 number false boolean 12345 string` + ) e.setAttribute('bar', '') await nextTick() - expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean`) + expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean 12345 string`) e.setAttribute('foo', '2e1') await nextTick() - expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean`) + expect(e.shadowRoot!.innerHTML).toBe( + `20 number true boolean 12345 string` + ) + + e.setAttribute('baz', '2e1') + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean 2e1 string`) }) test('handling properties set before upgrading', () => { @@ -392,5 +406,25 @@ describe('defineCustomElement', () => { e2.msg = 'hello' expect(e2.shadowRoot!.innerHTML).toBe(`
hello
`) }) + + test('Number prop casting before resolve', async () => { + const E = defineCustomElement( + defineAsyncComponent(() => { + return Promise.resolve({ + props: { n: Number }, + render(this: any) { + return h('div', this.n + ',' + typeof this.n) + } + }) + }) + ) + customElements.define('my-el-async-3', E) + container.innerHTML = `` + + await new Promise(r => setTimeout(r)) + + const e = container.childNodes[0] as VueElement + expect(e.shadowRoot!.innerHTML).toBe(`
20,number
`) + }) }) }) diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index ca29a436c72..f72b8765f6d 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -154,6 +154,7 @@ export class VueElement extends BaseClass { private _connected = false private _resolved = false + private _numberProps: Record | null = null private _styles?: HTMLStyleElement[] constructor( @@ -179,19 +180,18 @@ export class VueElement extends BaseClass { this._setAttr(this.attributes[i].name) } // watch future attr changes - const observer = new MutationObserver(mutations => { + new MutationObserver(mutations => { for (const m of mutations) { this._setAttr(m.attributeName!) } - }) - observer.observe(this, { attributes: true }) + }).observe(this, { attributes: true }) } connectedCallback() { this._connected = true if (!this._instance) { this._resolveDef() - render(this._createVNode(), this.shadowRoot!) + this._update() } } @@ -215,15 +215,33 @@ export class VueElement extends BaseClass { const resolve = (def: InnerComponentDef) => { this._resolved = true + const { props, styles } = def + const hasOptions = !isArray(props) + const rawKeys = props ? (hasOptions ? Object.keys(props) : props) : [] + + // cast Number-type props set before resolve + let numberProps + if (hasOptions) { + for (const key in this._props) { + const opt = props[key] + if (opt === Number || (opt && opt.type === Number)) { + this._props[key] = toNumber(this._props[key]) + ;(numberProps || (numberProps = Object.create(null)))[key] = true + } + } + } + if (numberProps) { + this._numberProps = numberProps + this._update() + } + // 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() { @@ -246,7 +264,11 @@ export class VueElement extends BaseClass { } protected _setAttr(key: string) { - this._setProp(camelize(key), toNumber(this.getAttribute(key)), false) + let value = this.getAttribute(key) + if (this._numberProps && this._numberProps[key]) { + value = toNumber(value) + } + this._setProp(camelize(key), value, false) } /** @@ -263,7 +285,7 @@ export class VueElement extends BaseClass { if (val !== this._props[key]) { this._props[key] = val if (this._instance) { - render(this._createVNode(), this.shadowRoot!) + this._update() } // reflect if (shouldReflect) { @@ -278,6 +300,10 @@ export class VueElement extends BaseClass { } } + private _update() { + render(this._createVNode(), this.shadowRoot!) + } + private _createVNode(): VNode { const vnode = createVNode(this._def, extend({}, this._props)) if (!this._instance) { @@ -298,7 +324,7 @@ export class VueElement extends BaseClass { if (!(this._def as ComponentOptions).__asyncLoader) { // reload this._instance = null - render(this._createVNode(), this.shadowRoot!) + this._update() } } }