Passing events from a grandchild component up to its grandparent component in VueJS 2.0

Vue.js 2.0 appears to have a limitation where events cannot be emitted directly from a grand child component to its grand parent.

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'
})

A solution is provided in this JsFiddle https://jsfiddle.net/y5dvkqbd/4/, which involves emitting two events:

  • Emitting an event from the grand child to a middle component
  • Then emitting another event from the middle component to the grand parent

The need for this additional "middle" event may seem redundant and unnecessary. Is there a more direct way to emit an event to the grand parent that I may be overlooking?

Answer №1

In Vue version 2.4, a new feature was introduced to facilitate the passing of events up the component hierarchy using vm.$listeners

You can find more information about this feature at https://v2.vuejs.org/v2/api/#vm-listeners:

The v-on event listeners from the parent scope (without .native modifiers) can be accessed via v-on="$listeners", which is particularly useful when creating wrapper components with transparent functionality.

Take a look at the code example below that demonstrates the usage of v-on="$listeners" in the grand-child component within 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>

Answer №2

UPDATED ANSWER (November 2018)

I have recently come across a more efficient way to achieve this using the $parent property in the grandchild component:

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

This method is both cleaner and simpler.

Answer №3

When it comes to tackling this issue, the Vue community generally leans towards using Vuex. By updating the Vuex state, changes are automatically reflected in the DOM representation without the need for additional events in many scenarios.

If Vuex isn't your preferred route, re-emitting could be a viable alternative, or as a last resort, utilizing an event bus as outlined in the top-voted answer to this question.

While the following was my initial solution to the problem, with more experience in Vue, I would approach it differently now.


In instances like these, where I may diverge from Vue's design philosophy, resorting to direct manipulation of the DOM is an option.

Within the grand-child component,

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 within the parent,

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

If all else fails, resorting to re-emitting or employing an event bus could also work.

Note: I included code in the doEvent method to address issues with IE; this could potentially be refactored for better reusability.

Answer №4

A common misconception is that events in Vue only travel from child to parent and do not go any further, like from child to grandparent.

To address this issue briefly, the Vue documentation discusses the topic of Non-Parent-Child Communication in a specific section.

The suggested approach involves creating an empty Vue component in the grandparent component, which is then passed down to children and grandchildren through props. The grandparent component can then listen for events while grandchildren emit events on this designated "event bus".

In some cases, applications utilize a global event bus instead of individual event buses for each component. In such situations, it is crucial to use unique event names or namespaces to prevent conflicts between different components' events.

For those interested, here is an example demonstrating how to implement a simple global event bus: how to implement a simple global event bus.

Answer №5

To achieve flexibility and seamlessly propagate an event to all ancestors and their predecessors up to the root, you can utilize the following approach:

let component = this.$parent

while(component) {
    component.$emit('submit')
    component = component.$parent
}

Answer №6

A different approach would involve triggering the event at the root node:

Start by using vm.$root.$emit in the grand-child component, then listen for that event using vm.$root.$on in the ancestor component (or any desired location).

Update: If there are specific situations where you need to disable the listener, you can utilize vm.$off (for instance: vm.$root.off('event-name') within the beforeDestroy lifecycle hook).

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>

Answer №7

Within VueJS 2 components, there exists a $parent property that holds reference to their immediate parent component.

This parent component, in turn, possesses its own $parent property.

To access the "grandparent" component, one must navigate through the "parent's parent" component:

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

Nevertheless, this method may be considered somewhat complex, and it is advisable to utilize a global state manager such as Vuex or similar tools, as suggested by other contributors.

Answer №8

I've created a handy mixin inspired by @digout's solution. Simply place it before initializing your Vue instance (new Vue...) to utilize it across your entire project. You can easily use it just like a regular event.

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

Answer №9

Expanding on the responses from @kubaklam and @digout, here is my approach to prevent emitting on every parent component between a grandchild and a 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 creating a component with distant grandchildren and you want to avoid many components being connected to the store but still maintain the root component as a source of truth, this method works effectively. It aligns with Ember's philosophy of data down actions up. The limitation is that it won't work if you need to listen for the event on every parent in between. In such cases, you can utilize $propogateEmit as discussed in the previous response by @kubaklam.

Edit: The initial vm variable should reference the component itself rather than its parent. Meaning let vm = this, not let vm = this.parent

Answer №10

When it comes to passing data from a deep nested child component to its parent, I always resort to using an event bus.

To start off: Begin by creating a JavaScript file (I typically name it eventbus.js) containing the following code:

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

Next: Emit an event in your child component like so:

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

Then: Listen for that event in the parent component:

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

Note: To stop listening to an event, simply unregister it:

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

NOTE: You can skip the personal opinion section below

Personally, I prefer not to use Vuex for communication between grand-child and grand-parent components.

In vue.js, you can utilize provide/inject for passing data from grand-parent to grand-child components. However, there isn't a similar built-in method for the opposite direction (grand-child to grand-parent). This is why I rely on event bus for such scenarios.

Answer №11

Expanding on @digout's response, I believe that if the intention is to transmit data to a distant ancestor, using $emit may not be necessary. I experimented with this in a specific scenario and found success without it. While this functionality could potentially be achieved through a mixin, it is not a requirement.

/**
 * Send some content as a "message" to a named ancestor of the component calling this method.
 * This is an edge-case method where you need to send a message many levels above the calling component.
 * Your target component must have a receiveFromDescendant(content) method and it decides what
 * to do with the content it gets.
 * @param {string} name - the name of the Vue component eg name: 'myComponentName'
 * @param {object} content - the message content
 */
messageNamedAncestor: function (name, content) {
  let vm = this.$parent
  let found = false
  while (vm && !found) {
    if (vm.$vnode.tag.indexOf('-' + name) > -1) {
      if (vm.receiveFromDescendant) {
        found = true
        vm.receiveFromDescendant(content)
      } else {
        throw new Error(`Found the target component named ${name} but you dont have a receiveFromDescendant method there.`)
      }
    } else {
      vm = vm.$parent
    }
  }
}

If dealing with an ancestor:

export default {
  name: 'myGreatAncestor',
  ...
  methods: {
     receiveFromDescendant (content) {
        console.log(content)
     }
   }
}

A great grand-child expresses

// Communicate essential information to the ancestor component
this.messageNamedAncestor('myGreatAncestor', {
  importantInformation: 'Hello from your great descendant'
})

Answer №12

Vue 3 has ushered in a series of significant changes to root events:

The traditional $on, $off, and $once root methods no longer exist. However, there are alternatives available for listening to root events by following this approach: guide to root component events.

createApp(App, {
  // Listen for the 'expand' event
  onExpand() {
    console.log('expand')
  }
})

Event buses offer another solution, although Vue.js documentation cautions against them due to potential maintenance complications in the long term. Examples of event bus implementations mentioned include mitt and tiny-emitter.

The preferred approach according to the docs is as follows:

  • Props: Ideal for parent-child communication.
  • Provide/Inject: Ancestor-descendant communication method (not vice versa).
  • Vuex: Recommended for managing global state, primarily intended for state management rather than event handling.

Ultimately, the decision between using an event bus or Vuex depends on the specific requirements. Integrating the event bus within Vuex can help centralize its functionality if global state access is also required. Otherwise, strictly controlling the behavior and placement of the event bus could be beneficial.

Answer №13

The method of creating a class that is linked to the window is quite impressive, especially in simplifying the broadcast/listen process within a 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);  
    }

}

With this setup, you can easily trigger actions from any part of the codebase:

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

Furthermore, you can set up listeners in various components by using:

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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

When using jsPlumb's beforeDrop function alongside ngToast, the message may not display immediately

I have been working on a project where I am integrating the jsPlumb library to create a node-based interface. jsPlumb provides an event called 'beforeDrop' which is triggered before connecting two endpoints. I plan to use this event to check a c ...

Unusual behavior being exhibited by the background in an HTML5 canvas

I have an 800 by 100 image for the background and I'm trying to extract sections from it (like a sprite sheet) in order to create a background. I believe this method is efficient and allows me to experiment and learn how to generate backgrounds in the ...

Ways to modify the pre-defined value in a TextField

I encountered a situation where I have a form with pre-filled data from a specific country. The data is initially read-only, but the user can click on the edit button if they wish to make changes. The issue arises when trying to edit the value in the Text ...

Focus on selecting just one button within Angular

By retrieving values from a database and displaying them using ng-repeat within a div: <div ng-controller = "myTest"> <div ng-repeat="name in names"> <h4>{{name.name}}</h4> <button ng-class="{'active ...

Ways to detach event listener in Vue Component

One of my Vue2 components has a custom eventListener that I added in the mounted lifecycle hook. I am now trying to figure out the correct way to remove this listener when the component is destroyed. <template> <div> ... </di ...

Steps to display a div element periodically at set time intervals

I've created a user greeting message that changes based on the time of day - saying Good Morning, Good Afternoon, or Good Evening. It's working well, but I'm wondering how I can make the message hide after it shows once until the next part o ...

Adjusting the zoom level in leaflet.js ImageOverlay will cause the marker

Using ImageOverlay to display an image as a map with Leaflet.js, but encountering issues with marker positions shifting when changing the zoom level. Followed instructions from this tutorial, and you can find a code pen example here. // Code for markers ...

How can images from a dynamic table be converted to base64 format in Vue.js and then sent in a JSON file?

Hello everyone, I'm facing an issue with a dynamic table where I need to add multiple rows, each containing a different image. I attempted to convert these images to base64 format and save them in a JSON file along with the table data. However, the pr ...

Understanding Vuex: When to Utilize an Action Versus Managing State within a Component

I am puzzled as to why actions in Vuex cannot be simply managed within a component. Let's say I have a basic store: store.js const initialState = () => ({ overlayText: '' }) const mutations = { setOverlayText: (state, payload ...

Obtain information stored locally when an internet connection is established

I'm currently facing an issue with my Local Storage data retrieval process in Vuejs while being online. My ToDo app setup consists of Vuejs, Laravel, and MySQL. When the internet connection is available, I store data in localStorage. The ToDo app has ...

Accessing dynamically created AJAX controls in ASP.NET during postback operations

I am dynamically creating 2 dropdown boxes and a CheckBoxList control using AJAX callbacks to a web service (.asmx file). The server-side service generates the Dropdowns and CheckBoxList, returning the rendered html as a string which is then inserted into ...

Align the center of table headers in JavaScript

I'm currently in the process of creating a table with the following code snippet: const table = document.createElement('table'); table.style.textAlign = 'center'; table.setAttribute('border', '2'); const thead ...

The AWS Cognito User Interface utilizes a hash in order to incorporate parameters during its invocation of the callback page

I'm encountering an issue with the AWS Cognito provided UI interface. When attempting to use the provided UI, I make a call to the endpoint using the populated URL: The issue arises after authentication when Cognito utilizes a # to return the requir ...

Can someone help me uncover the previous URL for login using just JavaScript? I've tried using document.referrer but it's not giving me the

Currently, I am utilizing express with pug templates and pure JavaScript. In order to enhance the user experience of my log in system, I would like to save the URL that someone came to the login page with, so that I can redirect them back to it once they h ...

Setting up ckeditor5 in Nuxt the right way

Recently, I encountered the CkEditor5 setting in Nuxt js for the first time and found it to be quite confusing with plenty of errors surfacing. I attempted to customize my version of ckeditor5 in nuxt.js by following this guide However, none of it seems ...

How can I locate and substitute a specific script line in the header using Jquery?

I need to update an outdated version of JQuery that I have no control over, as it's managed externally through a CMS and I can't make changes to it. But I want to switch to a newer version of JQuery. Here is the old code: <script type="text/ ...

Unlock the innerHTML of a DIV by clicking a button with ng-click in Angular JS

I am curious about something. There is a <div> and a <button> in my code. I am looking to access the inner markup of the <div> within the ng-click function without relying on jQuery. Can you give me some guidance? <div id = text-entry ...

What factors outside of the program could potentially lead to the splice function not functioning as expected?

Within my code, I encountered a peculiar issue involving a splice line along with debug lines on both sides. Take a look: const obj = { x:[1,2], y:{t:"!!!"} } const lenBefore = S.groupings.length const ret = S.groupings.splice(gix, 0, obj) console.log(`${ ...

What is the best way to access data from outside a forEach loop in JavaScript?

I am trying to access the value of my uid outside of the foreach loop in my code. Can anyone assist me with this? This is the code I am working with: var conf_url = "https://192.168.236.33/confbridge_participants/conference_participants.json?cid=009000 ...

Animations experiencing delays on mobile Chrome

I am currently exploring the world of website animations. I have a version of the jsfiddle code linked below that is similar to what I am working on. The animations function perfectly when viewed on desktop, but when accessed on mobile - specifically on my ...