I'm in the process of migrating my medium-sized app from Vue2 to Vue3. I noticed that Vue3 Child-Parent emit mechanism is different compared to Vue2 and confuses me a lot.
Here's a basic example for demonstration
// App.vue
<script>
import InputWrap from './InputWrap.vue';
export default {
name: 'App',
components: {
InputWrap,
},
data() {
return {
msg: 'Hello world',
}
},
methods: {
onChange() {
console.log('why change?');
}
}
}
</script>
<template>
<h1>{{ msg }}</h1>
<input-wrap v-model="msg" @change="onChange" />
</template>
// InputWrap.vue
<script>
import MyInput from './MyInput.vue';
export default {
name: 'InputWrap',
components: {
MyInput,
},
props: ['modelValue'],
methods: {
onUpdate(v) {
this.$emit('update:modelValue', v);
}
}
}
</script>
<template>
<my-input :modelValue="modelValue" @update:modelValue="onUpdate" />
</template>
// MyInput.vue
<script>
export default {
name: 'MyInput',
props: {
modelValue: String,
},
methods: {
onInput(e) {
this.$emit('update:modelValue', e.target.value);
}
}
}
</script>
<template>
<input :value="modelValue" @input="onInput" />
</template>
Link to Vue SFC playground.
I'm curious why does onChange
event handler is called in App.vue
?
It seems that change
event is generated by input element in MyInput.vue
. But it's totally not transparent why this event is going up through all components and being captured in App.vue
. Imagine a tree with dozens nested components and a root component listening for change
event. It's a total mess.
Vue2 have a different approach and I like it because it has a transparent child-parent communication. Is it possible to turn on Vue2 emit mechanism in Vue3?
The reason is that Vue implements Fallthrough Attributes mechanism
A "fallthrough attribute" is an attribute or
v-on
event listener that is passed to a component, but is not explicitly declared in the receiving component's props or emits.When a component renders a single root element, fallthrough attributes will be automatically added to the root element's attributes
In your example, your component meets 2 conditions:
InputWrap.vue
render a single root element (MyInput.vue
components)InputWrap.vue
does not declare onChange
as the component's emitsThe same with MyInput.vue
component
So the @change
listener will fall through to the input element.
To disable fallthrough attributes you can declare the attributes as the props/emits
of the child component or disable the whole feature for that component by adding inheritAttrs: false
:
// InputWrap.vue
<script>
import MyInput from './MyInput.vue';
export default {
name: 'InputWrap',
components: {
MyInput,
},
inheritAttrs: false
}
</script>