javascriptvuejs2vuejs3

Child component emits an event I didn't ask for in Vue 3


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?


Solution

  • 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:

    The 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>
    

    SFC link

    Vue 2 only inherit attributes