简体   繁体   中英

Stop or reverse highlighting of clicked Vuetify v-tab item

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 ie 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 ie 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 ie the next tab. In this case the $nextTick hack is unnecessary.

Thanks to the answer by @ParaBolt and his CodePen, I got this working. My answer differed slightly from his because my currentTab is stored in state, not the component, and I also used a filter method to get the component value, but the structure is the same.

Here are the important parts of the mixin:

 data: () => ({
    ..
    previousTab: '',
  }),
  methods: {
    ...mapActions('app', ['setUnsaved']),
    async tabChange(newTab) {
      this.setCurrentTab(newTab);
      // We need to get the corresponding component value from the tabs array.
      const newComponent = this.tabList.filter(tab => tab.link === newTab)[0].component;
      // Stash these in case we need then in unsavedModalConfirm()
      this.destTabComponent = newComponent;
      this.destTabLink = newTab;
      // Check to see if getUnsaved === true; if it is,
      // set variable to display warning modal.
      await this.$nextTick();
      if (this.getUnsaved) {
        this.setCurrentTab(this.previousTab);
        this.showUnsaved = true;
      } else {
        // There is no unsaved content, so continue to the desired tab.
        this.setCurrentTab(newTab);
        this.$router.push({ name: newComponent });
      }
    },
    unsavedModalConfirm(confirm) {
      if (confirm) {
        // User selected Continue from modal.
        this.setCurrentTab(this.destTabLink);
        this.$router.push({ name: this.destTabComponent });
        this.setUnsaved(false);
      } else {
        // User selected Cancel
        this.setCurrentTab(this.previousTab);
      }
      this.showUnsaved = false;
    },
  },
  mounted() {
    this.previousTab = this.currentTab;
  },

Whenever I cancel from the modal, it just stays where it is, and if I choose to continue, it goes on to the desired tab.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM