Vuejs - bubbling custom events

2020-03-13 04:52发布

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.

2条回答
虎瘦雄心在
2楼-- · 2020-03-13 05:23

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://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.

查看更多
来,给爷笑一个
3楼-- · 2020-03-13 05:43

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

查看更多
登录 后发表回答