【问题标题】:Vue js toggle class of each individual element每个单独元素的Vue js切换类
【发布时间】:2017-08-15 16:19:47
【问题描述】:

虽然在 Vuejs 中有很多切换类的示例,但我还没有找到一个切换类以缩小元素范围的示例。如果我这样定义一个全局变量:

data: {
  toggle: false
}

当我有一个元素时会遇到问题,比如这个导航栏:

<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>

看看这里发生了什么?如果我单击这两个元素之一,两者都将同时切换类,因为它正在更改全局变量。现在,我将如何切换 only 被点击的元素的类?

【问题讨论】:

  • 为什么不在状态 1 中为每个元素创建 2 个变量?
  • 每个列表项都应该是一个子组件,然后它可以存储自己的切换状态:)
  • 如果这是一个需要在大型网站上重复的功能,那将会非常不方便。
  • @harrypujols:这就是组件的用途。
  • @SLaks 我同意,但在我看来,对于不应该如此复杂的事情来说,这有点过度设计了。

标签: javascript vue.js vuejs2


【解决方案1】:

令我惊讶的是,如此简单的事情需要使用现代框架编写如此多的代码,这也是 JavaScript 开发变得如此复杂的一个原因。我求助于使用一个普通的 JavaScript 监听器来解决这个问题。

<li class="has-dropdown" @click="toggle">
    ...
</li>
...

methods: {
 toggle: function( event ) {
   event.target.classList.toggle('is-open')
 }
}

...

【讨论】:

  • 在接下来的几天里,我将接受同行投票最多的答案,因为我的答案不是真正的“Vue js”答案。
  • 这还可以同时打开两个不?
  • 否 @Bert 它只切换点击元素的类。这不再在数据中使用“切换”。
  • 我明白,但是如果一个有 is-open 类,我点击另一个也有 @click="toggle"li,那么我可以有两个 lis 和 is-open
  • 是的,你必须点击每一个你想切换到的类,这就是重点。
【解决方案2】:

这只是@SLaks 在他的回答中提到的一个小例子。本质上是将列表元素变成它们自己的组件,这样它们就可以拥有自己的状态。

Vue.component("clicktoggle", {
  template:`<li :class="{ 'is-open': toggle }" @click="toggle = !toggle"><slot></slot></li>`,
  data() {return {toggle: false}}
})

这是它的使用方法。

console.clear()

Vue.component("clicktoggle", {
  template:`<li :class="{ 'is-open': toggle }" @click="toggle = !toggle"><slot></slot></li>`,
  data() {return {toggle: false}}
})

new Vue({
  el:"#app"
})
.has-dropdown {
  cursor: pointer;
}
.has-dropdown:not(.is-open) ul {
  display: none
}
<script src="https://unpkg.com/vue@2.2.6/dist/vue.js"></script>
<div id="app">
  <ul class="menu">
    <li class="has-dropdown" is="clicktoggle">
      Foo
      <ul class="dropdown">
        <li>Dropdown Item 1</li>
        <li>Dropdown Item 2</li>
      </ul>
    </li>
    <clicktoggle class="has-dropdown">
      Bar
      <ul class="dropdown">
        <li>Dropdown Item 1</li>
        <li>Dropdown Item 2</li>
      </ul>
    </clicktoggle>
  </ul>
</div>

Vue 让淘汰这样的小组件变得异常简单。一种可能的快速改进是添加一个属性来指定您想要切换的 what 类。

【讨论】:

    【解决方案3】:

    VueJS(以及大多数其他现代 Web 框架)的基本指导原则是一切都来自模型。

    你从不谈论操作 DOM;相反,您制作一个模型来描述您想要的效果。

    在您的情况下,这意味着您需要两个 data 属性,而不是一个。

    但是,您实际上应该做的是让每个列表项成为自己的子组件(然后将获得自己的模型)。使用插槽指定每个插槽中的不同内容。

    【讨论】:

      【解决方案4】:

      我处理这个问题的方法不是使用布尔值,而是使用索引(数字或其他)。检查toggle === index 和点击是否设置切换到索引或-1


      这是一个使用字符串值作为切换的工作版本

      https://jsfiddle.net/dnqp2nc9/1/

      new Vue({
        el: '#app',
        data: {
      		toggle: null
        }
      })
      .has-dropdown li{
        opacity: 0.2;
      }
      .is-open li{
        opacity: 1;
      }
      <script src="https://unpkg.com/vue"></script>
      
      <div id="app">
        {{toggle}}
        <ul class="menu">
          <li class="has-dropdown" v-bind:class="{ 'is-open': toggle === 'foo' }" @click="toggle = toggle !== 'foo' ? 'foo' : null">
            Foo [{{toggle === 'foo' ? 'open' : 'closed'}}]
            <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 === 'bar' }" @click="toggle = toggle !== 'bar' ? 'bar' : null">
            Bar [{{toggle === 'bar' ? 'open' : 'closed'}}]
            <ul class="dropdown">
              <li>Dropdown Item 1</li>
              <li>Dropdown Item 2</li>
            </ul>
          </li>
        </ul>
      </div>

      打开/关闭多个,这里是数组版本 https://jsfiddle.net/hLm82x1d/1/

      new Vue({
        el: '#app',
        data: {
      		toggle: []
        },
        methods: {
        	toggleItem: function (key) {
          	var i = this.toggle.indexOf(key)
          	if (i < 0) {
            	this.toggle.push(key)
            } else {
            	this.toggle.splice(i, 1)
            }
          }
        }
      })
      .has-dropdown li{
        opacity: 0.2;
      }
      .is-open li{
        opacity: 1;
      }
      <script src="https://unpkg.com/vue"></script>
      
      <div id="app">
        {{toggle}}
        <ul class="menu">
          <li class="has-dropdown" v-bind:class="{ 'is-open': toggle.indexOf('foo') >= 0 }" @click="toggleItem('foo')">
            Foo [{{toggle === 'foo' ? 'open' : 'closed'}}]
            <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.indexOf('bar') >= 0 }" @click="toggleItem('bar')">
            Bar [{{toggle === 'bar' ? 'open' : 'closed'}}]
            <ul class="dropdown">
              <li>Dropdown Item 1</li>
              <li>Dropdown Item 2</li>
            </ul>
          </li>
        </ul>
      </div>

      【讨论】:

      • 你假设他不想同时打开两者。
      • 在问题样本中都已经同时打开,所以假设是正确的。重点是一次打开一个。
      • 是的,在multiple的情况下,可以设置为数组添加和删除索引
      • @harrypujols 如果您想一次打开一个,只需将打开的索引存储在数据中即可。你不需要存储所有项目的状态。
      • 我已经更新了答案以包含一些代码,只是知道这是为了说明这个概念,有一些方法可以使用更好的实践来实现这一点(例如使用计算值和方法)
      【解决方案5】:

      这似乎对我有用(Nuxt 和 Bootstrap 5),想法是将下拉菜单作为通过数据切换的子组件,这仍在进行中,但似乎有效,我正在学习 Js 和 Vue去吧,所以这可能不是最好的方法。

      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>
      

      点击外部时关闭下拉菜单

      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)
        },
      });
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-10-20
        • 1970-01-01
        • 2018-04-13
        • 1970-01-01
        • 2016-10-31
        • 1970-01-01
        • 2017-04-05
        • 1970-01-01
        相关资源
        最近更新 更多