Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to emit event from component inside slot #4332

Closed
purepear opened this issue Nov 28, 2016 · 18 comments

Comments

@purepear
Copy link

commented Nov 28, 2016

Vue.js version

2.1.3

Reproduction Link

https://jsfiddle.net/x8a3vvk2/8/

Events emitted from component inside slot are not received.
$emit('testevent') is not working but $parent.test() works.

<script src="https://unpkg.com/vue@2.1.3/dist/vue.js"></script>

<div id="app" >
  <parent>
    <child></child>
   </parent>
</div>
Vue.component('parent', {
  template: `<div><slot @testevent="test"></slot></div>`,
  methods: {
    test () {
      alert('Test ok')
    }
  }
})

Vue.component('child', {
  template: `<button @click="$emit('testevent')">I'm slot</button>`
  //template: `<button @click="$parent.test()">I'm slot</button>`
})

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

@purepear purepear changed the title Events emitted from slot components are not received Is it possible to emit event from slot component Nov 28, 2016

@purepear purepear changed the title Is it possible to emit event from slot component Is it possible to emit event from component inside slot Nov 28, 2016

@yyx990803

This comment has been minimized.

Copy link
Member

commented Nov 29, 2016

You cannot listen to events on <slot>. It can end up rendering anything: text, plain element, multiple nodes... the behavior will be unpredictable.

It seems you are trying to make a slot container communicate with a slot child - in most cases this means the two components are coupled by-design, so you can do something like this.$parent.$emit(...) from the child, and listen to that event in the parent with this.$on(...).

I am still open to ideas on improving the ways slot parent and child can communicate, but events on <slot> doesn't really sound like a right direction to me.

@guykatz

This comment has been minimized.

Copy link

commented Jul 31, 2017

not trying to wake this up from the dead but...
image I want a wrapper custom component that shows a loading spinner and has a slot (slot container). the spinner should be removed when an event is emitted from a component that occupies the slot (slot child).
the parent's template looks something like:

<div>
  <slot></slot>  
    <div class="spinner active"></div>
</div>

the child's code looks something like:

    mounted: function () {
      this.$emit('loadStatusChange');
     }
  },
  template:'<span>some template txt<span>'

the usage is something like this:

  <vue-loading-wrapper>
      <vue-child></vue-child>
  </vue-loading-wrapper>

in order for the slot child to fulfill the parent's interface, they should emit an event to turn the spinner to inactive. One can argue that using $parent.emit with $on (per suggestion @yyx990803 above) complicates the issue as you need to take into consideration the life cycle of components into this. if, for example:
IT the slot child emits the event on mount than the slot container should register for it in earlier phases (not mount) because the child is mounted before the parent container and so in such a case, the event will be fired but missed by the parent.
Now, if you passed the hurdle above, you find yourself in an odd situation because you want to remove the spinner on the slot container usually after it has been mounted (thats the whole point). so now you need some code trickery on the parent in order to see if the 'clear spinner' event was received AND the the parent component is mounted.

long story short, in order to get the full slot power I do believe some sort of a mechanism should be introduced.
I am no JS expect by any means, this is just from the short experience I have with vue.
your thoughts are welcomed

@Justineo

This comment has been minimized.

Copy link
Member

commented Nov 15, 2017

@guykatz

In your case:

<vue-loading-wrapper>
  <vue-child></vue-child>
</vue-loading-wrapper>

The state of vue-child should be driven by the data (data, props, computed) of the context component which holds this template. You can also bind that to vue-loading-wrapper. Even if vue-child loads data for its own, you can still do:

<vue-loading-wrapper :loading="childLoading">
  <vue-child @load="childLoading = false"></vue-child>
</vue-loading-wrapper>
@TomS-

This comment has been minimized.

Copy link

commented May 16, 2018

I'm having a lot of trouble with this. I can see that this will be a high use case, or at least I thought it would be.

Personally I have a card ui that carries a background, inside the card can be any content so it uses <slot></slot> in some cases it's just a background with text, however in others I would like to use a link that changes the background. The link is also a component. So the HTML looks like:

 <vue-card header="<?=$page->title ?>" background="<?=$page->background->size(500, 500)->url ?>">
    <?php foreach($page->link as $link): ?>
        <vue-link link="<?=$link->url ?>" @mouseover.native="background = '<?=$link->background->size(500, 500)->url ?>'"></vue-link>
    <?php endforeach; ?>
 </vue-card>

According to the documentation this should work and it would do if I could pass JSON over and make the HTML above into a component using v-for, however, because I need to access the CMS resize-image functions I can't do this. @mouseover.native targets the new Vue() instance rather than the <vue-card> component.

In this case $emit or even $parent.$emit will not work because it is firing on the new Vue() instance. So it seems impossible and I'll have to avoid using Vue for this element. I'll have to use native javascript on created I guess. But it isn't exactly the Vue way.

Edit:
Made a new component just to add native javascript on mount:

mounted: function() {
        var vm = this;

        [].forEach.call(this.$children[0].$children, function(n) {
            n.$el.addEventListener("mouseenter", function(e) {
                if(e.target.hasAttribute("data-background")) {
                    vm.activeBackground = e.target.getAttribute("data-background");
                }
            });
            n.$el.addEventListener("mouseleave", function(e) {
                vm.activeBackground = vm.background;
            });
        });
    }

It's very hacky and not "very Vue" at all... Wish there was a way to bubble up an event inside a slot...

@Danonk

This comment has been minimized.

Copy link

commented May 27, 2018

Holy shit @TomS- , such mixing php and Vue should be punishable by death! Your code is horribly unmaintainable.

@TomS-

This comment has been minimized.

Copy link

commented May 28, 2018

@Danonk I hardly think "punishable by death" is fair. Not that I have to justify myself to you, I'm using a PHP CMS that doesn't have a REST API and I want to use Vue. Luckily you don't have to deal with the code I'm writing.

@dodatrmit3618835

This comment has been minimized.

Copy link

commented May 29, 2018

I'm facing with problem with too

Currently I am creating a Modal in which will contain any Form components

My modal contain tag

<div>
   ...
   <slot></slot>
</div>

I call Modal component like below and I place LoginForm component inside Modal component

<modal>
   <loginForm/>
</modal>

Inside the LoginForm component, I place an anchor tag to close the modal

<div>
   ...
   <a @click="close">LOG IN</a>
</div>

<script>
   ...
   methods: {
      close () {
         this.$emit('close');
      }
   }
</script>

However, this button is not working. Whenever I click the button, I get this error:

Uncaught TypeError: Cannot read property '$emit' of null

Now I'm stuck at this.

I have already tried the suggestion from @Justineo but cannot make it work, any idea?

@drachum

This comment has been minimized.

Copy link

commented Jun 7, 2018

Hi!

I arrived at the exact same problem that @dodatrmit3618835, and I think that in cases like this, listening to events on slot could be a good solution.

@theianjohnson

This comment has been minimized.

Copy link

commented Jun 13, 2018

👍

I could also use this, my use case is trying to use one or more date picker components within a data table component. I'd like the date picker to be able to emit an event with their key/value pair for the data table to update itself with.

<datatable>
    <datepicker key="ordered_at" name="Ordered"></datepicker>
    <datepicker key="paid_on" name="Paid"></datepicker>
    // etc
</datatable>

Then these date pickers get slotted into their appropriate places in the data table component. If the slot can render anything why restrict event listeners?

@aggrosoft

This comment has been minimized.

Copy link

commented Jun 20, 2018

@dodatrmit3618835 I had exactly the same problem, scoped slots can you help in this case:

Modal (parent) has to get a scoped slot with a closeHandler property, this should be some kind of method to close your modal, please adapt to your needs - my usecase uses uiv for a bootstrap modal:

<template>
  <div>
    <Btn
      type="primary"
      block
      @click="open=true">{{ buttonText }}</Btn>
    <Modal
      v-model="open"
      :title="title">
      <slot
        v-if="open"
        :closeHandler="closeHandler"/>
    </Modal>
  </div>
</template>

<script>
import { Modal, Btn } from 'uiv'

export default {
  name: 'ModalButton',
  components: {
    Modal,
    Btn
  },
  props: {
    buttonText: String,
    title: String
  },
  data () {
    return {
      open: false
    }
  },
  methods: {
    closeHandler: function () {
      this.open = false
    }
  }
}
</script>

Your form (or whatever inner container) has to dispatch an event you can handle from the outside (close event in your case, mine is called productSelected) - use the component as following now:

<ModalButton button-text="My fancy button">
          <template slot-scope="{closeHandler}">
            <ProductList @productSelected="closeHandler"/>
          </template>
        </ModalButton>

This will allow you to wire up the parent/child components without any unneeded coupling.

@limistah

This comment has been minimized.

Copy link

commented Jun 25, 2018

This is my solution (similar to @aggrosoft 's):

<!-- Will be top level after the containing <div> -->
<grand-parent>
  <slot slot="parent"></slot>
</grand-parent>
<!-- Should be in a <grand-parent> -->
<parent>
  <!-- 1. Actually owns patChild method, does some magic but needs to know from a slot/child -->
  <!-- patChild (...receives data) { ...Magical!!! } -->
  <!-- 5. Receives the even through method call. -->
  <slot name="child" :patChild="pathChild">
    Expects a child
  </slot>
</parent>
<!-- Should be in a <parent> -->
<child @click="handleClick">
  <!--  2. Takes the method that needs data through props -->
  Accepts patChild as a prop (function)
  <!-- 3. Event actually happens here (a slot) -->
  <!-- 4. handleClick () { ...some magic; this.patChild(...some data)} -->
</child>

<!-- Composed structure -->
<div>
  <grand-parent>
    <parent slot="parent">
      <child slot="child" slot-scope="{pathChild}" :pathChild="pathChild"></child>
    </parent>
  </grand-parent>
</div>
@dodatrmit3618835

This comment has been minimized.

Copy link

commented Jul 3, 2018

It's really amazing that many solutions have been given by many developers

I wanna give my solution, too

Like what I mentioned above, I have several components to handle the Modal which listed below

Modal component: Modal.vue
Form with Background component: FormBackground.vue
Login Form content component: Login.vue
Navbar component: Navbar.vue - which is the highest parent

Inside Navbar component, I called Modal with "closeModal" methods and Login component

<Modal
   v-show="isModalVisible"
   @close="closeModal"
>
   <Login />
</Modal>

methods: {
   closeModal() {
      this.isModalVisible = false;
   }
}

Inside the Modal component, I have <slot> tag, which will contain anything (form, paragraph etc.) which need Modal effect. This component will listen to closeModal methods inside Login component (different from this one in Navbar component) on Created and trigger to @close inside Navbar component

<div>
  <slot></slot>
</div>

created () {
   this.$on('closeModal', function(message) {
      this.$emit('close')
   })
}

Inside the Login component, there is a Button which contains the click method to trigger the closeModal inisde Modal component

<a class="btn btn-block btn-success btn-custom" @click="$parent.$emit('closeModal')">
   LOG IN
</a>

It can be summarized like this:

Modal component listen to @click inside Login component; then, Modal component trigger to @close inside Navbar component

And this works like a charm

For the solution of @aggrosoft , I have not used the slot-scope before so I cannot say if my method is better than your method but I will try it in another Modal (or something else) then report the result here soon

@reneolivo

This comment has been minimized.

Copy link

commented Feb 16, 2019

@TomS- I also found myself in a similar situation some years ago with a different framework. I suggest you export the data that you need as JSON inside a script tag:

<script>
window.someGlobalVariable = <?php json_encode($somedata); ?>
</script>
<script src="/dist/my-nice-vue-dist.js"></script>
@jaiko86

This comment has been minimized.

Copy link

commented Apr 11, 2019

from the child, and listen to that event in the parent with this.$on(...)

Where would this.$on(...) go?

@drachum

This comment has been minimized.

Copy link

commented Apr 11, 2019

from the child, and listen to that event in the parent with this.$on(...)

Where would this.$on(...) go?

Hey @jaiko86 one way is to register this event when parent component is mounted, like:

// Parent.vue
...
mounted(){
   this.$on("child-event", this.handleChildEvent);
},
...
@florin1693

This comment has been minimized.

Copy link

commented May 9, 2019

you can use named slots and do something like:
<parent> <template v-slot="section"> <child @eventName="action"/> </template> </parent>

@kemaltatar

This comment has been minimized.

Copy link

commented Jul 31, 2019

<slot name="form"
  :saveHandler="values => handleSave(values)"
  :cancelHandler="handleCancel"
/>
<template v-slot:form="{saveHandler, cancelHandler}">
  <ExampleForm @save="saveHandler" @cancel="cancelHandler" />
</template>

Is this hacky way of doing this a really bad idea ?

@florin1693

This comment has been minimized.

Copy link

commented Jul 31, 2019

@alina-beck alina-beck referenced this issue Aug 20, 2019
2 of 2 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.