0

I'm working on an application that is 90% built without the help of any front-end boilerplate and this menu is causing some difficulty. I can open the menu items and then I can't close them! Why might that be?

I've tried specifying isMenuclose = true and negating itself for a toggle isMenuClose = !isMenuClose which it's what I thought would work best. See below, it still doesn't work:

closeMenu (index) {
  //this.openedItems[index] = !this.openedItems[index]
  this.isMenuClose = !this.isMenuClose
},

I've made a JS Fiddle with as much as I could to mimic the environment. It seems like such a simple functionality. All I need is the closeMenu and SubMenu to work close independently.

6
  • stackoverflow.com/questions/49320415/… Commented Oct 20, 2020 at 21:31
  • I've already tried @click="open = true" or in my case @click="isMenuClose = false". Unless there is something else in that answer that you want me to see Commented Oct 20, 2020 at 21:31
  • The state needs to be reset to false, yeah? Commented Oct 20, 2020 at 21:41
  • Right. The state is originally false. The openMenu function sets it to true. The closeMenu function sets it to false...or at least that's what it should do and unless my eyes are failing me, I did that. But the false is not sticking. Once it's true, the click to set it to false doesn't. The console.log shows a count of how many times I'm hitting the function and it's returning the same unchanged state back to me. It's weird. Commented Oct 20, 2020 at 21:45
  • I made a JSFiddle for it. Feeel free if you feel so inclined. I've tried all thta "could" work. I just need what "does" work. Commented Oct 20, 2020 at 21:47

3 Answers 3

1

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);
  },
})
Sign up to request clarification or add additional context in comments.

1 Comment

Works great in the fiddle! Thank you. Hopefuly it works in my app but that's not your fault lol
1

Variables openedSubMenuItems and openedMenuItems are not reactive because you are assign the values in a wrong way according to Vue docs (https://v2.vuejs.org/v2/guide/reactivity.html#For-Objects).

instead of:

this.openedSubMenuItems[index] = true

Use:

this.$set(this.openedSubMenuItems, index, true)

And when you click in the element to close the menu, click event is propagating, triggering handleMenuClick again, stop propagation using stop flag:

@click.stop="closeMenu(index)"

This changes should do the trick.

Comments

0

You need to define where to change this.isMenuClose.

Here:

handleMenuClick (index, item) {
  if (item.siteMapNode !== null && this.isMenuClose === false) {
    this.openedItems[index] = true
    this.$forceUpdate()
  }
  this.isMenuClose = !this.isMenuClose
},

or here:

closeMenu (index) {
  this.openedItems[index] = !this.openedItems[index]
  this.isMenuClose = !this.isMenuClose
  console.log("isMenuClose", this.isMenuClose)
},

Both functions are called on click event.

Suppose this.isMenuClose is true, when first function is executed this.isMenuClose is false, then when second function is executed this.isMenuClose is true (initial value).

I removed this line this.isMenuClose = !this.isMenuClose from handleMenuClick, and I can see how this.isMenuClose changes

1 Comment

I've already tried that sir. Test out your code and see it doesn't close the submenu.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.