From 9a36bac722ff021e09e5d665b4cdb26817b0262e Mon Sep 17 00:00:00 2001 From: Thorsten Luenborg Date: Tue, 25 Jan 2022 19:49:59 +0100 Subject: [PATCH 1/2] fix(runtime-core): ensure customElements have set domprop listeners synchronously if possible. --- .../__tests__/customElement.spec.ts | 23 ++++++++ packages/runtime-dom/src/apiCustomElement.ts | 54 ++++++++++++------- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index 6c991b9f523..f3ea77b847f 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -247,6 +247,29 @@ describe('defineCustomElement', () => { expect(el.maxAge).toBe(50) expect(el.shadowRoot.innerHTML).toBe('max age: 50/type: number') }) + + test('handle properties set before connecting', () => { + const obj = {} + const E = defineCustomElement({ + props: { + foo: String, + post: Object + }, + setup(props) { + expect(props.foo).toBe('hello') + expect(props.post).toBe(obj) + }, + render() { + return `foo: ${this.foo}` + } + }) + customElements.define('my-el-preconnect', E) + const el = document.createElement('my-el-preconnect') as any + el.foo = 'hello' + el.post = obj + + container.appendChild(el) + }) }) describe('attrs', () => { diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index ecb558b17a4..4de753e38be 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -186,6 +186,10 @@ export class VueElement extends BaseClass { ) } this.attachShadow({ mode: 'open' }) + if (!(this._def as ComponentOptions).__asyncLoader) { + // for sync component defs we can immediately resolve props + this._resolveProps(this._def) + } } } @@ -227,9 +231,8 @@ export class VueElement extends BaseClass { } }).observe(this, { attributes: true }) - const resolve = (def: InnerComponentDef) => { + const resolve = (def: InnerComponentDef, isAsync = false) => { const { props, styles } = def - const declaredPropKeys = isArray(props) ? props : Object.keys(props || {}) // cast Number-type props set before resolve let numberProps @@ -248,23 +251,10 @@ export class VueElement extends BaseClass { } this._numberProps = numberProps - // check if there are props set pre-upgrade or connect - for (const key of Object.keys(this)) { - if (key[0] !== '_' && declaredPropKeys.includes(key)) { - this._setProp(key, this[key as keyof this], true, false) - } - } - - // defining getter/setters on prototype - for (const key of declaredPropKeys.map(camelize)) { - Object.defineProperty(this, key, { - get() { - return this._getProp(key) - }, - set(val) { - this._setProp(key, val) - } - }) + if (isAsync) { + // defining getter/setters on prototype + // for sync defs, this already happened in the constructor + this._resolveProps(def) } // apply CSS @@ -276,12 +266,36 @@ export class VueElement extends BaseClass { const asyncDef = (this._def as ComponentOptions).__asyncLoader if (asyncDef) { - asyncDef().then(resolve) + asyncDef().then(def => resolve(def, true)) } else { resolve(this._def) } } + private _resolveProps(def: InnerComponentDef) { + const { props } = def + const declaredPropKeys = isArray(props) ? props : Object.keys(props || {}) + + // check if there are props set pre-upgrade or connect + for (const key of Object.keys(this)) { + if (key[0] !== '_' && declaredPropKeys.includes(key)) { + this._setProp(key, this[key as keyof this], true, false) + } + } + + // defining getter/setters on prototype + for (const key of declaredPropKeys.map(camelize)) { + Object.defineProperty(this, key, { + get() { + return this._getProp(key) + }, + set(val) { + this._setProp(key, val) + } + }) + } + } + protected _setAttr(key: string) { let value = this.getAttribute(key) const camelKey = camelize(key) From 19c8aa87f45eda716150f995529e2c4ed821dd7b Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 11 Nov 2022 15:41:20 +0800 Subject: [PATCH 2/2] refactor: tweak tests --- .../__tests__/customElement.spec.ts | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index f3ea77b847f..a3d35790f31 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -215,6 +215,30 @@ describe('defineCustomElement', () => { expect(el.hasAttribute('not-prop')).toBe(false) }) + test('handle properties set before connecting', () => { + const obj = { a: 1 } + const E = defineCustomElement({ + props: { + foo: String, + post: Object + }, + setup(props) { + expect(props.foo).toBe('hello') + expect(props.post).toBe(obj) + }, + render() { + return JSON.stringify(this.post) + } + }) + customElements.define('my-el-preconnect', E) + const el = document.createElement('my-el-preconnect') as any + el.foo = 'hello' + el.post = obj + + container.appendChild(el) + expect(el.shadowRoot.innerHTML).toBe(JSON.stringify(obj)) + }) + // https://github.com/vuejs/core/issues/6163 test('handle components with no props', async () => { const E = defineCustomElement({ @@ -247,29 +271,6 @@ describe('defineCustomElement', () => { expect(el.maxAge).toBe(50) expect(el.shadowRoot.innerHTML).toBe('max age: 50/type: number') }) - - test('handle properties set before connecting', () => { - const obj = {} - const E = defineCustomElement({ - props: { - foo: String, - post: Object - }, - setup(props) { - expect(props.foo).toBe('hello') - expect(props.post).toBe(obj) - }, - render() { - return `foo: ${this.foo}` - } - }) - customElements.define('my-el-preconnect', E) - const el = document.createElement('my-el-preconnect') as any - el.foo = 'hello' - el.post = obj - - container.appendChild(el) - }) }) describe('attrs', () => {