I have a VueJS/Vuetify application that has tab bars using v-tabs
/v-tab
components to navigate between pages. I have implemented code using the click
event in the v-tab
element that checks to make sure there is no unsaved content when the user clicks on another tab, and if there is, displays a modal using v-dialog
to alert the user. If the user chooses to continue, it continues on to the desired tab/component. However, if the user selects Cancel
in the modal, the page is left where it was.
Here is the tabs component:
<template>
<div>
<!-- Tabs -->
<v-tabs
color="secondary"
:value="currentTab"
>
<v-tab
v-for="(tab, i) in userTabs"
:key="i"
:href="tab.href"
@click="tabClick(tab.component, tab.link)"
:disabled="isDisabled(tab)"
>
{{ tab.title }}
</v-tab>
</v-tabs>
<BaseConfirmModal
:value="showUnsaved"
:title="unsavedContentTitle"
:text="unsavedContentText"
declineText="Cancel"
@clicked="unsavedModalConfirm"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import baseTabMixin from '@/components/mixins/workspace/baseTabMixin';
export default {
name: 'UserTabs',
data: () => ({
userTabs: [
{
title: 'General Info',
href: '#tab-general',
link: 'tab-general',
component: 'UserEdit',
},
{
title: 'Enrollments',
href: '#tab-enrollments',
link: 'tab-enrollments',
component: 'UserEnrollmentEdit',
},
{
title: 'Alerts',
href: '#tab-alerts',
link: 'tab-alerts',
component: 'UserAlertEdit',
},
{
title: 'Devices',
href: '#tab-devices',
link: 'tab-devices',
component: 'UserDeviceEdit',
},
],
}),
computed: {
...mapGetters('app', ['getStickyTenant', 'roleAtLeastTa', 'getUnsaved']),
...mapGetters('users', ['getCurrent']),
...mapGetters('tabs', {
currentTab: 'getSelected',
}),
},
methods: {
...mapActions('tabs', {
setCurrentTab: 'setSelected',
}),
isDisabled(item) {
if (item.component === 'UserEdit') {
return false;
}
if (item.component === 'UserDeviceEdit' && !this.roleAtLeastTa) {
return true;
}
return !this.getCurrent.userId;
},
},
mixins: [baseTabMixin],
};
</script>
and the referenced baseTabMixin
:
import { mapGetters, mapActions } from 'vuex';
const baseTabMixin = {
data: () => ({
showUnsaved: false,
unsavedContentTitle: 'Unsaved Changes',
unsavedContentText: 'You have made changes to this page that are not saved. Do you wish to continue?',
destTabComponent: '',
destTabLink: '',
}),
components: {
BaseConfirmModal: () => import('@/components/base/BaseConfirmModal'),
},
computed: {
...mapGetters('app', ['getUnsaved']),
},
methods: {
...mapActions('app', ['setUnsaved']),
tabClick(component, tab) {
// Check to see if getUnsaved === true; if it is,
// set variable to display warning modal.
if (this.getUnsaved) {
this.showUnsaved = true;
} else {
// There is no unsaved content, so continue to the desired tab.
this.destTabComponent = component;
this.destTabLink = tab;
this.setCurrentTab(tab);
this.$router.push({ name: component });
}
},
unsavedModalConfirm(confirm) {
if (confirm) {
this.setCurrentTab(this.destTabLink);
this.$router.push({ name: this.destTabComponent });
}
this.showUnsaved = false;
},
},
};
export default baseTabMixin;
The problem has to do with the tab item highlighting. When the new tab is clicked, the slider moves to the new tab and the new tab title is bolded before the click event (tabClick()
in this case) is called. When I select Cancel
in my modal, it leaves the page where it is (as expected), but the clicked tab is still highlighted, both with the slider underneath and the bolder text. Since that all happens before my click handler is even called, is there way to either a) stop the highlighting from happening before the click event is called, or b) reverse the highlighting back to the current tab?
Link to working pen.
Note the key parts of the code :-
<v-tabs :value="currentTab" @change="onTabChange">
<v-tab v-for="(tab, i) in tabs" :key="i">{{tab.title}}</v-tab>
</v-tabs>
async onTabChange(clickedTab)
{
this.currentTab = clickedTab;
await this.$nextTick();
if (!this.allowTabChange) this.currentTab = this.previousTab;
else this.previousTab = this.currentTab;
}
v-tabs
component uses an internal model to maintain it's state i.e. which tab is currently active. Using the :value
attribute, we can set an initial active tab. When the user clicks on a different tab, the internal model is updated leading to the clicked tab getting highlighted. v-tabs
also emits a change event to notify the parent component of this change. We need to listen for this event and manage the :value
variable by ourselves to maintain state in the parent. Since v-model
is an abstraction over :value
and @change
, we could use that as well.
To listen for this change event, add @change
to v-tabs
. You can shift the entire logic that you have at @click
to @change
resulting in a cleaner code.
To prevent selection of the next tab, we need to use some tricky code since the internal model of v-tabs
is not accessible to us. We need to allow currentTab
to change temporarily to the clicked tab and then we reset it to previousTab
in $nextTick
i.e. after the current batch of updates has been picked up by Vue. The highlight beneath the tabs itself occurs after $nextTick
so it will not change since we have reset the :value
to previousTab
by then.
If you are ok with letting the highlight temporarily shift to the new tab, then all you need to do is update currentTab
to the index of next tab and the highlight will shift over as normal. Then depending on the decision made by user, you can set currentTab
to previousTab
to revert the change or you can update previousTab
with the new value of currentTab
i.e. the next tab. In this case the $nextTick
hack is unnecessary.