From f482a61db5d1b1b8d6b2c3d3f26e85c903b44804 Mon Sep 17 00:00:00 2001 From: Joe Bond Date: Thu, 11 Jul 2024 15:48:09 -0400 Subject: [PATCH 1/4] fix(v-model): update initial render to run after nextTick --- packages/runtime-dom/src/apiCustomElement.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 2a96cafa0ea..786ed9a6a8a 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -290,8 +290,10 @@ export class VueElement extends BaseClass { // apply CSS this._applyStyles(styles) - // initial render - this._update() + nextTick(() => { + // initial render + this._update() + }) } const asyncDef = (this._def as ComponentOptions).__asyncLoader From 96eebe274d4e6d4921d59e7f443d13b3f88ee791 Mon Sep 17 00:00:00 2001 From: Joe Bond Date: Fri, 12 Jul 2024 11:56:15 -0400 Subject: [PATCH 2/4] test: update tests to account for next tick --- .../__tests__/customElement.spec.ts | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index 62ba166b030..9f39ae191e5 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -35,20 +35,22 @@ describe('defineCustomElement', () => { }) customElements.define('my-element', E) - test('should work', () => { + test('should work', async () => { container.innerHTML = `` const e = container.childNodes[0] as VueElement + await nextTick() expect(e).toBeInstanceOf(E) expect(e._instance).toBeTruthy() expect(e.shadowRoot!.innerHTML).toBe(`
hello
`) }) - test('should work w/ manual instantiation', () => { + test('should work w/ manual instantiation', async () => { const e = new E({ msg: 'inline' }) // should lazy init expect(e._instance).toBe(null) // should initialize on connect container.appendChild(e) + await nextTick() expect(e._instance).toBeTruthy() expect(e.shadowRoot!.innerHTML).toBe(`
inline
`) }) @@ -98,6 +100,7 @@ describe('defineCustomElement', () => { }) const app = createApp(containerComp) app.mount(container) + await nextTick() const myInputEl = container.querySelector('my-el-input')! const inputEl = myInputEl.shadowRoot!.querySelector('input')! await nextTick() @@ -112,6 +115,7 @@ describe('defineCustomElement', () => { test('should not unmount on move', async () => { container.innerHTML = `
` + await nextTick() const e = container.childNodes[0].childNodes[0] as VueElement const i = e._instance // moving from one parent to another - this will trigger both disconnect @@ -157,6 +161,7 @@ describe('defineCustomElement', () => { test('props via attribute', async () => { // bazQux should map to `baz-qux` attribute container.innerHTML = `` + await nextTick() const e = container.childNodes[0] as VueElement expect(e.shadowRoot!.innerHTML).toBe('
hello
bye
') @@ -177,6 +182,7 @@ describe('defineCustomElement', () => { e.foo = 'one' e.bar = { x: 'two' } container.appendChild(e) + await nextTick() expect(e.shadowRoot!.innerHTML).toBe('
one
two
') // reflect @@ -227,6 +233,7 @@ describe('defineCustomElement', () => { }) customElements.define('my-el-props-cast', E) container.innerHTML = `` + await nextTick() const e = container.childNodes[0] as VueElement expect(e.shadowRoot!.innerHTML).toBe( `1 number false boolean 12345 string`, @@ -248,7 +255,7 @@ describe('defineCustomElement', () => { }) // #4772 - test('attr casting w/ programmatic creation', () => { + test('attr casting w/ programmatic creation', async () => { const E = defineCustomElement({ props: { foo: Number, @@ -261,10 +268,11 @@ describe('defineCustomElement', () => { const el = document.createElement('my-element-programmatic') as any el.setAttribute('foo', '123') container.appendChild(el) + await nextTick() expect(el.shadowRoot.innerHTML).toBe(`foo type: number`) }) - test('handling properties set before upgrading', () => { + test('handling properties set before upgrading', async () => { const E = defineCustomElement({ props: { foo: String, @@ -284,12 +292,13 @@ describe('defineCustomElement', () => { el.notProp = 1 container.appendChild(el) customElements.define('my-el-upgrade', E) + await nextTick() expect(el.shadowRoot.firstChild.innerHTML).toBe(`foo: hello`) // should not reflect if not declared as a prop expect(el.hasAttribute('not-prop')).toBe(false) }) - test('handle properties set before connecting', () => { + test('handle properties set before connecting', async () => { const obj = { a: 1 } const E = defineCustomElement({ props: { @@ -310,6 +319,7 @@ describe('defineCustomElement', () => { el.post = obj container.appendChild(el) + await nextTick() expect(el.shadowRoot.innerHTML).toBe(JSON.stringify(obj)) }) @@ -328,7 +338,7 @@ describe('defineCustomElement', () => { }) // # 5793 - test('set number value in dom property', () => { + test('set number value in dom property', async () => { const E = defineCustomElement({ props: { 'max-age': Number, @@ -341,12 +351,13 @@ describe('defineCustomElement', () => { customElements.define('my-element-number-property', E) const el = document.createElement('my-element-number-property') as any container.appendChild(el) + await nextTick() el.maxAge = 50 expect(el.maxAge).toBe(50) expect(el.shadowRoot.innerHTML).toBe('max age: 50/type: number') }) - test('support direct setup function syntax with extra options', () => { + test('support direct setup function syntax with extra options', async () => { const E = defineCustomElement( props => { return () => props.text @@ -359,6 +370,7 @@ describe('defineCustomElement', () => { ) customElements.define('my-el-setup-with-props', E) container.innerHTML = `` + await nextTick() const e = container.childNodes[0] as VueElement expect(e.shadowRoot!.innerHTML).toBe('hello') }) @@ -374,6 +386,7 @@ describe('defineCustomElement', () => { test('attrs via attribute', async () => { container.innerHTML = `` + await nextTick() const e = container.childNodes[0] as VueElement expect(e.shadowRoot!.innerHTML).toBe('
hello
') @@ -382,11 +395,12 @@ describe('defineCustomElement', () => { expect(e.shadowRoot!.innerHTML).toBe('
changed
') }) - test('non-declared properties should not show up in $attrs', () => { + test('non-declared properties should not show up in $attrs', async () => { const e = new E() // @ts-expect-error e.foo = '123' container.appendChild(e) + await nextTick() expect(e.shadowRoot!.innerHTML).toBe('
') }) }) @@ -409,16 +423,18 @@ describe('defineCustomElement', () => { const E = defineCustomElement(CompDef) customElements.define('my-el-emits', E) - test('emit on connect', () => { + test('emit on connect', async () => { const e = new E() const spy = vi.fn() e.addEventListener('created', spy) container.appendChild(e) + await nextTick() expect(spy).toHaveBeenCalled() }) - test('emit on interaction', () => { + test('emit on interaction', async () => { container.innerHTML = `` + await nextTick() const e = container.childNodes[0] as VueElement const spy = vi.fn() e.addEventListener('my-click', spy) @@ -430,8 +446,9 @@ describe('defineCustomElement', () => { }) // #5373 - test('case transform for camelCase event', () => { + test('case transform for camelCase event', async () => { container.innerHTML = `` + await nextTick() const e = container.childNodes[0] as VueElement const spy1 = vi.fn() e.addEventListener('myEvent', spy1) @@ -449,6 +466,7 @@ describe('defineCustomElement', () => { const E = defineCustomElement(defineAsyncComponent(() => p)) customElements.define('my-async-el-emits', E) container.innerHTML = `` + await nextTick() const e = container.childNodes[0] as VueElement const spy = vi.fn() e.addEventListener('my-click', spy) @@ -471,6 +489,7 @@ describe('defineCustomElement', () => { ) customElements.define('my-async-el-props-emits', E) container.innerHTML = `` + await nextTick() const e = container.childNodes[0] as VueElement const spy = vi.fn() e.addEventListener('my-click', spy) @@ -500,8 +519,9 @@ describe('defineCustomElement', () => { }) customElements.define('my-el-slots', E) - test('default slot', () => { + test('default slot', async () => { container.innerHTML = `hi` + await nextTick() const e = container.childNodes[0] as VueElement // native slots allocation does not affect innerHTML, so we just // verify that we've rendered the correct native slots here... @@ -532,6 +552,8 @@ describe('defineCustomElement', () => { }) customElements.define('my-provider', Provider) container.innerHTML = `` + await nextTick() + await nextTick() const provider = container.childNodes[0] as VueElement const consumer = provider.shadowRoot!.childNodes[0] as VueElement @@ -555,6 +577,7 @@ describe('defineCustomElement', () => { customElements.define('my-provider-2', Provider) container.innerHTML = `` + await nextTick() const provider = container.childNodes[0] const consumer = provider.childNodes[0] as VueElement expect(consumer.shadowRoot!.innerHTML).toBe(`
injected!
`) @@ -596,6 +619,10 @@ describe('defineCustomElement', () => { customElements.define('provider-b', ProviderB) customElements.define('my-multi-consumer', Consumer) container.innerHTML = `` + // three components nested, so three ticks are needed + await nextTick() + await nextTick() + await nextTick() const providerA = container.childNodes[0] as VueElement const providerB = providerA.shadowRoot!.childNodes[0] as VueElement const consumer = providerB.shadowRoot!.childNodes[0] as VueElement From 87c967d09619f12e93631bbeccdba6655cee5d7a Mon Sep 17 00:00:00 2001 From: Joe Bond Date: Fri, 12 Jul 2024 12:59:34 -0400 Subject: [PATCH 3/4] chore: create example of broken version in sfc playground --- packages/runtime-dom/src/apiCustomElement.ts | 8 ++++---- packages/sfc-playground/src/App.vue | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 786ed9a6a8a..463d07da78e 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -290,10 +290,10 @@ export class VueElement extends BaseClass { // apply CSS this._applyStyles(styles) - nextTick(() => { - // initial render - this._update() - }) + // nextTick(() => { + // initial render + this._update() + // }) } const asyncDef = (this._def as ComponentOptions).__asyncLoader diff --git a/packages/sfc-playground/src/App.vue b/packages/sfc-playground/src/App.vue index 7501b200ce8..fa0000963c6 100644 --- a/packages/sfc-playground/src/App.vue +++ b/packages/sfc-playground/src/App.vue @@ -54,7 +54,7 @@ const sfcOptions = computed( template: { isProd: productionMode.value, compilerOptions: { - isCustomElement: (tag: string) => tag === 'mjx-container', + isCustomElement: (tag: string) => tag === 'mjx-container' || tag.startsWith('my-'), }, }, }), From 4ba2dbdcec5443005e9c51eb3e95a1b855a4d498 Mon Sep 17 00:00:00 2001 From: Joe Bond Date: Fri, 12 Jul 2024 13:06:48 -0400 Subject: [PATCH 4/4] chore: revert change to show update working --- packages/runtime-dom/src/apiCustomElement.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 463d07da78e..445efabaeed 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -290,10 +290,11 @@ export class VueElement extends BaseClass { // apply CSS this._applyStyles(styles) - // nextTick(() => { - // initial render - this._update() - // }) + // #9885 - nextTick fixes duplication issue with v-model + nextTick(() => { + // initial render + this._update() + }) } const asyncDef = (this._def as ComponentOptions).__asyncLoader