I have a sidebar with a NavBarItem component that receives a navItem object as a prop, and I want to change the active state to true when I click the link.
<template>
<nav class="mt-2 px-2">
<NavBarItem
@click="setActiveLink(item, _index)"
:item="item"
v-for="(item, _index) in navItems"
:key="item.label"
/>
</nav>
</template>
<script setup>
const setActiveLink = (el, i) => {
// 🤬🤯
}
const navItems = [
{
href: '#',
active: false,
label: 'VehicleBuild',
icon: vehicleTruckCube,
children: [
{
href: '/vehicle-packages',
active: false,
label: 'Paket',
icon: CubeIcon,
children: [],
},
],
}]
</script>
<script setup>
import { RouterLink } from 'vue-router'
import { computed } from 'vue'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
const props = defineProps({
item: Object,
})
const hasActiveChild = computed(() => {
function hasActiveItem(items) {
return items.some(item => item.active || hasActiveItem(item.children))
}
return hasActiveItem(props.item.children)
})
const emit = defineEmits(['click'])
</script>
<template>
<RouterLink
v-if="!item.children.length"
:class="[
'group flex w-full items-center rounded-md py-2 px-3 text-sm',
'hover:bg-gray-100',
item.active ? 'text-gray-800 font-semibold' : 'text-gray-600 font-medium',
]"
:to="item.href"
@click="emit('click', $event)"
>
<component
:class="[
'w-6 h-6 shrink-0 mr-2 group-hover:text-gray-600',
item.active ? 'text-gray-600' : 'text-gray-400',
]"
:is="item.icon"
v-if="item.icon"
></component>
<span>{{ item.label }}</span>
</RouterLink>
<Disclosure v-else v-slot="{ open }" :default-open="hasActiveChild">
<DisclosureButton
:class="[
'group flex w-full text-left items-center rounded-md py-2 px-3 text-sm',
'hover:bg-gray-100',
open ? 'font-semibold text-gray-800' : 'font-medium text-gray-600',
]"
>
<component
:class="[
'w-6 h-6 shrink-0 mr-2 group-hover:text-gray-600',
open ? 'text-gray-600' : 'text-gray-400',
]"
:is="item.icon"
v-if="item.icon"
></component>
<span class="flex-1">{{ item.label }}</span>
<ChevronDownIcon
:class="['w-4 h-4 shrink-0', open ? '-rotate-180 text-gray-600' : 'text-gray-400']"
/>
</DisclosureButton>
<DisclosurePanel class="ml-4">
<NavBarItem
v-for="(child, index) in item.children"
:item="child"
:key="index"
@click="emit('click', $event)"
/>
</DisclosurePanel>
</Disclosure>
</template>
I have tried to map the item I receive back from my emit in navBarItem but with no success, can somebody please help me out?
Any mutation you perform to navItems
after mounting <Navbar />
will not trigger an update in <template />
because the array is not reactive.
To add reactivity to it, wrap it in ref()
:
const navItems = ref([ /* items here */ ])
In your setActiveLink
method update navItems.value
accordingly and you will see the template updating accordingly.
Docs here.
Important note: Storing the active property inside each item only makes sense when you want to handle each item's active state individually (e.g: you want to be able to have more than 1 active item at a time). If the active
state is mutually exclusive (e.g: making an item active
means the previous one is no longer active), you should not store the active state inside each item.
Instead, you set a state variable holding the id
of the currently active item. And the isActive
property of each item is a computed
which compares the item's id
with the value of the state variable. Evidently, when setting the state var to a different id
the old active item becomes inactive, since its id
stops being equal to the state variable's value.
Proof of concept:
const { createApp, reactive, toRefs } = Vue
createApp({
setup() {
const state = reactive({
menuItems: ["A", "B", "C", "D", "E"].map((id) => ({ id })),
activeItem: "B",
isActive: (id) => state.activeItem === id,
setActive: (id) => (state.activeItem = id)
})
return toRefs(state)
}
}).mount("#app")
#app span {
border-bottom: 2px solid transparent;
padding: 1rem;
cursor: pointer;
}
#app span:hover {
border-bottom: 2px solid #eee;
}
#app span.active {
border-bottom: 2px solid red;
}
<script src="https://unpkg.com/vue@3.2.47/dist/vue.global.prod.js"></script>
<div id="app">
<span
v-for="({ id }, key) in menuItems"
:key="key"
v-text="id"
@click="() => setActive(id)"
:class="{active: isActive(id)}"
></span>
<pre
v-text="JSON.stringify({
activeItem,
menuItems: menuItems.map(({id}) => ({id, active: isActive(id)}))
}, null, 2)"
></pre>
</div>