Vue js toggle class of each individual element

Viewed 17348

While there are many examples of toggling classes in Vuejs, I am yet to find one that toggles a class narrowing down the scope of the element. If I define a global variable like this:

data: {
  toggle: false
}

I will run into a problem when I have an element, like this navigation bar:

<ul class="menu">
  <li class="has-dropdown" v-bind:class="{ 'is-open': toggle }" @click="toggle = !toggle">
    Foo
    <ul class="dropdown">
      <li>Dropdown Item 1</li>
      <li>Dropdown Item 2</li>
    </ul>
  </li>
  <li class="has-dropdown" v-bind:class="{ 'is-open': toggle }" @click="toggle = !toggle">
    Bar
    <ul class="dropdown">
      <li>Dropdown Item 1</li>
      <li>Dropdown Item 2</li>
    </ul>
  </li>
</ul>

See what happens here? If I click in one of these two elements, both are going to toggle the class at the same time, because it's changing a global variable. Now, how would I toggle the class of only the element that is clicked?

5 Answers

This seem to have worked for me (Nuxt and Bootstrap 5), idea was to have dropdown as child component that toggles via data, this is still work in progress but it seem to work, I am learning Js and Vue as I go so this might not be the best way to do it.

ThePrimary.vue

<template>
  <nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container-fluid">
      <nuxt-link class="navbar-brand" to="/">Navbar</nuxt-link>
      <button
        @click="isNavbarCollapsed = !isNavbarCollapsed"
        ref="navbar-toggler"
        :aria-expanded="[!isNavbarCollapsed ? 'true' : 'false']"
        :class="{ collapsed: isNavbarCollapsed}"
        class="navbar-toggler"
        type="button"
        data-toggle="collapse"
        data-target="#navbarNavDropdown"
        aria-controls="navbarNavDropdown"
        aria-label="Toggle navigation"
      >
        <span class="navbar-toggler-icon"></span>
      </button>
      <div 
        :class="{ show: !isNavbarCollapsed}"
        class="collapse navbar-collapse" 
        id="navbarNavDropdown"
      >
        <NavbarNav :items="loadedPrimaryMenu" />
      </div>
    </div>
  </nav>
</template>

<script>
import NavbarNav from '@/components/Navigation/ThePrimary/NavbarNav'

export default {
  name: 'TheNavigationPrimary',
  data() {
    return {
      isNavbarCollapsed: true
    }
  },
  computed: {
    loadedPrimaryMenu() {
      return this.$store.getters.loadedPrimaryMenu
    }
  },
  components: {
    NavbarNav
  }
}
</script>

<style scoped lang="scss">


</style>

NavbarNav.vue

 <template>
  <ul class="navbar-nav">
    <li
      v-for="item in items"
      :key="item.id"
      class="nav-item"
      :class="{ dropdown: hasChildren(item.children) }"
    >
      <NavLink
        v-if="!hasChildren(item.children)" 
        :attributes="item"
      />
      <NavbarNavDropdownMenu
        v-else
        :item="item"
      />
    </li>
  </ul>
</template>

<script>
import NavbarNavDropdownMenu from "@/components/Navigation/ThePrimary/NavbarNavDropdownMenu";
import NavLink from '@/components/Navigation/NavLink';

export default {
  name: "NavbarNav",
  props: {
    items: {
      type: Array,
      required: true,
    },
  },
  data() {
    return {
    };
  },
  methods: {
    hasChildren(item) {
      return item.length > 0 ? true : false;
    },
  },
  components: {
    NavbarNavDropdownMenu,
    NavLink
  }
};
</script>

<style scoped lang="scss">

</style>

NavbarNavDropdownMenu.vue

<template>
  <span v-if="item">
    <nuxt-link
      to="#"
      @click.prevent.native="openDropdownMenu"
      v-click-outside="closeDropdownMenu"
      :title="item.title"
      :class="[
        item.cssClasses, 
        { show: isDropdownMenuVisible }
      ]"
      :id="`navbarDropdownMenuLink-${item.id}`"
      :aria-expanded="[isDropdownMenuVisible ? true : false]"
      class="nav-link dropdown-toggle"
      aria-current="page"
      role="button"
      data-toggle="dropdown"
    >
      {{ item.label }}
    </nuxt-link>
    <ul
      :class="{ show: isDropdownMenuVisible }"
      :aria-labelledby="`navbarDropdownMenuLink-${item.id}`"
      class="dropdown-menu"
    >
      <li v-for="item in item.children" :key="item.id">
        <NavLink
          :attributes="item"
          class="dropdown-item"
        />
      </li>
    </ul>
  </span>
</template>

<script>
import NavLink from '@/components/Navigation/NavLink';

export default {
  name: "DropdownMenu",
  props: {
    item: {
      type: Object,
      required: true,
    },
  },
  data() {
    return {
      isDropdownMenuVisible: false,
    };
  },
  methods: {
    openDropdownMenu() {
      this.isDropdownMenuVisible = !this.isDropdownMenuVisible;
    },

    closeDropdownMenu() {
        this.isDropdownMenuVisible = false;
    }
  },
  components: {
    NavLink
  }
};
</script>

<style scoped lang="scss">

</style>

NavLink.vue

 <template>
  <component
    v-bind="linkProps(attributes.path)"
    :is="attributes"  
    :title="attributes.title"
    :class="[ attributes.cssClasses ]"
    class="nav-link active"
    aria-current="page"
    prefetch
  >
  {{ attributes.label }}
  </component>
</template>

<script>
export default {
  name: 'NavLink',
  props: {
    attributes: {
      type: Object,
      required: true
    }
  },
  methods: {
    linkProps (path) {
      if (path.match(/^(http(s)?|ftp):\/\//) || path.target === '_blank') {
        return {
          is: 'a',
          href: path,
          target: '_blank',
          rel: 'noopener'
        }
      }
      return {
        is: 'nuxt-link',
        to: path
      }
    }
  }
}
</script>

<style scoped lang="scss">

</style>

To close dropdown on click outside

import Vue from 'vue'

Vue.directive('click-outside', {
  bind: function (el, binding, vnode) {
    el.clickOutsideEvent = function (event) {
      if (!(el == event.target || el.contains(event.target))) {
        vnode.context[binding.expression](event);
      }
    };
    document.body.addEventListener('click', el.clickOutsideEvent)
  },
  unbind: function (el) {
    document.body.removeEventListener('click', el.clickOutsideEvent)
  },
});
Related