<MenuItems /> is missing a parent <Menu /> component error (with MenuItems in slot) - vue.js

I am creating a component in Vue3 called <Dropdown>.
The Dropdown component uses other components in turn: Menu, MenuItems, MenuItem and MenuButton from the headlessui/vue library.
My idea is that you can put any content in the Dropdown, that's why I created a slot called #contentdropdown.
The problem is that when I pass this slot content to the Dropdown component, Vue gives me the following error:
< MenuItems /> is missing a parent < Menu /> component
This is my Dropdown componente code:
<template>
<Menu as="div" class="relative inline-block text-left">
<div>
<MenuButton class="btn inline-flex justify-center w-full text-sm" :class="'btn-'+variant">
Options
<ChevronDownIcon class="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
</MenuButton>
</div>
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75" leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
<slot name="contentdropdown"></slot>
</transition>
</Menu>
</template>
<script>
import { Menu, MenuButton } from '#headlessui/vue'
import { ChevronDownIcon } from '#heroicons/vue/solid'
import { vVariantProp } from '../../../../constants'
import { reactive, computed } from 'vue';
export default {
name: 'dropdown',
props: {
...vVariantProp,
},
setup(props) {
props = reactive(props);
return {
}
},
};
</script>
Why does it need the parent component called Menu?, if in fact I am already painting the slot inside the component and also importing it inside the Dropdown component.
This is how I pass to the Dropdown component the content through its #contentdropdown slot:
<Dropdown v-bind="{'variant':'primary'}">
<template #contentdropdown>
<MenuItems class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<div class="py-1">
<MenuItem>
Subitem1
</MenuItem>
<MenuItem>
Subitem2
</MenuItem>
</div>
</MenuItems>
</template>
</Dropdown>

The error <MenuItems /> is missing a parent <Menu /> component is not a Vue specific error. It is an error thrown by headlessui/vue - source
MenuItems component (as well as MenuButon etc - see doc) is designed to be used inside Menu component. It is using inject to tap into state provideded by the Menu component. There is nothing you can do about it - it is designed that way
Problem is that slot content (everything inside <template #contentdropdown> in the last code example) in Vue is always rendered in parent scope
Everything in the parent template is compiled in parent scope; everything in the child template is compiled in the child scope.
This means that MenuItems rendered as slot content has no access to data provideded by the Menu component rendered inside your Dropdown component
I don't see any way to overcome this limitation. You'll need to change your design (or describe your use case to headlessui/vue maintainers and ask them to implement alternative approach to share MenuContext with child components - for example using slot props)

Related

Why is v-model not working properly, returning'' Property "modelValue" was accessed during render but is not defined on instance.'' Vue 3

The Component I am making is a basic toggle switch made in vue using tailwind. during the initial render the switch is in an incorrect position, first click toggles the theme but the switch still stays the same, third click and forward the switch works properly
Switch.vue
<template>
<label class="switch relative inline-block w-[39px] h-[19px]">
<input type="checkbox" class="w-0 h-0 opacity-0" v-model="modelValue">
<span
class="absolute rounded-[34px] before:rounded-[50%] cursor-pointer top-0 left-0 right-0 bottom-0 bg-gray-300 transition shadow-inner before:absolute before:h-[17px] before:w-[17px] before:left-[1px] before:top-[1px] before:bg-white before:transition "
:class="[{
'before:translate-x-5 bg-blue-500': modelValue
}]" #click="$emit('update:modelValue', modelValue)"></span>
</label>
</template>
Parent.vue
// darkTheme comes from a pinia store.
<Switch v-model="darkTheme.enableDarkTheme" />
I was expecting the switch to work with v-model and toggle the theme, it works after a few clicks but first 3 clicks it does not toggle.
import { defineStore } from "pinia";
export let useThemeStore = defineStore('darkTheme', {
state: () => {
return {
enableDarkTheme: window.matchMedia('(prefers-color-scheme: dark)').matches,
}
},
actions: {
toggle() {
!this.enableDarkTheme;
}
}
})
Edit: Pinia seems to be sending the prop value as undefined on the first mutation, which seems to be causing the issue. please verify the store code
Referring to custom components v-model in the docs
https://vuejs.org/guide/components/v-model.html
for v-model in custom components props and emits need to be explicitly defined in the script setup, also see #Estus's comment about nested v-model
This is a simple vue component for a toggle slider
Switch.vue
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<label class="switch relative inline-block w-[39px] h-[19px]">
<input type="checkbox" class="w-0 h-0 opacity-0" :checked="modelValue">
<span
class="absolute rounded-[34px] before:rounded-[50%] cursor-pointer top-0 left-0 right-0 bottom-0 bg-gray-300 transition shadow-inner before:absolute before:h-[17px] before:w-[17px] before:left-[1px] before:top-[1px] before:bg-white before:transition "
:class="[{
'before:translate-x-5 bg-blue-500': modelValue
}]" #click="$emit('update:modelValue', !modelValue)"></span>
</label>
</template>
Parent Component
//Inside the parent component
<Switch v-model="darkTheme.enableDarkTheme" />

How to change styles of child component based on grid / list toggled in parent component, in vue?

I have a view in Vue project.
Home.vue
<template>
<TestLayout >
<Card/>
<Card/>
<Card/>
</TestLayout>
</template>
<script>
import TestLayout from "../components/TestLayout.vue"
import Card from "../components/Card.vue"
export default {
name: "Home",
props:{
isList:{
type: Boolean
}
},
components: {
TestLayout,
Card
},
}
</script>
The TestLayout has a section where we can display cards in list or grid view
TestLayout.vue
<template>
<div class="flex border-solid ">
<ListBulletIcon class="h-10 w-10 cursor-pointer shadow border-2 border-indigo-600 rounded-l p-2"
#click="listView = true" />
<TableCellsIcon #click="listView = false"
class="h-10 w-10 cursor-pointer shadow border-2 border-indigo-600 rounded-r p-2" />
</div>
<section
:class="[listView ? 'md:grid-cols-1 grid-cols-1' : 'md:grid-cols-4 grid-cols-2', 'rounded-md grid gap-5 col-span-full']">
<slot :listView="listView"></slot>
</section>
</template>
<script>
import {
ListBulletIcon,TableCellsIcon} from '#heroicons/vue/24/outline'
export default {
data: function () {
return {
listView: false,
}
},
components: {
ListBulletIcon,
TableCellsIcon,
},
}
}
</script>
I want to change the style of Card.vue based on whether user clicks grid view or list view icon.
For example, I want to add this style to Card.vue div tag in its template:
:class="[isList ? 'dark:bg-midnight' : 'dark:bg-red-300', 'min-h-80 w-full bg-gray-50 shadow-xl rounded-md flex flex-col']"
How will I check isList is clicked or not?
How can I achieve this?
You're half way there. After defining a slot prop (<slot :listView="listView"></slot>) you should access it in parent and pass it down to slot components.
<TestLayout>
<template v-slot="{ listView }">
<Card :isList="listView" />
<Card :isList="listView" />
</template>
</TestLayout>

Vue - Component that accepts a nested component

I would like to have a component that can have another component placed into it. I'm struggling to find how this can be achieved.
If I pass a component in it doesn't get displayed. I'm assuming I need to specify it in the component however I can't find how in the documentation.
E.g.
<component-that-allows-nesting>
<nested-component/>
</component-that-allows-nesting>
What needs to be added to my component to allow it to accept a nested component?
Vuejs support nested component like Base Component and you can use it. if you want send data from parent component to child component you can use props.
for example
//parent-component
<form>
<base-input />
<base-button>
add form
</base-button>
</form>
//child-component
//BaseInput.vue
<input type="text" placeholder="userName" />
//BaseButton.vue
<button #click="submitForm" >
<slot></slot>
</button>
Here is an example for vue3 carousel.
You should import nested component
You should add this component in components
Use it as nested component
<template>
<carousel :items-to-show="1.5">
<slide v-for="slide in 10" :key="slide">
{{ slide }}
</slide>
</carousel>
</template>
<script>
import { Carousel, Slide } from 'vue3-carousel';
export default {
name: 'App',
components: {
Carousel,
Slide,
},
};
</script>

How to access slot props from the component used inside the slot?

so everything i can find about scoped slots and passing props dont work for my specific situation:
i have following component order:
Home/List/ListItem
now i desided to replace the ListItem with a slot and because i use the List in a other Component too, but in there i need the ListOptionsItem.
in my home component i did this:
<list
class="mapkeyList"
:content="displayList"
:filterbar="true"
#handleSelection="addSelection"
#delete="deleteElement"
#editItem="editItem"
header="Mapkeys"
:key="mapkeyListKey"
>
<list-item>
</list-item>
</list>
in my List component i have this:
<template>
<div>
<h2 v-if="header">{{header}}</h2>
<div class="listContainer" v-if="showedContent.length > 0">
<div v-for=" (item, index) in showedContent" :key="index">
<slot
:item="item"
:index="index"
:dragable="dragableItems"
#auswahl="auswahlHandle"
#deleteElement="deleteElement"
#editItem="editItem"
:dontShowButtons="dontShowButtons"
#dragStart="handleOverDragStart"
:dragItem="dragItem"
#position="$emit('emitPosition',item)"
:deaktivierbar="deaktivierbar"
>
</slot >
</div>
finaly the listItem and the listOptionsItem need to access this props in the slot:
listItem:
<template>
<div class= "flexSpaceBetween" #click="$emit('auswahl',item)">
<div class="textFett">
{{item[0]}}
</div>
<div>
{{item[1]}}
</div>
</div>
i dont want to write all the neccessarry code in the home component because the listOptionsItem does need more informations and more space to write code.
my goal was it to define in the Home component that i want the list to use the listItem component and in the Options component the list should use the listItemOptions component. in the future there could be added new listItem versions.
Any component used inside scoped slot has no implicit access to the slot props. To make them available inside the component, you must pass it down to that component as props explicitly...
<list
class="mapkeyList"
:content="displayList"
:key="mapkeyListKey">
<template v-slot:default="{ item }">
<list-item :item="item">
</list-item>
</template>
</list>
If you have a lot of props/events you want to pass along, the ability of both v-bind and v-on to take an object as an argument is very useful because you can pass all the data and event handlers at the same time:
// List component
<template>
<div>
<h2 v-if="header">{{header}}</h2>
<div class="listContainer" v-if="showedContent.length > 0">
<div v-for=" (item, index) in showedContent" :key="index">
<slot :props="slotProps" :on="slotEventsHandlers"
</slot >
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
slotProps() {
return {
item: this.item,
dragable: this.dragableItems
}
},
slotEventsHandlers() {
return {
deleteElement: this.deleteElement,
dragStart: this.handleOverDragStart
}
}
}
}
</script>
And use it in parent:
<list
class="mapkeyList"
:content="displayList"
:key="mapkeyListKey">
<template v-slot:default="{ props, on }">
<list-item v-bind="props" v-on="on">
</list-item>
</template>
</list>

My dynamic component (layout) doesn't work with named slots in vuejs

I have problems to combine dynamic generated layouts with named slots.
To define my layouts I'm using "component :is"
//app.vue
<template>
<component :is="layout">
<router-view />
</component>
</template>
<script>
computed: {
layout() {
const layout = this.$route.meta.layout || 'default'
return () => import(`#/app/layouts/${layout}.vue`)
}
},
</script>
//layouts/default.vue
<template>
<div>
<div>
<slot name="header" />
</div>
<div>
<div>
<slot name="sidebar" />
</div>
<div>
<slot name="default"/>
</div>
</div>
</div>
</template>
// views/page.vue
<template>
<div>
<template #header>
<h1>Primitives</h1>
</template>
<template #sidebar>
<ul>
<li v-for="primitive in sections.sections" :key="primitive">
<router-link :to="`/primitives/${primitive}`">{{primitive}}</router-link>
</li>
</ul>
</template>
<template #default>
<router-view :key="$router.path" />
</template>
</div>
</template>
But now I get this error inside my code
'v-slot' directive must be owned by a custom element, but 'div' is not.
and console displays this error
<\template v-slot> can only appear at the root level inside the receiving component
If I remove the main div I get the error
The template root requires exactly one element.
What I'm doing wrong?
This is not easy to explain so please cope with me...
I really understand what you are trying to do but unfortunately it is not possible in Vue.
Reason for that is slots are more template compiler feature than runtime feature of Vue. What I mean by that ? When Vue template compiler sees something like <template #header>, it will take the inner content and compile it into a function returning virtual DOM elements. This function must be passed to some component which can call it and include the result in it's own virtual DOM it is generating. To do that template compiler needs to know to what component it should pass the function (that is the real meaning of 'v-slot' directive must be owned by a custom element, but 'div' is not. error message...ie compiler is "looking" for a component to pass the slot content to...)
But you are trying to use the slots as if they were "discoverable" at runtime. For your code to work the dynamic layout component must at runtime somehow discover that it's child (also dynamic thanks to <router-view />) has some slot content it can use. And this is not how slots work in Vue. You can pass the slot content your component receives from parent to a child components but do not expect that parent component (layout in this case) can "discover" slot content defined in it's child components...
Unfortunately only solution for your problem is to import the layout component in every "page" and use it as a root element in the template. You can use mixins to reduce code duplication (to define layout computed)
#/mixins/withLayout.js
export default = {
computed: {
layout() {
const layout = this.$route.meta.layout || 'default'
return () => import(`#/app/layouts/${layout}.vue`)
}
}
}
views/page.vue
<template>
<component :is="layout">
<template #header>
<h1>Primitives</h1>
</template>
<template #sidebar>
<ul>
<li v-for="primitive in sections.sections" :key="primitive">
<router-link :to="`/primitives/${primitive}`">{{primitive}}</router-link>
</li>
</ul>
</template>
<template #default>
<router-view :key="$router.path" />
</template>
</component>
</template>
<script>
import withLayout from '#/mixins/withLayout'
export default {
mixins: [withLayout]
}
</script>