VueJs Lazy initialization of tab's component just once - vue.js

I'm trying to make the bootstrap-vue tabs initialization lazy and though it works if i set the lazy attribute on true, it render the component every time i'm visiting a specific tab:
BtabsWrapper.vue:
<b-tabs
:lazy="true"
>
<b-tab
v-for="(tab, index) in tabs"
:key="'li-tab-' + index"
:title="tab.title"
:href="'#' + tab.id"
>
<slot
:name="tab.id"
/>
</b-tab>
</b-tabs>
I need more of a lazy initialization of each tab(just once) rather than re rendering the tab's component every time the user visits it. Any ideas?

If you wrap the content of each tab in a v-if and change that condition once when they're loaded you should get your desired outcome.
And in your case if you're using a v-for, you can utilize the index in your v-for in the includes part as visitedTabs.includes(index)
window.onload = () => {
new Vue({
el: '#app',
data() {
return {
visitedTabs: []
}
},
methods: {
onInput(value) {
if(!this.visitedTabs.includes(value)){
this.visitedTabs.push(value)
}
}
}
})
}
body {
padding: 1em;
}
<link href="https://unpkg.com/bootstrap#4.3.1/dist/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://unpkg.com/bootstrap-vue#2.0.4/dist/bootstrap-vue.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.0.4/dist/bootstrap-vue.js"></script>
<div id="app">
<b-card clas no-body>
<b-tabs card #input="onInput">
<b-tab title="First tab">
<div v-if="visitedTabs.includes(0)">
Hello World
</div>
</b-tab>
<b-tab title="Second tab">
<div v-if="visitedTabs.includes(1)">
<b-input />
</div>
</b-tab>
<b-tab title="Third tab">
<div v-if="visitedTabs.includes(2)">
<b-checkbox />
</div>
</b-tab>
</b-tabs>
</b-card>
</div>

Related

Passing props dynamically to a component inside a v-for loop

I have a v-for loop that iterates through an array of meetings (meetings between sellers and potential buyers of used cars) and prints a card for each meeting, a basic display of who the meeting is with, what car it is about and the scheduled date. Now, I implemented a button that when clicked, opens a dialog with a Google Maps component that shows the marker for the agreed location of the meeting.
My problem is that no matter what card I click on, the dialog will always display the location of the LAST card, regardless of which has been clicked. I would think that since Im calling the component INSIDE the v-for loop it would pass props dynamically for each card, on each iteration, but that does not seem to be the case.
Here is the HTML:
<div
v-for="meeting in meetings"
:key="meeting.did"
class="col-12 col-md-6 col-lg-3 q-pa-md q-mx-xl"
>
<q-card class="my-card homeCard q-pa-md">
<q-dialog class="mapDialog flex column" v-model="mapDialog">
<MeetMapComponent
:key="componentKey"
:mapDiv="mapDiv"
:mapData="meeting.address"
:buyerName="meeting.name"
/>
</q-dialog>
<q-card-section
class="tipCardImage flex row justify-end"
:style="`background-image: url(${meeting.car.carImg})`"
>
<router-link
:to="`/user/meet/edit/${meeting.did}`"
style="text-decoration: none"
>
<q-icon
#click="fetchMeeting(meeting.did)"
name="fa-solid fa-pencil editNameIcon q-mb-sm q-ml-sm"
></q-icon>
</router-link>
<q-icon
name="fa-solid fa-trash editNameIcon q-mb-sm q-ml-sm"
#click="triggerDelete(meeting.did)"
></q-icon>
</q-card-section>
<q-card-section>
<div class="cardTitle">
<span>Encuentro Con</span> {{ truncateString(meeting.name, 30) }}
</div>
<div class="tipCardText">
<span>Agendado para el </span>
<p>{{ truncateString(meeting.date, 120) }}</p>
</div>
<div class="flex row justify-end">
<q-btn
#click="mapDialog = true"
class="text-white cardButton"
:class="{ cardButtonMobile: $q.screen.lt.md }"
>Ver UbicaciĆ³n</q-btn
>
</div>
</q-card-section>
</q-card>
</div>
And here is the code for the MeetMapComponent:
<template>
<div class="meetMapContainer">
<div ref="mapDiv" style="width: 100%; height: 500px" />
<h5 class="text-center text-white">{{ props.mapData.address }}</h5>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useAuthStore } from "stores/auth";
import { storeToRefs } from "pinia";
import { Loader } from "#googlemaps/js-api-loader";
const props = defineProps({
mapData: Object,
buyerName: String,
});
const GOOGLE_MAPS_API_KEY = "...";
const loader = new Loader({ apiKey: GOOGLE_MAPS_API_KEY });
const mapDiv = ref(null);
async function mapRender() {
await loader.load();
const map = new google.maps.Map(mapDiv.value, {
mapTypeId: "roadmap",
center: props.mapData.coordinates,
zoom: 13,
});
console.log(map);
new google.maps.Marker({
position: props.mapData.coordinates,
map,
title: `Encuentro con ${props.buyerName}`,
});
}
mapRender();
</script>
I will help you as much as I understand. You use the mapDialog variable to open the dialogue. But even if this variable is used in v-for, its reference does not change. For this reason, when you want to open a modal, all modals may be opened and the last one may appear because it is the last one opened. Please check the dom.
I think this method can solve the problem.
in script
const meetings = [
{
did: 'some value',
address: 'some address',
name: 'some name',
// add modal flag
showMapModal: false
}
]
template
<div
v-for="meeting in meetings"
:key="meeting.did"
class="col-12 col-md-6 col-lg-3 q-pa-md q-mx-xl"
>
<q-card class="my-card homeCard q-pa-md">
<q-dialog class="mapDialog flex column" v-model="meeting.showMapModal">
<MeetMapComponent
:key="componentKey"
:mapDiv="mapDiv"
:mapData="meeting.address"
:buyerName="meeting.name"
/>
</q-dialog>
</q-card>
</div>

Show different component conditionally if parent tag contains active class - Vue Bootstrap

I want to customize tab component's title part on Vue Bootstrap. If the tab is active, I want to show different type of tab item. Of course we can solve it by using css classes by setting .active class. But In my situation, I want to display a different component if that tab is active.
I believe this can be done using 'v-if' but I dont know how to get its tag's classes contains active class. If tab is active, I want to render a spinner component, else I only want to render name of that tab.
Do you have any idea?
<b-tabs fill>
<b-tab title="First">
<template #title v-if="active">
<b-spinner type="grow" small variant="success"></b-spinner> <b>Active Title</b>
</template>
<template #title v-else>
<b>Title</b>
</template>
</b-tab>
<b-tab title="Second">
<template #title v-if="active">
<b-spinner type="grow" small variant="success"></b-spinner> <b>Second Title</b>
</template>
<template #title v-else>
<b>Title</b>
</template>
</b-tab>
You could bind a property to <b-tabs> v-model, which will contain the index of the active tab. Then use this to check inside your v-if whether the tab is active or not.
new Vue({
el: '#app',
data() {
return {
currentTab: 0
}
}
});
<link href="https://unpkg.com/bootstrap#4.5.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://unpkg.com/vue#2.6.12/dist/vue.js"></script>
<script src="https://unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.js"></script>
<div id="app" class="p-4">
<b-tabs v-model="currentTab" fill>
<b-tab title="First">
<template #title v-if="currentTab === 0">
<b-spinner type="grow" small variant="success"></b-spinner> <b>Active Title</b>
</template> Tab 1 content
</b-tab>
<b-tab title="Second">
<template #title v-if="currentTab === 1">
<b-spinner type="grow" small variant="success"></b-spinner> <b>Second Title</b>
</template> Tab 2 content
</b-tab>
</b-tabs>
</div>

Bootstrap vue Tab component is not working when an Index is used to navigate trough the tabs

Why this very simple bootstrap vue script does not work, on pressing the toggle button it just does not increment the value tabIndex.
tabIndex starts in 0, user presses tabIndex, value still 0.
if tabIndex is removed from disabled attribute it will work but it will not follow the purpose I require.
Script can be seen here:
https://jsfiddle.net/Xarina/tkoyph4x/1/
<div id="app">
<b-card no-block>{{tabIndex}}
<b-tabs small card ref="tabs" v-model="tabIndex">
<b-tab title="General">
I'm the first fading tab
</b-tab>
<b-tab title="Edit profile" :disabled="tabIndex < 1">
I'm the second tab
</b-tab>
<b-tab title="Premium Plan" :disabled="tabIndex < 2">
Sibzamini!
</b-tab>
</b-tabs>
</b-card>
<button #click="tabIndex++">
Click to toggle disable
</button>
</div>
You can bind a custom class to each tab using title-item-class according to the tabIndex:
window.app = new Vue({
el: '#app',
data: () => ({ tabs: [], tabCounter: 0, disabled: false, tabIndex: 0 }),
});
.disabledTab .nav-link { pointer-events: none; color: #666666; }
<link type="text/css" rel="stylesheet" href="https://unpkg.com/bootstrap/dist/css/bootstrap.min.css"/>
<link type="text/css" rel="stylesheet" href="https://unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<script src="https://unpkg.com/babel-polyfi0ll#latest/dist/polyfill.min.js"></script>
<script src="https://unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.js"></script>
<div id="app">
<b-card no-block>{{tabIndex}}
<b-tabs small card ref="tabs" v-model="tabIndex">
<b-tab title="General">I'm the first fading tab</b-tab>
<b-tab title="Edit profile" :title-item-class="{ disabledTab: tabIndex < 1 }">I'm the second tab</b-tab>
<b-tab title="Premium Plan" :title-item-class="{ disabledTab: tabIndex < 2 }">Sibzamini!</b-tab>
</b-tabs>
</b-card>
<button #click="tabIndex++">Click to toggle disable</button>
</div>
This happens because you can't swap to a tab that's disabled. And the check happens before the index is disabled, meaning it will set the tab index back to 0.
You can get around this by using two data properties, one for disabling the tabs, and one for the <b-tabs> v-model. Then updating the property using for disabling first, and the v-model property in the a $nextTick callback.
In the example below i utilize a watcher to set the second property automatically when the other changes.
new Vue({
el: '#app',
data() {
return {
currentTab: 0,
disabledTabIndex: 0
}
},
watch: {
disabledTabIndex(newVal) {
this.$nextTick(() => {
this.currentTab = newVal;
})
}
}
});
<link href="https://unpkg.com/bootstrap#4.5.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.min.js"></script>
<script src="https://unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.js"></script>
<div id="app">
<b-card no-block>
Current tab: {{ currentTab }}
<b-tabs small card ref="tabs" v-model="currentTab">
<b-tab title="General">I'm the first fading tab</b-tab>
<b-tab title="Edit profile" :disabled="disabledTabIndex < 1">
I'm the second tab
</b-tab>
<b-tab title="Premium Plan" :disabled="disabledTabIndex < 2">
Sibzamini!
</b-tab>
</b-tabs>
</b-card>
<button #click="disabledTabIndex++">Click to toggle disable</button>
</div>

Trying to stop loading same component again and again

About the code
Using Laravel 5.8 and Vue.js components.
Issue Details
There are 3 tabs. When I click component 1, it loads. Then I click Component 2, It loads. So far everything is okay.
When I go back to Component 1, it loads the component again. Here the problem is that there is captcha which gets the response code again from server because the component is loading everytime on clicking the tab.
Can i stop loading the component loading after first time? I meant, I just want to reuse the html which was loaded.
Main Component
<template>
<div class="container-fluid">
<b-tabs pills card vertical>
<b-tab title="Main" active>
<profile></profile>
</b-tab>
<b-tab title="Comp 1" #click="setComponent('comp1')">
<component :is = "comp1"></component>
</b-tab>
<b-tab title="Comp 2" #click="setComponent('comp2')">
<component :is = "comp2"></component>
</b-tab>
</b-tabs>
</div>
</template>
<script>
export default {
methods: {
setComponent(name) {
if(name == "comp1") {
this.comp1 = name;
}
else {
this.comp2 = name;
}
}
},
data() {
return {
comp1: '', comp2: ''
}
}
}
</script>
Component - Comp 1
<template>
<div class="container">
Comp 1 rendered
<vue-recaptcha
:sitekey="My Site Key">
</vue-recaptcha>
</div>
</template>
Component - Comp 2
<template>
<div class="container">
Comp 2 Rendered
<vue-recaptcha
:sitekey="My Site Key">
</vue-recaptcha>
</div>
</template>
You can adds keep-alive, getting the components are not destroyed re-create
<div class="container-fluid">
<keep-alive>
<b-tabs pills card vertical>
<b-tab title="Main" active>
<profile></profile>
</b-tab>
<b-tab title="Comp 1" #click="setComponent('comp1')">
<component :is = "comp1"></component>
</b-tab>
<b-tab title="Comp 2" #click="setComponent('comp2')">
<component :is = "comp2"></component>
</b-tab>
</b-tabs>
</keep-alive>
</div>
After that you will have two new hooks in both child components, activated and deactivated.

bootstrap-vue v-tabs inside v-card renders 'undefined'

Using bootstrap-vue I have a working example using b-tabs and some other of it's components.
However, on one page (.vue) when I try to wrap the block within a tag the page always renders that card and its contents as 'undefined'
I use the sample from here.
Without the v-card I see:
and with it I see:
The code literally follows the sample referenced in the page above and in my main.ts I have the following
import {Card} from 'bootstrap-vue/es/components'
import { Button } from 'bootstrap-vue/es/components';
import { Collapse } from 'bootstrap-vue/es/components';
import { Alert } from 'bootstrap-vue/es/components';
import { Tabs } from 'bootstrap-vue/es/components';
Vue.use(Tabs);
Vue.use(Alert);
Vue.use(Collapse);
Vue.use(Button);
Vue.use(Card);
new Vue({
router,
store,
components: {
bCard: Card,
bButton: Button,
bCollapse: Collapse,
bAlert: Alert,
bTabs: Tabs,
pagination
},
render: h => h(App)
}).$mount("#app");
Can anyone point me in the right direction even to see how to isolate any underlying problem please?
EXTRA CODE ADDED BELOW
Vue (lang="ts") component which does not display correctly:
<template>
<div class="container-fluid">
<div class="row ml-1">
<h4>Complaint Reference: {{ complaint.ComplaintReference }}</h4>
<div class="col-12"><p>Enter the details of the complaint and click Save</p></div>
<div class="col-12">
<b-alert variant="success"
dismissible
:show="showDismissableAlert"
#dismissed="showDismissableAlert=false">
Your changes have been saved
</b-alert>
</div>
</div>
<div class="row">
<!-- the next line is a separate vue component that uses the same approach and which renders correctly -->
<tabs-view></tabs-view>
<div class="col-md-12">
<b-card no-body> <!-- THIS IS WHAT BREAKS! -->
<b-tabs card>
<b-tab title="Details" active>
<p> in first tab</p>
</b-tab>
<b-tab title="Contact">
<p> in second tab</p>
</b-tab>
<b-tab title="Description">
<p> in third tab</p>
</b-tab>
<b-tab title="Outcome">
<p> in final tab</p>
</b-tab>
</b-tabs>
</b-card> <!-- THIS IS WHAT BREAKS! -->
</div>
</div>
<div class="clearfix"></div>
</div>
</template>
<script lang="ts">
import {Component, Prop, Vue} from "vue-property-decorator";
import {Complaint} from "#/components/complaints/Complaint";
import TabsView from '../shared/Tabs.vue'
#Component({
components: {
tabsView: TabsView
}
})
export default class EditComplaintComponent extends Vue {
complaint: Complaint = new Complaint("");
baseUri: string = "api/Complaints/GetByReference/?id=";
showDismissableAlert: Boolean = false;
}
</script>
<style scoped>
</style>
and then another non-TS vue component consumed on the broken one which works correctly with b-tabs inside b-card
<template>
<div>
<b-card title="Card Title"
img-src="https://lorempixel.com/600/300/food/5/"
img-alt="Image"
img-top
tag="article"
style="max-width: 20rem;"
class="mb-2">
<!--<p class="card-text">-->
<!--Some quick example text to build on the card title and make up the bulk of the card's content.-->
<!--</p>-->
<b-button href="#" variant="primary">Go somewhere</b-button>
<b-card no-body>
<b-tabs card>
<b-tab title="first" active>
<br>I'm the first fading tab
</b-tab>
<b-tab title="second" >
<br>I'm the second tab content
</b-tab>
<b-tab title="disabled" disabled>
<br>Disabled tab!
</b-tab>
</b-tabs >
</b-card>
</b-card>
</div>
</template>
<script>
export default {
components: { }
}
</script>
<style scoped>
</style>
I ran into a similar issue with bootstrap-vue 2.21.2. Ended up being that I had a component that wasn't marked up with #Component. After adding it in all my components went from 'undefined' text to display the proper content. See https://github.com/bootstrap-vue/bootstrap-vue/issues/5985#issuecomment-765558938.
Upgrade to the latest version of BootstrapVue to get around this bug.
I had a parent component missing the #Component decorator (in my case it was App.vue) that was causing the undefined behaviour. Make sure that all components have this decorator applied.