Closing a dropdown menu when opening another one

As a beginner in Vue.js, I am currently working on a CRUD project for my coursework.

One issue I am facing is with the behavior of the dropdown menu. Can anyone provide assistance?

https://i.sstatic.net/1MozuN3L.png

I have specific requirements:

  1. The dropdown menu without an active state should automatically close when a user tries to open another dropdown menu without an active state.
  2. A dropdown menu with an active state should not close automatically; only the user can close or open it.
  3. If a user closes a dropdown menu that has an active state, I want the dropdown menu without an active state to remain open if it was already open.

Note:

  • An active state is triggered when a user accesses :active route.

  • isOpen represents the value indicating whether the dropdown menu is open or closed (collapsed or not collapsed).

  • sidebarContent.vue contains all the menu items on the sidebar.

  • sidebarCollapsibleItem.vue handles the links for the dropdown menus.

  • sidebarCollapsible.vue acts as a container for sidebarCollapsibleItem to create dropdown menus.

  • sidebarLink.vue behaves like an anchor tag in HTML to manage non-dropdown menu items.

How can I achieve these requirements?

Below is a snippet of my code:

sidebarCollapsible.vue

<script setup>
import { ref, watch } from 'vue';
import { sidebarState } from '@/Composables';
import SidebarLink from '@/Components/Sidebar/SidebarLink.vue';
import { EmptyCircleIcon } from '@/Components/Icons/Outline';

const props = defineProps({
    title: {
        type: String,
    },
    icon: {
        required: false,
    },
    active: {
        type: Boolean,
        default: false,
    },
});

const { active } = props;
const isOpen = ref(active);

const beforeEnter = (el) => {
    el.style.maxHeight = `0px`;
};
const enter = (el) => {
    el.style.maxHeight = `${el.scrollHeight}px`;
};
const beforeLeave = (el) => {
    el.style.maxHeight = `${el.scrollHeight}px`;
};
const leave = (el) => {
    el.style.maxHeight = `0px`;
};

watch(() => sidebarState.isHovered, (sideHover) => {
    if (!sideHover && !sidebarState.isOpen && !active) {
        isOpen.value = false;
    };
});
</script>

<template>
    <div class="relative">
         <SidebarLink @click="isOpen = !isOpen" :title="title" :active="active">
            <template #icon>
                <slot name="icon">
                    <EmptyCircleIcon aria-hidden="true" class="flex-shrink-0 w-6 h-6" />
                </slot>
            </template>

            <template #arrow>
                <span v-show="sidebarState.isOpen || sidebarState.isHovered" aria-hidden="true" class="relative block w-6 h-6 ml-auto">
                    <span :class="['absolute right-[9px] bg-gray-400 mt-[-5px] h-2 w-[2px] top-1/2 transition-all duration-200', {'-rotate-45': isOpen, 'rotate-45': !isOpen}]"></span>
                    <span :class="['absolute left-[9px] bg-gray-400 mt-[-5px] h-2 w-[2px] top-1/2 transition-all duration-200', {'rotate-45': isOpen, '-rotate-45': !isOpen}]"></span>
                </span>
            </template>
        </SidebarLink>

        <transition 
            @before-enter="beforeEnter"
            @enter="enter"
            @before-leave="beforeLeave"
            @leave="leave"
            appear>

            <div v-show="isOpen && (sidebarState.isOpen || sidebarState.isHovered)" class="overflow-hidden transition-all duration-200">
                <ul class="relative px-0 pt-2 pb-0 ml-5 before:w-0 before:block before:absolute before:inset-y-0 before:left-0 before:border-l-2 before:border-l-gray-200 dark:before:border-l-gray-600">
                    <slot />
                </ul>
            </div>
        </transition>
    </div>
</template>

sidebarCollapsibleItem.vue

<script setup>
import { defineProps } from 'vue';
import { Link } from '@inertiajs/vue3';

const props = defineProps({
    href: String,
    title: String,
    active: {
        type: Boolean,
        default: false,
    },
    external: {
        type: Boolean,
        default: false,
    },
});

const { external } = props;

const Tag = external ? 'a' : Link;
</script>

<template>
    <li
        :class="[
            'relative leading-8 m-0 pl-6',
            'before:block before:w-4 before:h-0 before:absolute before:left-0 before:top-4 before:border-t-2 before:border-t-gray-200 before:-mt-0.5',
            'last:before:bg-white last:before:h-auto last:before:top-4 last:before:bottom-0',
            'dark:last:before:bg-dark-eval-1 dark:before:border-t-gray-600',
        ]"
    >
        <component
            :is="Tag"
            :href="href"
            v-bind="$attrs"
            :class="[
                'transition-colors hover:text-gray-900 dark:hover:text-gray-100',
                {
                    'text-purple-500 dark:text-purple-500 hover:text-purple-600 dark:hover:text-purple-600': active,
                    'text-gray-500 dark:text-gray-400': !active,
                },
            ]"
        >
            {{ title }}
        </component>
    </li>
</template>

sidebarContent.vue

<script setup>
import PerfrectScrollbar from '@/Components/PerfectScrollbar';
import SidebarLink from '@/Components/Sidebar/SidebarLink.vue';
import { DashboardIcon } from '@/Components/Icons/Outline';
import { UserGroupIcon, BuildingOffice2Icon } from '@heroicons/vue/24/outline';
import SidebarCollapsible from '@/Components/Sidebar/SidebarCollapsible.vue';
import SidebarCollapsibleItem from '@/Components/Sidebar/SidebarCollapsibleItem.vue';
</script>

<template>
    <PerfrectScrollbar
        tagname="nav"
        aria-label="main"
        class="relative flex flex-col flex-1 max-h-full gap-4 px-3"
    >
        <SidebarLink
            title="Dashboard"
            :href="route('dashboard')"
            :active="route().current('dashboard')"
        >
            <template #icon>
                <DashboardIcon
                    class="flex-shrink-0 w-6 h-6"
                    aria-hidden="true"
                />
            </template>
        </SidebarLink>

        <SidebarCollapsible
            title="Management"
            :active="route().current('users.*') || route().current('teams.*') || route().current('divisions.*') || route().current('departments.*')"
        >
            <template #icon>
                <UserGroupIcon
                class="flex-shrink-0 w-6 h-6"
                aria-hidden="true"
                />
            </template>

            <SidebarCollapsibleItem
                title="Users"
                :href="route('users.index')"
                :active="route().current('users.*')"
            />

            <SidebarCollapsibleItem
                title="Roles"
                :href="route('teams.index')"
                :active="route().current('#')"
            />

            <SidebarCollapsibleItem
                :href="route('teams.index')"
                title="Teams"
                :active="route().current('teams.*')"
            />

            <SidebarCollapsibleItem
                title="Departments"
                :href="route('departments.index')"
                :active="route().current('departments.*')"
            />

            <SidebarCollapsibleItem
                title="Divisions"
                :href="route('divisions.index')"
                :active="route().current('divisions.*')"
            />
        </SidebarCollapsible>

        <SidebarCollapsible
            title="Locations"
            :active="route().current('externals.*') || route().current('internals.*')"
        >
            <template #icon>
                <BuildingOffice2Icon
                class="flex-shrink-0 w-6 h-6"
                aria-hidden="true"
                />
            </template>

            <SidebarCollapsibleItem
                title="Internals"
                :href="route('internals.index')"
                :active="route().current('internals.*')"
            />

            <SidebarCollapsibleItem
                title="Externals"
                :href="route('externals.index')"
                :active="route().current('externals.*')"
            />
        </SidebarCollapsible>

        <SidebarCollapsible
            title="test"
            :active="route().current('api-tokens.*')"
        >
            <template #icon>
                <BuildingOffice2Icon
                class="flex-shrink-0 w-6 h-6"
                aria-hidden="true"
                />
            </template>

            <SidebarCollapsibleItem
                title="Internals"
                :href="route('api-tokens.index')"
                :active="route().current('user.*')"
            />

            <SidebarCollapsibleItem
                title="Externals"
                :href="route('api-tokens.index')"
                :active="route().current('api-tokens.*')"
            />

        </SidebarCollapsible>

    </PerfrectScrollbar>
</template>

Thanks for any assistance provided. Apologies for any language barriers due to my limited proficiency in English.

Answer №1

Extending a massive thank you to @danielricado for the exceptional input!

toggleMenu.vue

<script setup>
import { ref, watch } from 'vue';
import { menuHelper } from '@/Utilities';
import MenuLink from '@/Components/Menu/MenuLink.vue';
import { CircleOutlineIcon } from '@/Icons/Packed';

const dataProps = defineProps({
    id: {
        type: String,
        required: true
    },
    title: {
        type: String,
    },
    icon: {
        required: false,
    },
    active: {
        type: Boolean,
        default: false,
    },
    isOpen: {
        type: Boolean,
        default: false,
    },
});

const emitActions = defineEmits(['toggle']);

const displayState = ref(dataProps.isOpen || dataProps.active);

watch(() => dataProps.isOpen, (showMenu) => {
    if (!dataProps.active) {
        displayState.value = showMenu;
    }
});

watch(() => dataProps.active, (activeMenu) => {
    if (activeMenu) {
        displayState.value = true;
    }
});

watch(() => menuHelper.isHovered, (hovered) => {
    if (!hovered && !menuHelper.isOpen) {
        if (!dataProps.active) {
            emitActions('toggle', null);
        } else {
            displayState.value = true;
        }
    }
});


const beforeShowAnimation = (el) => {
    el.style.maxHeight = `0px`;
};
const displayAnimation = (el) => {
    el.style.maxHeight = `${el.scrollHeight}px`;
};
const beforeHideAnimation = (el) => {
    el.style.maxHeight = `${el.scrollHeight}px`;
};
const hideAnimation = (el) => {
    el.style.maxHeight = `0px`;
};

const toggleView = () => {
    displayState.value = !displayState.value;

    if (!dataProps.active) {
        emitActions('toggle', dataProps.id);
    }
};
</script>

<template>
    <div class="relative">
      ...

  </div></answer1>
<exanswer1><div class="answer accepted" i="78493205" l="4.0" c="1715901764" a="UmV5bmFsZG8=" ai="13698501">
<p>An enormous shoutout to @danielricado for providing outstanding feedback!</p>
<p>expandViewer.vue</p>
<pre><code><script setup>
import { ref } from 'vue';
import Scrollable from '@/Components/Scrollable';
import MenuLink from '@/Components/Menu/MenuLink.vue';
import { DashboardIcon } from '@/Icons/Filled';
import { UserGroupIcon, BuildingOffice2Icon } from '@heroicons/vue/24/filled';
import ExpandedCollapsible from '@/Components/Menu/ExpandedCollapsible.vue';
import ExpandedCollapsibleItem from '@/Components/Menu/ExpandedCollapsibleItem.vue';

const expandId = ref(null);

const changeExpandStatus = (id) => {
    expandId.value = expandId.value === id ? null : id;
};
</script>

<template>
    <Scrollable
        tagname="nav"
        aria-label="main"
        class="relative flex flex-col flex-1 max-h-full gap-4 px-3&q...

    </div></answer1>
<exanswer1><div class="answer accepted" i="78493205" l="4.0" c="1715901764" a="UmV5bmFsZG8=" ai="13698501">
<p>A special recognition goes to @danielricado for the exceptional advice!</p>
<p>sidebarToggleContent.vue</p>
<pre><code><script setup>
import { ref, watch } from 'vue';
import { sidebarAssistant } from '@/Utils';
import SidebarLink from '@/Components/Sidebar/SidebarLink.vue';
import { FilledCircleIcon } from '@/Components/Icons/Packed';

const info = defineProps({
    id: {
        type: String,
        required: true
    },
    title: {
        type: String,
    },
    icon: {
        required: false,
    },
    active: {
        type: Boolean,
        default: false,
    },
    isOpen: {
        type: Boolean,
        default: false,
    },
});

const e...   :is-open="activeExpandId === 'management'"
            @toggle="flipExpands"
        >
            <template #icon>
                <UserGroupIcon
                class="flex-shrink-0 w-6 h-6"
                aria-hidden="true"
                />
            </template>

            <ExpandedCollapsibleItem
                title="Users"
                :href="route('users.index')"
                :active="route().current('users.*')"
            />

            <ExpandedCollapsibleItem
                title="Roles"
                :href="route('teams.index')"
                :active="route().current('#')"
            />

            <ExpandedCollapsibleIte...
  
    </div></answer1>
<exanswer1><div class="answer accepted" i="78493205" l="4.0" c="1715901764" a="UmV5bmFsZG8=" ai="13698501">
<p>Kudos to @danielricado for the insightful feedback!</p>
<p>menuNavigation.vue</p>
<pre><code><script setup>
import { ref, watch } from 'vue';
import { navigationHelper } from '@/Utilities';
import NavigationLink from '@/Components/Navigation/Nav.vue';
import { CircleOutlineIcon } from '@/Icons/Packed';

const dataProps = defineProps({
    id: {
        type: String,
        required: true
    },
    title: {
        type: String,
    },
    icon: {
        required: false,
    },
    active: {
        type: Boolean,
        default: false,
    },
    isOpen: {
        type: Boolean,
        default: false,
    },
});

const emitActions = defineEmits(['toggle']);

const displayState = ref(dataProps.isOpen || dataProps.active);

watch(() => dataProps.isOpen, (showNav) => {
    if (!da...

    </div></answer1>
<exanswer1><div class="answer accepted" i="78493205" l="4.0" c="1715901764" a="UmV5bmFsZG8=" ai="13698501">
<p>Appreciate the feedback, @danielricado! Thanks!</p>
<p>navigationExpand.vue</p>
<pre><code><script setup>
import { ref } from 'vue';
import Scrollable from '@/Components/Scrollable';
import NavigationLink from '@/Components/Navigation/NavigationLink.vue';
import { DashboardIcon } from '@/Icons/Filled';
import { UserGroupIcon, BuildingOffice2Icon } from '@heroicons/vue/24/filled';
import ExpandedCollapsible from '@/Components/Navigation/ExpandedCollapsible.vue';
import ExpandedCollapsibleItem from '@/Components/Navigation/ExpandedCollapsibleItem.vue';

const expandId = ref(null);

const changeExpandStatus = (id) => {
    expandId.value = expandId.value === id ? null : id;
};
</script>

<template>
    <Scrollable
        tagname="nav"
        aria-label="main"
        class="relative flex flex-col flex-1 max-h-full gap-4 px-3"
    >
        <NavigationLink
      …

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Best practices for alerting using React and Redux

As I delve into Redux for the first time and work on revamping a fairly intricate ReactJS application using Redux, I've decided to create a "feature" for handling notifications. This feature will involve managing a slice of state with various properti ...

I am facing an issue with my if statement not functioning properly when verifying a radio input

Trying to validate radio input to confirm if it's the correct answer, but encountering an issue where it skips the line if (answer == allQuestions[q].correctAnswer). Here is the complete code: https://jsfiddle.net/alcatel/sopfmevh/1/ for (let k = 0; ...

Unleashing the Power of Node Js: Retrieving File Data and Form Data in POST Requests

When sending form data values using Postman, you can utilize a NodeJS server in the backend to handle the POST request. Here is an example of how to structure your code: require('dotenv').config(); const express = require('express'); co ...

What is the best way to capture data sent by Express within a functional component?

const Header = (props) => { const [ serverData, setServerData ] = useState({}); useEffect(() => { fetch('http://localhost:4001/api') .then(res => res.json()) .then((data) => { setServerData(data); ...

The functionality of the Firebase Realtime Database has ceased

After successfully developing an app with Firebase's Real-time Database for the past month, I suddenly encountered an issue today. Despite the authentication features working seamlessly, my app is no longer able to retrieve data from Firebase. It&apos ...

The conversion of string to number is not getting displayed correctly when using console.log or document.write. Additionally, the concatenated display is also not functioning as intended

Being new to JS and HTML, this program was created to enhance my understanding of the concepts. I attempted a basic program to convert a string to a number in three different ways, but I am having trouble determining if the conversion actually took place. ...

Top Strategies for PHP - Managing Directs and Header Content

PHP is a versatile language frequently used for generating 'templates' like headers to maintain a consistent look across websites and simplify updates via require or include commands. Another common task involves managing log-ins and redirecting ...

When a user clicks on an anchor tag in a React component, the input element will automatically receive

I am currently working with two components: Within my parent component, I have the following set up: // Initialize focus object as false // Import Child Component and React libraries const parent = React.createClass({ getInitialState: function() { ...

What is the best way to showcase page content once the page has finished loading?

I'm facing an issue with my website. It has a large amount of content that I need to display in a jQuery table. The problem is that while the page is loading, all rows of the table are showing up and making the page extremely long instead of being sho ...

Verify if a particular string is present within an array

I am in possession of the key StudentMembers[1].active, and now I must verify if this particular key exists within the following array const array= ["StudentMembers.Active", "StudentMembers.InActive"] What is the method to eliminate the index [1] from Stu ...

Ways to guarantee a distinct identifier for every object that derives from a prototype in JavaScript

My JavaScript constructor looks like this: var BaseThing = function() { this.id = generateGuid(); } When a new BaseThing is created, the ID is unique each time. var thingOne = new BaseThing(); var thingTwo = new BaseThing(); console.log(thingOne.id == ...

Having issues with the input event not triggering when the value is modified using jQuery's val() or JavaScript

When a value of an input field is changed programmatically, the expected input and change events do not trigger. Here's an example scenario: var $input = $('#myinput'); $input.on('input', function() { // Perform this action w ...

Issue arises when child component fails to receive updated props from parent component after data changes in parent component

I am working with a template named Report.vue, which contains a data() property called dataSent:[]. Within this template, I have a component called BarChart that should receive an updated array from the dataSent property to display graphs. However, when I ...

Extending Vue components with TypeScript for enhanced styling features

Exploring Vuejs with TypeScript components has been an educational journey for me. While I found using class-based components quite intuitive, I've encountered errors when trying to use the Vue.extend({}) approach. Are there any resources such as arti ...

Guide to incorporating a jade file into a node.js application after executing an Ajax request

While working on my node.js application, I encountered an issue with loading a new HTML file using Ajax on a button click. Despite everything else working perfectly, the new HTML file was not being loaded. This is how I am making the ajax call in my main. ...

Reversing data in Vue3

I've been struggling to create a custom dropdown for my application. I need to implement a function that adds a class called is-active to a div element. So, what I have done is created a simple div with an onclick function as shown below: <div :cla ...

Is there a way to transfer a value from one JS function to another in Node.js?

I am struggling to retrieve the return value from a JavaScript function in Node.js and pass it back in a POST method within my server file. Despite my efforts, I keep receiving an undefined result. What could be causing this issue? My objective is to retur ...

Passing state to getStaticProps in Next JSLearn how to effectively pass state

I am currently fetching games from IGDB database using getStaticProps and it's all working perfectly. However, I now have a new requirement to implement game searching functionality using a text input field and a button. The challenge I'm facing ...

Angular binding for selecting all data

Upon checking a checkbox for a single item, the bound data is retrieved and added to an array. However, this does not happen when using selectAll. Code snippet in Angular for obtaining the object of a checked item: $scope.selectedOrganisations = []; $sco ...

Is there a built-in method or library for extracting data from JSON strings in programming languages?

Duplicate Query: how to parse json in javascript The data sent back by my server is in JSON format, but it doesn't follow the usual key/value pairs structure. Here's an example of the data I'm receiving: ["Value1","Value2","Value3"] ...