116
votes

It seems that Vue.js 2.0 doesn't emit events from a grand child to his grand parent component.

Vue.component('parent', {
  template: '<div>I am the parent - {{ action }} <child @eventtriggered="performAction"></child></div>',
  data(){
    return {
      action: 'No action'
    }
  },
  methods: {
    performAction() { this.action = 'actionDone' }
  }
})

Vue.component('child', {
  template: '<div>I am the child <grand-child></grand-child></div>'
})

Vue.component('grand-child', {
  template: '<div>I am the grand-child <button @click="doEvent">Do Event</button></div>',
  methods: {
    doEvent() { this.$emit('eventtriggered') }
  }
})

new Vue({
  el: '#app'
})

This JsFiddle solves the issue https://jsfiddle.net/y5dvkqbd/4/ , but by emtting two events:

  • One from grand child to middle component
  • Then emitting again from middle component to grand parent

Adding this middle event seems repetitive and unneccessary. Is there a way to emit directly to grand parent that I am not aware of?

11

11 Answers

98
votes

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

From https://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>
51
votes

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.

46
votes

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.

26
votes

Yes, you're correct events only go from child to parent. They don't go further, e.g. from child to grandparent.

The Vue documentation (briefly) addresses this situation in the Non Parent-Child Communication section.

The general idea is that in the grandparent component you create an empty Vue component that is passed from grandparent down to the children and grandchildren via props. The grandparent then listens for events and grandchildren emit events on that "event bus".

Some applications use a global event bus instead of a per-component event bus. Using a global event bus means you will need to have unique event names or namespacing so events don't clash between different components.

Here is an example of how to implement a simple global event bus.

19
votes

If you want to be flexible and simply broadcast an event to all parents and their parents recursively up to the root, you could do something like:

let vm = this.$parent

while(vm) {
    vm.$emit('submit')
    vm = vm.$parent
}
18
votes

Another solution will be on/emit at root node:

Uses vm.$root.$emit in grand-child, then uses vm.$root.$on at the ancestor (or anywhere you'd like).

Updated: sometimes you'd like to disable the listener at some specific situations, use vm.$off (for example: vm.$root.off('event-name') inside lifecycle hook=beforeDestroy).

Vue.component('parent', {
  template: '<div><button @click="toggleEventListener()">Listener is {{eventEnable ? "On" : "Off"}}</button>I am the parent - {{ action }} <child @eventtriggered="performAction"></child></div>',
  data(){
    return {
      action: 1,
      eventEnable: false
    }
  },
  created: function () {
    this.addEventListener()
  },
  beforeDestroy: function () {
    this.removeEventListener()
  },
  methods: {
    performAction() { this.action += 1 },
    toggleEventListener: function () {
      if (this.eventEnable) {
        this.removeEventListener()
      } else {
        this.addEventListener()
      }
    },
    addEventListener: function () {
      this.$root.$on('eventtriggered1', () => {
        this.performAction()
      })
      this.eventEnable = true
    },
    removeEventListener: function () {
      this.$root.$off('eventtriggered1')
      this.eventEnable = false
    }
  }
})

Vue.component('child', {
  template: '<div>I am the child <grand-child @eventtriggered="doEvent"></grand-child></div>',
  methods: {
    doEvent() { 
    	//this.$emit('eventtriggered') 
    }
  }
})

Vue.component('grand-child', {
  template: '<div>I am the grand-child <button @click="doEvent">Emit Event</button></div>',
  methods: {
    doEvent() { this.$root.$emit('eventtriggered1') }
  }
})

new Vue({
  el: '#app'
})
<script src="https://unpkg.com/vue/dist/vue.js"></script>

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

This is the only case when I use event bus!! For passing data from deep nested child, to not directly parent, communication.

First: Create a js file (I name it eventbus.js) with this content:

import Vue from 'vue'    
Vue.prototype.$event = new Vue()

Second: In your child component emit an event:

this.$event.$emit('event_name', 'data to pass')

Third: In the parent listen to that event:

this.$event.$on('event_name', (data) => {
  console.log(data)
})

Note: If you don't want that event anymore please unregister it:

this.$event.$off('event_name')

INFO: No need to read the below personal opinion

I don't like to use vuex for grand-child to grand-parent communication (Or similar communication level).

In vue.js for passing data from grand-parent to grand-child you can use provide/inject. But there is not something similar for the opposite thing. (grand-child to grand-parent) So I use event bus whenever I have to do that kind of communication.

3
votes

VueJS 2 components have a $parent property that contains their parent component.

That parent component also includes its own $parent property.

Then, accessing the "grandparent" component it's a matter of accessing the "parent's parent" component:

this.$parent["$parent"].$emit("myevent", { data: 123 });

Anyway, this is kinda tricky, and I recommend using a global state manager like Vuex or similar tools, as other responders have said.

2
votes

I've made a short mixin based on @digout answer. You want to put it, before your Vue instance initialization (new Vue...) to use it globally in project. You can use it similarly to normal event.

Vue.mixin({
  methods: {
    $propagatedEmit: function (event, payload) {
      let vm = this.$parent;
      while (vm) {
        vm.$emit(event, payload);
        vm = vm.$parent;
      }
    }
  }
})
1
votes

Riffing off @kubaklam and @digout's answers, this is what I use to avoid emitting on every parent component between the grand-child and the (possibly distant) grandparent:

{
  methods: {
    tunnelEmit (event, ...payload) {
      let vm = this
      while (vm && !vm.$listeners[event]) {
        vm = vm.$parent
      }
      if (!vm) return console.error(`no target listener for event "${event}"`)
      vm.$emit(event, ...payload)
    }
  }
}

When building out a component with distant grand children where you don't want many/any components to be tied to the store, yet want the root component to act as a store/source of truth, this works quite well. This is similar to the data down actions up philosophy of Ember. Downside is that if you want to listen for that event on every parent in between, then this won't work. But then you can use $propogateEmit as in above answer by @kubaklam.

Edit: initial vm should be set to the component, and not the component's parent. I.e. let vm = this and not let vm = this.$parent

0
votes

I really dig the way this is handled by creating a class that is bound to the window and simplifying the broadcast/listen setup to work wherever you are in the Vue app.

window.Event = new class {

    constructor() {
        this.vue = new Vue();
    }

    fire(event, data = null) {
        this.vue.$emit(event, data);
    }

    listen() {
        this.vue.$on(event, callback);  
    }

}

Now you can just fire / broadcast / whatever from anywhere by calling:

Event.fire('do-the-thing');

...and you can listen in a parent, grandparent, whatever you want by calling:

Event.listen('do-the-thing', () => {
    alert('Doing the thing!');
});