Vue.js
Emitting and receiving events within slots
10
230
Solved

Haz

Started this discussion 1 year ago

Hello,

I have a component called AccountPanel. It's a simple component with no real logic. The component has two slots. Inside of the main slot, I slot in a component. In the named slot (footer), I slot in a button (later to be a component as well). Pretty simple stuff. Here's some code.

<AccountPanel
    panelDescription="Update your personal information."
    panelId="personalInformation"
    panelTitle="Personal Information"
>
    <PersonalInformation @update-personal-information="alert('update')" />

    <template v-slot:footer>
        <button class="inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-black rounded-md border border-transparent shadow-sm hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500" type="button" @click="alert('emit the event'); $emit('updatePersonalInformation')">Save</button>
    </template>
</AccountPanel>
  • The event is not being received. ❌
  • The button is firing the event. ✅
  • There are no errors in the console. 😎
  • I am using the Composition API. 🔥

👀 From the docs, it looks like I'm doing what is shown, although my issue might be related to way I am using it with slots.

https://vuejs.org/guide/components/events.html#emitting-and-listening-to-events ↗️

Originally the @update-personal-information was inside of the component, but I moved it here as a test. I assume either is possible.

Any help is greatly appreciated! 🙌

Haz

Replied 1 year ago

Update:

It doesn't make a difference if I do it outside of the component slot. Even a dumbed down version with two elements doesn't work.

alex

Replied 1 year ago

What exactly are you trying to do here? I assume PersonalInformation is a form and you're using the button within the footer slot to submit this? You then want to fire an event that gets picked up to say the form has been submitted?

Haz

Replied 1 year ago

Yeah, pretty much. The logic is in PersonalInformation component. This is the form. I need to fire it when clicking the button in the footer slot. I can't get any event emitting to work though. It may seem silly but some other components follow this way for good reason. Like the Addresses component. It contains logic for Google Places API, adding, updating, as well as the markup. I would never want a button for saving as it isn't relevant on say the checkout page, but it is for account addresses. Having one component makes more sense to me.

alex

Replied 1 year ago

Gotcha. Not disputing the way you've done it, and that makes sense now. Working on a solution for you now!

Haz

Replied 1 year ago

I'm open to a better approach! This one just made more sense to me. Though it does seem silly for PersonalInformation right now, but later down the line, that component could also be used elsewhere. I'm all ears!

Best answer

alex

Replied 1 year ago

Ok, so I don't think emitting events is the right way to go here, since you have a child component looking to receive an event to handle. Events should be submitted upwards to parents.

Here's a better solution that I hope will work for you.

I guess your AccountPanel looks like this.

<template>
    <slot />
    <slot name="footer" />
</template>

You could use your AccountPanel component the same way, but instead add a ref to the PersonalInformation form to call a method within that child component.

<template>
  <AccountPanel>
    <PersonalInformation ref="form" />

    <template v-slot:footer>
      <button v-on:click="form.submit()">Click me</button>
    </template>
  </AccountPanel>
</template>

<script setup>
    import { ref } from 'vue'
    import AccountPanel from './components/AccountPanel.vue'
    import PersonalInformation from './components/PersonalInformation.vue'

    const form = ref(null)
</script>

In your PersonalInformation component, use defineExpose to expose the submit method.

<template>
    <form action=""></form>
</template>

<script setup>
    const submit = () => {
        console.log('submit the form')
    }

    defineExpose({ submit })
</script>

Let me know if that makes sense!

Haz

Replied 1 year ago

Hey,

Thanks for that. Very close, but not quite. I am getting a new error now (see below).

Notable changes:

  • The form ref is now personalInformationForm
  • The submit method is now updatePersonalInformation
    const updatePersonalInformation = () => {
        Inertia.patch...etc...
defineExpose({
    updatePersonalInformation,
});
const personalInformationForm = ref(null);
<PersonalInformation ref="personalInformationForm" />

The form:

<form class="grid grid-cols-6 gap-6" method="POST" v-on:submit.prevent="updatePersonalInformation">
    Input fields... No submit button.
</form>

The submit button (footer slot):

<button class="inline-flex justify-center px-4 py-2 text-sm font-medium text-white bg-black rounded-md border border-transparent shadow-sm hover:bg-black focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500" type="submit" v-on:click="personalInformationForm.submit()">Save</button>

I have tested using the original names you supplied as well, it made no difference. The error:

TypeError: $setup.personalInformationForm.submit is not a function

alex

Replied 1 year ago

The problem here is that you're using an on-submit handler on the form when this isn't necessary.

Instead of calling personalInformationForm.submit() you need personalInformationForm. updatePersonalInformation() (the method you're exposing) and to remove the event handler from your form.

An alternative approach would be to do this (I've not tested this code locally):

<template>
  <AccountPanel>
    <PersonalInformation />

    <template v-slot:footer>
      <button type="submit" form="personal-information-form">Click me</button>
    </template>
  </AccountPanel>
</template>

And in PersonalInformation:

<template>
    <form action="" id="personal-information-form" v-on:submit.prevent="submit"></form>
</template>

<script setup>
    const submit = () => {
        console.log('submit the form')
    }
</script>

This works because using the form attribute on a button will submit the form with the same ID, even if it's not within the form itself.

<button form="personal-information-form">Click me</button>

Haz

Replied 1 year ago

Thanks @alex! Everything is working now. Here is the final dumbed down result in case anyone else stumbles upon this thread.

const personalInformationForm = ref(null);

 <AccountPanel
    panelDescription="Update your personal information."
    panelId="personalInformation"
    panelTitle="Personal Information"
>
    <PersonalInformation ref="personalInformationForm" />

    <template v-slot:footer>
        <button v-on:click="personalInformationForm.updatePersonalInformation()">Save</button>
    </template>
</AccountPanel>
<script setup>
    import { reactive } from "vue";

    const updatePersonalInformation = () => {
        // logic...
    };

    defineExpose({
        updatePersonalInformation,
    });
</script>

<template>
    <form method="POST" v-on:submit.prevent="updatePersonalInformation">
        <div>
            <label for="first_name">First name</label>

            <input v-model="form.first_name">
        </div>

        <div>
            <label for="last_name">Last name</label>

            <input v-model="form.last_name">
        </div>

        <div>
            <label for="email">Email address</label>

            <input v-model="form.email">
        </div>

        <div>
            <label for="phone">Phone</label>

            <input v-model="form.phone">
        </div>
    </form>
</template>

alex

Replied 1 year ago

Awesome!