24

Is there a way to allow events to bubble up when using a component within a component?

My application is a dynamic menu. The dynamic menu is a component (dyn-menu) and it uses a local component (menu-item) for each of the <li> elements. Each <menu-item> has a click handler associated with it that emits a custom event (with an ID for menu item in the full implementation). But the application doesn't see the events issued by <menu-item> because they are not bubbled up.

Is there a way to allow the <menu-item> component, which is local to the <dyn-menu> component, emit the event and still allow vapp to see and handle the event?

I'm pretty new to Vuejs so I might be missing something obvious. And it's possible that I'm trying to solve this by using two components and that's not the best way to handle it. Is there is a better way to approach it?

Here's a jsfiddle. You have to remove the @dyn-menu-item-click='itemClick' line in the <dyn-menu> template to illustrate that the event doesn't bubble up if the component doesn't handle the event. If that line is removed then <dyn-menu> doesn't handle the event but vapp never sees the event either.

5
  • Can you explain it a bit more, your js-fiddle seems to bubble the events up. Commented Feb 2, 2017 at 6:26
  • Yes, I manually bubble the events up. I'm wondering if there is a way to have the events bubble up naturally, e.g., so the <menu-item> can issue the event and the app capture it BUT without having to manually propagate the event in the <dyn-menu> component. (If you take out the line in the template `@dyn-menu-item-click='itemClick' then the event isn't propagated and doesn't bubble up.) Commented Feb 2, 2017 at 7:10
  • Incorporated previous comment into question. Commented Feb 2, 2017 at 13:20
  • 1
    I could use this too. I did find out you can bubble events like this: <log-out-button v-on:logged-out="$emit('logged-out')"></log-out-button> Commented Feb 24, 2017 at 10:31
  • I don't think bubbling will be a feature of Vue custom events, because, they're custom. Bubbling happens in DOM events as per their definition. Since custom events are up to the implementer, it is up to you to put the code the bubble them. The comment by @robbeclerckx could do your trick. @mycustomevent="$emit('mycustomevent')" Commented Sep 25, 2017 at 14:46

2 Answers 2

28

There are 4 options I know of

  1. Re-emit events like you did
  2. Use this.$parent (repetitively) on the child component to access the desired parent and emit the event. (see "Implement your own bubbling event plugin" below)
  3. Use an event bus that is provided by the parent and injected in the children.
  4. Use a Vuex store and push events to an event queue in the child component. Somewhere else in the app, watch that reactive event queue for new elements or just bind it to something.

Implement your own bubbling event plugin

It's very simple. The plugin adds a new $bubble method that emits events that bubble to their parents. I considered publishing a plugin that does this, but it's so simple that the overhead is not worth it.

// Add this as a Vue plugin
Vue.use((Vue) => {
  Vue.prototype.$bubble = function $bubble(eventName, ...args) {
    // Emit the event on all parent components
    let component = this;
    do {
      component.$emit(eventName, ...args);
      component = component.$parent;
    } while (component);
  };
});

// Some nested components as an example

// note usage of "$bubble" instead of "$emit"
Vue.component('component-c', {
  template: `
    <button type="button" @click="$bubble('my-event', 'payload')">
      Emit bubbling event
    </button>`,
});

Vue.component('component-b', {
  template: `<component-c @my-event="onMyEvent" />`,
  
  methods: {
    onMyEvent(...args) {
      console.log('component-b listener: ', ...args);
    },
  },
});

Vue.component('component-a', {
  template: `<component-b @my-event="onMyEvent" />`,
  
  methods: {
    onMyEvent(...args) {
      console.log('component-a listener: ', ...args);
    },
  },
});

var vapp = new Vue({
  el: '#app',

  methods: {
    onMyEvent(...args) {
      console.log('root listener: ', ...args);
    },
  },
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <component-a @my-event="onMyEvent" />
</div>

Event bus

The event bus looks like this:

Vue.component('dyn-menu', {
  components: {
    'menu-item': {
      template: '<li @click="itemClick">{{item.text}}</li>',
      props: ['item'],
      inject: ['eventBus'], // <-- Inject in the child
      methods: {
        itemClick() {
          // Emit the event on the event bus
          this.eventBus.$emit('dyn-menu-item-click', ['menu-item dyn-menu-item-click']);
        }
      }
    }
  },

  // ...
});

var vapp = new Vue({
  el: '#app',
  data: {
    // ...
    eventBus: new Vue(),
  },
  provide() {
    return {
      // The parent component provides the event bus to its children
      eventBus: this.eventBus,
    };
  },

  created() {
    // Listen to events on the event bus
    this.eventBus.$on('dyn-menu-item-click', this.menuClick);
  },
  methods: {
    menuClick(message) {}
  }
})

Working example: https://jsfiddle.net/7vwfx52b/

There are plenty of event bus plugins listed here: https://github.com/vuejs/awesome-vue#custom-events

Sign up to request clarification or add additional context in comments.

3 Comments

I guess you've answered the question. It's "no", there isn't a way to do what I asked. Nice range of options though.
It's not built-in per se, but it can be bolted on to your app in a couple lines of code. I added another example.
Wonder if there's a way to create that plugin in Vue 3. Would be lovely to have a custom directive to do <button v-bubble @click="handler">
8

As of Vue 2.4, components can access their parent's listeners through the $listeners property. You can set a component to pass through its parent's listeners to particular children by adding an attribute v-on="$listeners" to the tags for those child elements. See the docs at https://v2.vuejs.org/v2/api/#vm-listeners.

You can also forward specific events with an attribute like: @dyn-menu-item-click=$listeners['dyn-menu-item-click'].

It's still not true bubbling, but a less verbose way to re-emit events.

1 Comment

$listeners is now deprecated in Vue 3

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.