Let me start by pointing out two problems with your code.
First, if you use v-for, it's almost always a better idea to use id instead of index. If you use id, your code reduces dramatically. F.E. In the code down below, there is no sub-menu control since it became unnecessary thanks to ids.
Second, I didn't get the idea of v-if / v-else in ul tag. I might be wrong but that two parts look very similar except the data type they accept. An array can have single object element. So you don't need v-else part. But if the similarity I saw isn't the case, please ignore this paragraph. In the codes down below I removed ul with v-else.
I made some other changes both in the template and script. Please read the code.
Here is the code that solves your problem:
<div id="app">
<div class="sidebar desktop expanded">
<div class="row" ref="menuPanel">
<div class="col-md-12">
<nav class="left-nav" role="navigation">
<ul>
<li
v-for="item in getMenu"
:key="item.id"
@click="handleMenuClick(item)"
>
<a
v-if="item.title"
:title="addTitle(item.title)"
:to="item.url"
class="menu-link"
>
<div v-if="collapsible" :class="{ iconOnly: collapsible }">
<i class="fa" :class="item.icon"></i>
</div>
<div v-else :class="{ iconAndText: !collapsible }">
<i class="fa" :class="item.icon"></i>
<span>{{ item.title }}</span>
</div>
<i class="fa fa-chevron-right"></i>
</a>
<!-- Menu Level 2 -->
<div
class="sidebar submenu"
ref="subMenuPanel"
v-if="openedItems.includes(item.id)"
:style="{
display: openedItems.includes(item.id) ? 'flex' : 'none',
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
}"
>
<ul>
<li>
<strong
@click="closeMenu(item.id)"
style="
padding: 0 0 0 0.8rem;
line-height: 2.5;
font-weight: 800;
"
>
x {{ item.title }}
</strong>
</li>
<li
v-for="sub_menu in item.siteMapNode"
:key="sub_menu.id"
@click="handleMenuClick(sub_menu)"
>
<a
v-if="sub_menu.title"
:title="addTitle(sub_menu.title)"
:to="sub_menu.url"
class="submenu-link"
>
<span>{{ sub_menu.title }}</span>
<i
class="fa fa-chevron-right mr-2"
v-if="subMenu.length !== 0"
></i>
</a>
<!-- Menu Level 3 -->
<div
class="sidebar subsubmenu"
:class="{ 'closed-menu': isSubMenuClose }"
ref="subSubMenuPanel"
v-if="openedItems.includes(sub_menu.id)"
v-bind:style="{
display: openedItems.includes(sub_menu.id)
? 'flex'
: 'none',
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
}"
>
<ul>
<li>
<strong
@click="closeMenu(sub_menu.id)"
style="
padding: 0 0 0 0.8rem;
line-height: 2.5;
font-weight: 800;
"
>
x {{ sub_menu.title }}
</strong>
</li>
<li
v-for="sub_sub_menu in sub_menu.siteMapNode"
:key="sub_sub_menu.id"
>
<a
v-if="sub_sub_menu.title"
:title="addTitle(sub_sub_menu.title)"
:to="sub_sub_menu.url"
class="submenu-link"
>
<span>{{ sub_sub_menu.title }}</span>
</a>
</li>
</ul>
</div>
</li>
</ul>
</div>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
The main trick here is to add event.stopPropagation() to closeMenu. Otherwise, when you click the submenu, you also click the main menu item. So at the same time, you close and open the menu. stopPropagation stops parent's methods.
In the script part, I removed some codes because they became unnecessary as well. Read the code, you will see. BTW, I don't know what is this.$forceUpdate() but should be required if it's about menu visibility.
new Vue({
el: "#app",
data() {
return {
openedItems: [],
// openedSubMenuItems: {},
// items: [],
isCollapsed: false,
// isMobile: false,
// active: false,
// compDisplay: "none",
// compSubMenuDisplay: "none",
subMenu: [],
// subSubMenu: [],
windowWidth: window.innerWidth,
windowHeight: window.innerHeight,
// show: false,
subMenuTop: 0,
subMenuLeft: 0,
subMenuHeight: 0,
subMenuBottom: 0,
subMenuOverflow: 0,
subSubMenuTop: 0,
subSubMenuLeft: 0,
subSubMenuHeight: 0,
subSubMenuBottom: 0,
subSubMenuOverflow: 0,
isMenuClose: false,
isSubMenuClose: false,
getMenu: [
{
id: Math.floor(Math.random() * 1000000),
title: "Parent 1",
menuLevel: "1_",
url: "",
show: "false",
icon: "fa-search",
siteMapNode: [
{
id: Math.floor(Math.random() * 1000000),
title: "Child 1",
menuLevel: "1_1",
url: "",
show: "false",
siteMapNode: [
{
title: "GrandChild 1",
menuLevel: "1_1_2",
url: "",
},
{
title: "GrandChild 2",
menuLevel: "1_1_3",
url: "",
},
{
title: "GrandChild 3",
menuLevel: "1_1_4",
url: "",
},
],
},
{
id: Math.floor(Math.random() * 1000000),
title: "Child 2",
menuLevel: "1_2",
url: "",
show: "false",
siteMapNode: [
{
title: "GrandChild 1",
menuLevel: "1_2_1",
url: "",
},
{
title: "GrandChild 2",
menuLevel: "1_2_2",
url: "",
},
{
title: "GrandChild 3",
menuLevel: "1_2_3",
url: "",
},
{
title: "GrandChild 4",
menuLevel: "1_2_4",
url: "",
},
],
},
],
},
{
id: Math.floor(Math.random() * 1000000),
title: "Parent 2",
menuLevel: "1_",
url: "",
show: "false",
icon: "fa-search",
siteMapNode: [
{
id: Math.floor(Math.random() * 1000000),
title: "Child 1",
menuLevel: "1_1",
url: "",
show: "false",
siteMapNode: [
{
title: "GrandChild 1",
menuLevel: "1_1_2",
url: "",
},
{
title: "GrandChild 2",
menuLevel: "1_1_3",
url: "",
},
{
title: "GrandChild 3",
menuLevel: "1_1_4",
url: "",
},
],
},
{
id: Math.floor(Math.random() * 1000000),
title: "Child 2",
menuLevel: "1_2",
url: "",
show: "false",
siteMapNode: [
{
title: "GrandChild 1",
menuLevel: "1_2_1",
url: "",
},
{
title: "GrandChild 2",
menuLevel: "1_2_2",
url: "",
},
{
title: "GrandChild 3",
menuLevel: "1_2_3",
url: "",
},
{
title: "GrandChild 4",
menuLevel: "1_2_4",
url: "",
},
],
},
],
},
],
};
},
methods: {
handleResize() {
(this.windowWidth = window.innerWidth),
(this.windowHeight = window.innerHeight);
},
addTitle(title) {
if (this.isCollapsed) {
return title;
}
},
handleMenuClick(item) {
if (item.siteMapNode !== null) {
this.openedItems.push(item.id);
this.$forceUpdate();
}
},
closeMenu(id) {
event.stopPropagation();
this.openedItems = this.openedItems.filter((x) => x !== id);
},
},
computed: {
collapsible() {
return this.sideNavCollapsed;
},
allMenu() {
return this.getMenu[0];
},
computedSubMenuDisplay() {
return this.compSubMenuDisplay;
},
computedDisplay() {
return this.windowHeight - 142;
},
},
beforeDestroy() {
window.removeEventListener("resize", this.handleResize);
},
})
@click="open = true"or in my case@click="isMenuClose = false". Unless there is something else in that answer that you want me to see