[@vue/compat] v-model unwrapping + $attrs forwarding does not match "real" vue@3 behaviour
Vue version
3.2.45
Link to minimal reproduction
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
.
- clone the
repro
https://github.com/renatodeleao/playgrounds - switch to the branch
vue-compat-component-is-behaviour-is-different-from-regular-vue
cd
into thevue-2-3-migration-build
folderyarn install
yarn dev
to run the demo- Type any text into the input box
What is expected?
- The
msg
binding should reflect the value of the input
What is actually happening?
- 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 |
---|---|
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
I'll summarize a few things, some of which you might be aware, just for clarity:
There are two levels of compat:
- compiler
- runtime
Consequently, there are 2 places that require compat config:
- compiler config (you set that to 3)
- 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 theAppInputField
.
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,
})
@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 :/
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.
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!