VueJs 2.0 emit event from grand child to his grand parent component

NEW ANSWER (Nov-2018 update)

I discovered that we could actually do this by leveraging the $parent property in the grand child component:

this.$parent.$emit("submit", {somekey: somevalue})

Much cleaner and simpler.


The Vue community generally favors using Vuex to solve this kind of issue. Changes are made to Vuex state and the DOM representation just flows from that, eliminating the need for events in many cases.

Barring that, re-emitting would probably be the next best choice, and lastly you might choose to use an event bus as detailed in the other highly voted answer to this question.

The answer below is my original answer to this question and is not an approach I would take now, having more experience with Vue.


This is a case where I might disagree with Vue's design choice and resort to DOM.

In grand-child,

methods: {
    doEvent() { 
        try {
            this.$el.dispatchEvent(new Event("eventtriggered"));
        } catch (e) {
            // handle IE not supporting Event constructor
            var evt = document.createEvent("Event");
            evt.initEvent("eventtriggered", true, false);
            this.$el.dispatchEvent(evt);
        }
    }
}

and in parent,

mounted(){
    this.$el.addEventListener("eventtriggered", () => this.performAction())
}

Otherwise, yes, you have to re-emit, or use a bus.

Note: I added code in the doEvent method to handle IE; that code could be extracted in a reusable way.


Vue 2.4 introduced a way to easily pass events up the hierarchy using vm.$listeners

From https://v2.vuejs.org/v2/api/#vm-listeners :

Contains parent-scope v-on event listeners (without .native modifiers). This can be passed down to an inner component via v-on="$listeners" - useful when creating transparent wrapper components.

See the snippet below using v-on="$listeners" in the grand-child component in the child template:

Vue.component('parent', {
  template:
    '<div>' +
      '<p>I am the parent. The value is {{displayValue}}.</p>' +
      '<child @toggle-value="toggleValue"></child>' +
    '</div>',
  data() {
    return {
      value: false
    }
  },
  methods: {
    toggleValue() { this.value = !this.value }
  },
  computed: {
    displayValue() {
      return (this.value ? "ON" : "OFF")
    }
  }
})

Vue.component('child', {
  template:
    '<div class="child">' +
      '<p>I am the child. I\'m just a wrapper providing some UI.</p>' +
      '<grand-child v-on="$listeners"></grand-child>' +
    '</div>'
})

Vue.component('grand-child', {
  template:
    '<div class="child">' +
      '<p>I am the grand-child: ' +
        '<button @click="emitToggleEvent">Toggle the value</button>' +
      '</p>' +
    '</div>',
  methods: {
    emitToggleEvent() { this.$emit('toggle-value') }
  }
})

new Vue({
  el: '#app'
})
.child {
  padding: 10px;
  border: 1px solid #ddd;
  background: #f0f0f0
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <parent></parent>
</div>