Subscribe on changes!

[@vue/compat] v-model unwrapping + $attrs forwarding does not match "real" vue@3 behaviour

avatar
Dec 9th 2022

Vue version

3.2.45

Link to minimal reproduction

https://github.com/renatodeleao/playgrounds/tree/issue/vue-compat-component-is-behaviour-is-different-from-regular-vue/vue-2-3-migration-build

Steps to reproduce

The repro is a bare vite project configured by default with @vue/compat, precisely as the migration documentation recommends and without any flags besides setting MODE: 3.

  1. clone the repro https://github.com/renatodeleao/playgrounds
  2. switch to the branch vue-compat-component-is-behaviour-is-different-from-regular-vue
  3. cd into the vue-2-3-migration-build folder
  4. yarn install
  5. yarn dev to run the demo
  6. Type any text into the input box

What is expected?

  1. The msg binding should reflect the value of the input

What is actually happening?

  1. The msg binding does not reflect the value of the input

System Info

System:
    OS: macOS 13.0
    CPU: (10) arm64 Apple M1 Max
    Memory: 160.69 MB / 32.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 16.16.0 - ~/.nvm/versions/node/v16.16.0/bin/node
    Yarn: 1.22.19 - /opt/homebrew/bin/yarn
    npm: 8.11.0 - ~/.nvm/versions/node/v16.16.0/bin/npm
  Browsers:
    Chrome: 108.0.5359.94
    Firefox: 107.0.1
    Safari: 16.1
  npmPackages:
    vue: ^3.2.45 => 3.2.45

Any additional comments?

To make the repro work as expected simply comment out the @vue/compat configuration from vite.config.js and repeat step 6 from "steps to reproduce"

// vite.config.js
export default defineConfig({
-  resolve: {
-    alias: {
-      vue: '@vue/compat'
-    }
-  },
  plugins: [
    vue({
-      template: {
-        compilerOptions: {
-          compatConfig: {
-            MODE: 3
-          }
-        }
-      }
    })
  ]
})

By looking at devtools, I've noticed that when @vue/compat is "on", the $attrs only contains a value entry, while the real vue@3 contains the expected modelValue/onUpdate:modelValue entries. Do note that explicitly setting COMPONENT_V_MODEL: false does not change the outcome. So it seems that the compiler is not "unwrapping" v-model to the correct prop/event combo if they are not explicitly defined in the wrapper component's options. On vue3 v-model is unwrapped correctly regardless

@vue/compat MODE: 3 vue@3
@vue:compat vue@3

Workarounds

explicitly declare the prop modelValue + emits: ['update:modelValue'] on AppInputField and stop relying on $attrs forwarding solves the issue, but since using $attrs work in vue3 I think it can be still considered a bug.

context

I am migrating a very large codebase from vue@2 to vue@3 and I am using "the migration build" @vue/compat to help me out with the process. I'm reaching the final steps, so my expectation is that running a @vue/compat app without any flags besides MODE: 3 basically matched the behaviour of a "real" vue@3 app so I could be 100% sure that I could remove it, with exceptions only for the documented "known limitations"

In the real-world version of this repro, my <app-input-field> is a more complex wrapper around several dynamic "input" components and takes care of the boilerplate structure of a field (label/error etc) and it includes features like dynamic async components among several other stuff. I've been able to trim down to the bare example of the repro, to make sure they didn't influence the outcome.

Edits

  • Using <component> has nothing to do with the issue. So I've amended the title and reproduction example accordingly
avatar
Dec 9th 2022

I'll summarize a few things, some of which you might be aware, just for clarity:

There are two levels of compat:

  1. compiler
  2. runtime

Consequently, there are 2 places that require compat config:

  1. compiler config (you set that to 3)
  2. runtime (you did not set that at all, meaning it defaults to 2)

This asymmetry explains what you are seeing in your demo. And if you set the runtime compatConfig to MODE: 3 as well, it starts working as expected.

import { createApp, configureCompat } from 'vue'
import './style.css'
import App from './App.vue'

configureCompat({
  MODE: 3,
})

createApp(App).mount('#app')

However you might need more fine-grained control, and you say:

Do note that explicitly setting COMPONENT_V_MODEL: false does not change the outcome.

It does not change the outcome in your demo, but it does what it's supposed to:

  • It ensures that the modelValue prop now gets its correct value, and
  • the event is also passed down correctly as update:modelValue to the AppInputField.

But the update:modelValue event still doesn't work for you, and that's because INSTANCE_LISTENERS compat removes all event listeners from $attrs (they are found in $listeners in Vue 2) within AppInputField.

So you have two choices:

A) bind $listeners while the component is still running in Vue 2 compat:

<app-input v-bind="$attrs" v-on="$listeners" />

B) disable INSTANCE_LISTENERS compat (locally in AppInputField or globally, showing the latter):

configureCompat({
  COMPONENT_V_MODEL: false,
  INSTANCE_LISTENERS: false,
})
avatar
Dec 9th 2022

@LinusBorg many thanks for bringing it to my attention and sorry for wasting your time! 🤦 In my "real-world" project, I do not have global runtime compatConfig set, but my individual components have the compatConfig: { MODE: 3 } option. Probably one of the consumers hasn't.

EDIT: yes my wrapper didn't have it :/

avatar
Dec 9th 2022

To clarify what drove me to report this and my confusion for others that might end up here: in the real-world project my whole component tree has compatConfig.MODE === 3, but as above-mentioned, I did not change the global runtime configuration with exported configureCompat. Without that, the built-in runtime components don't seem to be in MODE: 3.

With that in mind and since my AppInputField in real-world uses defineAsyncComponent combined with <component :is="x" />, that makes it render an internal AsyncComponentWrapper between AppInputField and AppInput component tree which breaks the modelValue/onUpdate:modelValue forwarding. Setting configureCompat({ MODE: 3 }) still fixes it.

https://github.com/vuejs/core/blob/665f2ae121ec31d65cf22bd577f12fb1d9ffa4a2/packages/runtime-core/src/apiAsyncComponent.ts#L114-L115 vue-compat-async-component-is

Updated the reproduction demo accordingly => https://github.com/renatodeleao/playgrounds/commit/6e450694cb3cf2f2920dff915c104fb1ad4b1112


Now that I know why, after fixing the initial compiling errors and making the app rendering something, using configureCompat({ MODE: 3 }) and cherry-pick vue2 core features COMPONENT_V_MODEL, INSTANCE_LISTENERS, INSTANCE_ATTRS_CLASS_AND_STYLE seems a "happier migration path" to make sure internals are all in vue3.

Once again, thanks and sorry @LinusBorg!