Reputation: 83
In my vue app I have a page containing some tabs. I want to change/show the tabs based on different routes.
For this, I used this answer as a reference.
In general this is working fine! I can even change the tabs by swiping on mobile devices (thanks to the @change
Listener on the v-tabs-items
.
BUT: when clicking on the tab labels, the component loaded by the <router-view>
is getting mounted twice. When swiping, it's only mounted once.
The cause has to do something with the <router-view>
being inside the loop of <v-tab-item>
s.
If I place it outside of this loop, the child components get mounted correctly once. Unfortunatly I then can't change the tabs using swipe anymore, because the content is decoupled.
So: Is there any chance to have both functionalities (dynamic routed content and swipability)?
Thanks guys!
Vue:
<template>
<!-- [...] -->
<v-tabs centered="centered" grow v-model="activeTab">
<v-tab v-for="tab of tabs" :key="tab.id" :id="tab.id" :to="tab.route" exact>
<v-icon>{{ tab.icon }}</v-icon>
</v-tab>
<v-tabs-items v-model="activeTab" @change="updateRouter($event)">
<v-tab-item v-for="tab of tabs" :key="tab.id" :value="resolvePath(tab.route)" class="tab_content">
<!-- prevent loading multiple route-view instances -->
<router-view v-if="tab.route === activeTab" />
</v-tab-item>
</v-tabs-items>
</v-tabs>
<!-- [...] -->
</template>
<script lang="ts">
data: () => ({
activeTab: '',
tabs: [
{id: 'profile', icon: 'mdi-account', route: '/social/profile'},
{id: 'friends', icon: 'mdi-account-group', route: '/social/friends'},
{id: 'settings', icon: 'mdi-cogs', route: '/social/settings'},
]
}),
methods: {
updateRouter(tab:string) {
this.$router.push(tab)
}
},
</script>
Router:
{
path: "/social",
component: () => import("../views/Social.vue"),
meta: {
requiresAuth: true
},
children: [
{
path: "profile",
component: () => import("@/components/social/Profile.vue")
},
{
path: "friends",
component: () => import("@/components/social/Friendlist.vue")
},
{
path: "settings",
component: () => import("@/components/social/ProfileSettings.vue")
}
]
}
Upvotes: 0
Views: 1813
Reputation: 1900
I have a different answer to provide. @Estradiaz's answer wouldn't work for me because I have child views inside some of my tabs and redirects from the tab to the first child view on that tab. The end result is that I had lots of cases where $route.path didn't match tab.route because tab.route was the route for the tab and $route.path pointed one of the children of that tab.
I did find an alternative solution that I think is better and more generally applicable. But before I get into that, I want to fully explain what is happening.
Because we are using the :to attribute of the , we are letting the router handle all the selection and display of the component. This means that the route changes before the infrastructure has a chance to react. For instance, if we had 2 tabs and I am showing tab 1, then what is being displayed is this (you can see this in your html)
<v-tabs-items>
<v-tab-item><router-view>tab 1 component</router-view></v-tab-item>
<--- tab 2 commented out --->
</v-tabs-items>
Then when we click on tab 2, what happens is this:
<v-tabs-items>
<v-tab-item><router-view>tab 2 component</router-view></v-tab-item>
<--- tab 2 commented out --->
</v-tabs-items>
Then some time passes and the code gets to update and you transition to this:
<v-tabs-items>
<--- tab 1 commented out --->
<v-tab-item><router-view>tab 2 component</router-view></v-tab-item>
</v-tabs-items>
But because the v-if changed, we are destroying the for tab 1 and with it the new tab 2 component, only to have the for tab 2 create a new instance of the tab 2 component.
There seems to be nothing on the infrastructure that can be used to know that the tab showing and the component in the are mismatched. @Estradiaz's solution is good for simple tab components, but it is not reliable.
Instead of trying to block the mismatch case, I am embracing it with this solution and using the keep-alive infrastruture to "reuse" the when we blow away tab 1's . Here is what that looks like with the original markup given in the question:
<template>
<!-- [...] -->
<v-tabs centered="centered" grow v-model="activeTab">
<v-tab v-for="tab of tabs" :key="tab.id" :id="tab.id" :to="tab.route" exact>
<v-icon>{{ tab.icon }}</v-icon>
</v-tab>
<v-tabs-items v-model="activeTab" @change="updateRouter($event)">
<v-tab-item v-for="tab of tabs" :key="tab.id" :value="resolvePath(tab.route)" class="tab_content">
<div v-if="tab.route === activeTab">
<keep-alive>
<router-view key="1" />
</keep-alive>
</div>
</v-tab-item>
</v-tabs-items>
</v-tabs>
<!-- [...] -->
</template>
Now what happens is the following when you click on tab 2:
<v-tabs-items>
<v-tab-item><router-view>tab 2 component</router-view></v-tab-item>
<--- tab 2 commented out --->
</v-tabs-items>
and the router-view now creates the tab 2 component and displays it. Some time passes and the v-tab infrastructure updates and the markup changes to this:
<v-tabs-items>
<--- tab 1 commented out --->
<v-tab-item><router-view>tab 2 component</router-view></v-tab-item>
</v-tabs-items>
but now when the original is removed from the dom, it isn't deleted, instead, it is cached. And when the new is created, it comes from the cache ready to go with the already constructed tab 2 component.
In my case, this worked perfectly to eliminate the double mounting that was happening. And it is probably a little faster since it doesn't have to recreate the every time you switch.
Upvotes: 0
Reputation: 3563
can this behaviour describe the order of things hapenning? @change updates the route post activeTab, clicking the tab updates the route and then activeTab? thus the router-view is on the next view before the tab-view updated thus it shows two different router-views on the same path.
to fix this just change
<router-view v-if="tab.route === activeTab" />
to
<router-view v-if="tab.route === $route.fullPath && tab.route === activeTab" />
or
<router-view v-if="tab.route === $route.path && tab.route === activeTab" />
Upvotes: 1