calin24
calin24

Reputation: 981

how to make bootstrap4 collapse work in vue?

I am using bootstrap 4.3.1 and [email protected]

I have this menu (is using collapse - and I don`t want to use JQuery):

 <li class="nav-item">
     <a class="nav-link" href="#sidebar-products" data-toggle="collapse" role="button" aria-expanded="false" aria-controls="sidebar-products">
         <i class="ni ni-single-copy-04 text-primary"></i>
         <span class="nav-link-text">Products</span>
     </a>
     <div class="collapse" id="sidebar-products">
         <ul class="nav nav-sm flex-column">
             <li class="nav-item">
                 <a href="#" class="nav-link">Item List 1</a>
             </li>
             <li class="nav-item">
                 <a href="#" class="nav-link">Item List 2</a>
             </li>
         </ul>
     </div>
 </li>

This is only a single block that contains 2 sub-items.

What I saw using JQuery, when click on "Products" the #sidebar-products receives the .show class and aria-expanded="true".

When having multiple blocks - when click on a block to close (if there are collapsed) the others blocks.

How can I make it work the collapse with vue?

UPDATE 1

I created a click event that do the job:

<a class="nav-link" href="javascript:void(0)" @click="navItemCollapse('sidebar-products', $event)" data-toggle="collapse" role="button" aria-expanded="false" aria-controls="sidebar-products">

and the event:

 navItemCollapse(id, event) {
     let expanded = event.target.getAttribute('aria-expanded').toLocaleLowerCase() == 'true' ? true : false;
     let el = document.getElementById(id);
     expanded ? el.classList.remove('show') : el.classList.add('show');
                event.target.setAttribute('aria-expanded', !expanded);
 }

But what if I have more blocks ? When click to open the current collapse on a block to close the others ???

Upvotes: 2

Views: 7989

Answers (4)

Lucian Filote
Lucian Filote

Reputation: 31

This is a fully working version using bootstrap-vue:

 <div class="accordion" role="tablist">
            <b-card v-for="(value, key) in this.jobs" :key="key" no-body class="mb-1">
              <b-card-header header-tag="header" class="p-1" role="tab">
                <b-button block v-b-toggle="'accordion-'+key" variant="primary">{{ value.title }}</b-button>
              </b-card-header>
              <b-collapse :id="'accordion-'+key.toString()" accordion="my-accordion" role="tabpanel">
                <b-card-body>
                  <b-card-text>{{ value.specs }}</b-card-text>
                </b-card-body>
              </b-collapse>
            </b-card>
          </div>

Data object:

data() {
    return {
     jobs: [
        {
          title: 'Design artist',
          specs: 'Have an eye for web beauty'
        },
        {
          title: 'Backend guru',
          specs: 'Do stuff that don\'t break'
        },
        {
          title: 'Frontend master',
          specs: 'Create an UI that works'
        }
      ]
}
}

Upvotes: 1

calin24
calin24

Reputation: 981

I like the @sugars approach :)

So...the final version is this:

<li v-for="(navItem, i) in sidenavItems" class="nav-item">
                            <router-link v-if="!navItem.isCollapsible" class="nav-link" @click.native="navItemCollapse(i)" active-class="active" :to="{name: navItem.route}" exact>
                                <i :class="navItem.class"></i>
                                <span class="nav-link-text">{{ navItem.name }}</span>
                            </router-link>

                            <a v-if="navItem.isCollapsible" class="nav-link" href="javascript:void(0)" @click="navItemCollapse(i)" data-toggle="collapse" :aria-expanded="navItem.expanded">
                                <i :class="navItem.class"></i>
                                <span class="nav-link-text">{{ navItem.name }}</span>
                            </a>
                            <div v-if="navItem.isCollapsible" class="collapse" :class="navItem.expanded ? 'show' : ''">
                                <ul class="nav nav-sm flex-column">
                                    <li v-for="subItem in navItem.items" class="nav-item">
                                        <router-link class="nav-link" :to="{name: subItem.route}">{{ subItem.name }}</router-link>
                                    </li>
                                </ul>
                            </div>
                        </li>

the sidenavItems:

 sidenavItems: [
                    {name: 'Dashboard', isCollapsible: false, route: 'dashboard', class: 'class1'},
                    {name: 'Categories', isCollapsible: false, route: 'category', class: 'class2'},
                    {name: 'Brands', isCollapsible: false, route: 'brand', class: 'class3'},
                    {name: 'Products', isCollapsible: true, expanded: false, class: 'class4', items: [{name: 'List', route: 'product'}]},
                    {name: 'Orders', isCollapsible: false, route: 'order', class: 'class5'},
                    {name: 'Blog', isCollapsible: true, expanded: false, class: 'class6', items: [{name: 'List', route: ''}]},
                ],

and the navItemCollapse method:

 navItemCollapse(index) {
     this.sidenavItems = this.sidenavItems.map( (item, i) => {
                item.expanded = !item.expanded;
                if(i !== index) {
                    item.expanded = false;
                }
                return item;
            })
        }

Upvotes: 0

Carol Skelly
Carol Skelly

Reputation: 362410

No jQuery or bootstrap-vue ...

Create a function in the Component to handle the normal Bootstrap class and timing logic...

  data() {
    return {
      classArr: ['collapse'],
      styleObj: {}
    };
  },
  methods: {
    toggleCollapse(ref) {
        let show = this.classArr.indexOf('show')>-1?false:'show'
        this.classArr = ['collapsing']
        setTimeout(() => {
            if (show){
                let height = this.$refs[ref].firstChild.clientHeight + 'px';
                this.styleObj = { height }
            }
            else {
                this.styleObj = {}  
            }
        }, 10)
        setTimeout(() => {
            this.classArr = ['collapse', show]
        }, 340)
    }
  }

In the component template, bind the class and style attrs to the data manipulated by the method. The ref of the specific collapse is passed in to the method...

  <li class="nav-item">
        <a class="nav-link" href="#sidebar-products" role="button" @click="toggleCollapse('sidebar')">
            <i class="ni ni-single-copy-04 text-primary"></i>
            <span class="nav-link-text">Products</span>
        </a>
        <div :class="classArr" :style="styleObj" id="sidebar-products" ref="sidebar">
            <ul class="nav nav-sm flex-column">
                <li class="nav-item">
                    <a href="#" class="nav-link">Item List 1</a>
                </li>
                <li class="nav-item">
                    <a href="#" class="nav-link">Item List 2</a>
                </li>
            </ul>
        </div>
  </li>

https://www.codeply.com/p/GA5CaNMzmc

EDIT: I updated the demo to make it scaleable for multiple collapses

Upvotes: 1

sugars
sugars

Reputation: 1493

This is the implementation of no jquery

new Vue({
  el: '#app',
  data() {
    return {
      menuList: [{
          name: 'Products',
          expand: false,
          items: [{
              name: 'Item List 1',
              link: ''
            },
            {
              name: 'Item List 2',
              link: ''
            }
          ]
        },
        {
          name: 'Others',
          expand: false,
          items: [{
              name: 'Other Item 1',
              link: ''
            },
            {
              name: 'Other Item 2',
              link: ''
            }
          ]
        }
      ]
    }
  },
  methods: {
    navItemCollapse(index) {
      this.menuList = this.menuList.map((item, i) => {
        item.expand = !item.expand
        if (i !== index) {
          item.expand = false
        }
        return item
      })
    }
  }
})
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>

<ul id="app">
  <li v-for="(navItem,i) in menuList" :key="i" class="nav-item">
    <a class="nav-link" href="javascript:;" data-toggle="collapse" role="button" :aria-expanded="navItem.expand" aria-controls="sidebar-products" @click.prevent="navItemCollapse(i)">
      <i class="ni ni-single-copy-04 text-primary"></i>
      <span class="nav-link-text">{{navItem.name}}</span>
    </a>
    <div v-if="navItem.items.length>0" class="collapse" :class="{show: navItem.expand}">
      <ul class="nav nav-sm flex-column">
        <li v-for="(subItem,j) in navItem.items" :key="j" class="nav-item">
          <a href="#" class="nav-link">{{subItem.name}}</a>
        </li>
      </ul>
    </div>
  </li>
</ul>

I integrate the menu data into an array of objects. Each object has an expand flag to determine whether it is currently expanded. When you click on the menu, switch the expand flag of the current menu.

Note: You don't need to care about the id of the <a> tag.

Upvotes: 4

Related Questions