How to bubble events on a component subcomponent c

2020-06-01 05:22发布

I have my vue application using:

component-parent component that is made of component-child

inside component-parent I have buttons, when someone click a button I want to emit an event in order to be handled by vue and passed to another component

What I did so far:

var vm = new Vue({
    el: '#app',
    methods: {
        itemSelectedListener: function(item){
            console.log('itemSelectedListener', item);
        }
    }
});

Vue.component('component-child', {
    template: ' <span  v-on:click="chooseItem(pty )" >Button  </span>',
    methods: {
        chooseItem: function(pty){
            console.log(pty);
            this.$emit('itemSelected', {
                'priority' : pty
            });
        }
    }
});

Vue.component('component-parent', {
    template: '<component-child  v-for="q in items" ></component-child>'
});

HTML:

<component-parent v-on:itemSelected="itemSelectedListener"  ></component-parent>

It reaches my console.log(pty); line but it seems that this.$emit('itemSelected' wont get through:

console.log('itemSelectedListener', item); // this is not going to be called...

an hint?

should I bubble up the event from child->parent->Vue-instance? ( I also tried that but with no success)

6条回答
Summer. ? 凉城
2楼-- · 2020-06-01 05:48

Create a custome directive for bubbling and use it in the child component.

Sample directive:

// Add this to main.ts when initializing Vue
Vue.directive('bubble', {
  bind(el, { arg }, {context, componentInstance}) {
    if (!componentInstance || !context || !arg) {
      return;
    }

    // bubble the event to the parent
    componentInstance.$on(v, context.$emit.bind(context, arg));
  }
});

Child component can use that directive to emit through the parent component (i switched to kabob case and shorthand for event).

<!-- template for component-parent -->
<component-child v-bubble:item-selected  v-for="q in items"></component-child>

<!-- usage of component-parent -->
<component-parent @:item-selected="itemSelectedListener"></component-parent>

Including my full bind directive below. It allows bubbling multiple events.

<!--------------------
 * A few examples 
--------------------->
<!-- bubble single event -->
<child v-bubble:click/>
<child v-bubble:_.click/>
<child v-bubble="'click'"/>
<child v-bubble:any-costume-event-will-work/>

<!-- bubble: click, focus, blur -->
<child v-bubble:_.click.focus.blur/> 
<child v-bubble="'click, focus, blur'"/>

<!-- prefixed bubbling: click, focus, blur as child-click, child-focus, child-blur -->
<child v-bubble:child.click.focus.blur/> 
<child v-bubble:child="'click, focus, blur'"/>
Vue.directive('bubble', {
        bind(el, { value, arg: prefix = '', modifiers }, {context, componentInstance}) {
  const events = value && value.trim() ? value.split(',') : Object.keys(modifiers);
    if (!events.length && prefix) {
      events.push(prefix);
      prefix = '';
    } else if(prefix) {
      prefix = prefix === '_' ? '' : prefix += '-';
    }

    if (!componentInstance || !context || !events.length) {
      return;
    }

    events.forEach((v: string) => {
      v = v.trim();
      const eventName = `${prefix}${v}`;
      const bubble = context.$emit.bind(context, eventName);
      componentInstance.$on(v, bubble);
    });
  }
});
查看更多
做个烂人
3楼-- · 2020-06-01 05:58

I'm a little surprised nobody suggested using an event bus component. It is a fairly common pattern in heavily decoupled systems to have a shared event bus, and then use it to link multiple disconnected components together pub/sub style.

//eventbus.js
import Vue from 'vue'

export const EventBus = new Vue()

Once you have one, it's simple to publish events from any place

// component1.js
import { EventBus } from '@/services/eventbus'
...
EventBus.$emit('do-the-things')

And listen to them from somewhere else.

// component2.js
import { EventBus } from '@/services/eventbus'
...
EventBus.$on('do-the-things', this.doAllTheThings)

Note that the two components do not know anything about each other, and they don't really need to care how or why the event was raised.

There are some potentially undesirable side effects of this approach. Event names do need to be globally unique so that you don't confuse your app. You'll also potentially be channeling every single event through a single object unless you do something more sophisticated. You can do the cost / benefit analysis on your own app source to see if it's right for you.

查看更多
放我归山
4楼-- · 2020-06-01 06:04

In your child component, simply use $emit to send an event to the $root like this:

v-on:click="$root.$emit('hamburger-click')"

Then, in your parent component (eg: "App"), setup the listener in the Vue mounted lifecycle hook like this:

  export default {
    <snip...>
    mounted: function() {
      this.$root.$on('hamburger-click', function() {
        console.log(`Hamburger clicked!`);
      });
    }
  }
查看更多
聊天终结者
5楼-- · 2020-06-01 06:05

There is one issue with your component-parent template as it tries to render multiple child components. Vue usually requires a single root div inside components therefore you need to wrap it in a div or other tag.

<div>
    <component-child  v-for="q in items"></component-child>
</div>

A second thing to point out is that you emit an event from a child component which is 2 levels down and you listen to it in the root.

Root //but you listen to the event up here 1 level above
 Component 1 //you should listen to the event here
  Component 2 //your try to emit it from here

You have 2 options here. Either emit from component-child listen to that even in component-parent then propagate that even upwards. Fiddle https://jsfiddle.net/bjqwh74t/29/

The second option would be to register a global so called bus which is an empty vue instance that you can use for such cases when you want communication between non child-parent components. Fiddle https://jsfiddle.net/bjqwh74t/30/

Usually between parent and child components you use the events directly by emitting from child and listening in parent with v-on:event-name="handler" but for cases where you have more levels between components you use the second approach.

Doc link for the first case: https://vuejs.org/v2/guide/components.html#Using-v-on-with-Custom-Events

Doc link for the second case: https://vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication

PS: prefer using kebab-case for event names which means you write with - instead of capital letters. Writing with capital letters can result in weird situations where your event is not caught in the root.

查看更多
Lonely孤独者°
6楼-- · 2020-06-01 06:08

It's a little bit late but here's how I did it:

component-child:

this.$root.$emit('foobar',{...});

component-parent:

this.$root.$on('foobar')
查看更多
我命由我不由天
7楼-- · 2020-06-01 06:10

For what it's worth you can use the browser's event API. It requires a little more scripting than Vue's built-in stuff, but it also gets you around these bubbling issues (and is about the same amount of code as creating a "bus", as in the accepted answer).

On child component:

this.$el.dispatchEvent(new CustomEvent('itemSelected', { detail: { 'priority' : pty }, bubbles: true, composed: true });

On parent component, in mounted lifecycle part:

mounted() {
    this.$el.addListener('itemSelected', e => console.log('itemSelectedListener', e.detail));
}
查看更多
登录 后发表回答