How to store state of tabs based on route? - vue.js

Is it possible to store different state of a <v-tab> component based on the route i'm currently on? What i'm basically trying to achieve is, have a single component like the code below that i use in several different routes, but have it remember the tab i'm currently at depending on the route.
<v-layout justify-space-around row>
<v-tab v-for="tab in navbarTabs" :key="tab.id">{{ tab.name }}</v-tab>
</v-layout>
data() {
return {
navbarTabs: [
{
id: 0,
name: "Home"
},
{
id: 1,
name: "Schedule"
},
{
id: 2,
name: "Tech card"
},
{
id: 3,
name: "Specifications"
},
{
id: 4,
name: "Control card"
},
{
id: 5,
name: "Packing"
}
]
}
}
I previously managed to achieve the desired result by having a few identical <v-tab> components that i visualised conditionally based on current route with v-if, but that solutions seems a bit crude and not scalable at all.
Can someone give me some pointers on how to achieve that? Thanks in advance!

The v-tabs component has a value prop which you can use to open a tab with a given key. And the v-tab component has a to prop which you can use to set the route of a tab. So essentially, you can use Vue Router params to select a tab and you can set the route using the v-tab's to prop.
You can define your route like this to pass the selectedTab param to the component:
routes: [
{ path: '/tabs/:selectedTab', component: TabsComponent, props: true },
]
And your TabsComponent can look something like this:
<template>
<v-layout justify-space-around row>
<v-tabs v-model="selectedTab">
<v-tab
v-for="tab in navbarTabs"
:key="tab.id"
:to=`/tabs/${tab.id}`
>{{ tab.name }}</v-tab>
</v-layout>
</template>
<script>
export default {
props: ['selectedTab'],
}
</script>

Related

Reuse existing tab in VueJS for routing to child and grandchild routes with Vue Router

I have a VueJS application where I want to reuse the current tab when routing to grand-child routes. The child component is what is defined as the initial path for the tab and I want to remain on that tab when I route to the GrandChild route. Currently, when I use Router-link to redirect, it paints the tab as the active tab but displays the child component instead of the grandchild. My router view <router-view></router-view> includes the tabs as well as a header object. How could I setup a router-link to redirect to the grandchild component?
routes.js
{
name: "parent",
path: "/parent/:parentId(\\d+)",
props: function(route) {
let parentId = null;
if (route.constructor === Array) {
parentId = route[0].params.parentId;
} else {
parentId = route.params.parentId;
}
return {
parentId: Number(parentId)
};
},
component: Parent,
children: [
{
name: "child",
path: "child",
component: Child,
children: [
{
name: "grandchild",
path: "grandchild/:grandchildId(\\d+)",
props: function(route) {
return { grandchildId: Number(route.params.grandchildId) };
},
component: GrandChild
}
]
}
The tabs are dynamically created based on an Array passed into the Tabs component.
Tabs.vue
<v-tabs background-color="primary" dark>
<v-tab v-for="tab in tabs"
:key="tab.id"
replace
:to="{
path: tab.path
}"
>{{tab.name}}</v-tab
>
</v-tabs>
</template>
<script>
export default {
props: {
tabs: {
type: Array,
required: true
}
}
}
</script>
Here is a simplified version of the parent component. Its really just displays some basic information and then paints the tabs.
Parent.vue
<template>
<v-container>
<v-row>
<v-col sm4>
<v-card>
<v-row no-gutters>
<v-col>
HEADER HERE
</v-col>
</v-row>
</v-card>
<atomic-tabs :tabs="tabs"/>
<router-view></router-view>
</v-col>
</v-row>
</v-container>
</template>
<script>
import axios from "axios";
import Tabs from "#/components/Tabs";
export default {
components: {
"tabs": Tabs
},
props: {
parentId: {
type: Number
}
},
data() {
return {
pageName: "Parent",
tabs: [
{ id: 1, name: "child", path: `/parent/${this.parentId}/child`}
]
};
}
</script>
I think you have a pretty straight forward issue - you are nesting twice.
Option 1
You'd need another <router-view/> inside the child component to be able to see the grandchild with the current route configuration (and then hide the child content with v-show or something like that)
Option 2
Alternatively you could change the way you structure the child and grandchild routes, don't nest them:
{
name: "parent",
path: "/parent/:parentId(\\d+)",
props: function(route) {
let parentId = null;
if (route.constructor === Array) {
parentId = route[0].params.parentId;
} else {
parentId = route.params.parentId;
}
return {
parentId: Number(parentId)
};
},
component: Parent,
children: [
{
name: "child",
path: "child",
component: Child,
},
{
name: "grandchild",
path: "child/grandchild/:grandchildId(\\d+)",
props: function(route) {
return { grandchildId: Number(route.params.grandchildId) };
},
component: GrandChild
}
}
either way, the router link would be:
:to="{name:'grandchild', params:{grandchildId:'134551}}"
or
:to="'child/grandchild/${'123431')'"

Is it possible to use dynamic scoped slots to override column values inside <v-data-table>?

I'm trying to create a reusable table component that utilizes Vuetify's v-data-table component in order to group common aspects such as a search bar, table actions (refresh, create, etc.) and other features that all of my tables will have. However, I'm running into issues with implementing dynamic, scoped slots inside the table component to account for custom columns. Think of columns like actions, formatted ISO strings, etc.
Here's a simplified example of what I'm trying currently. In the example, I am passing the array customColumns to CustomDataTable.vue as a prop. customColumns has one element with two properties. The slotName property specifies the name of the slot that I'd like to reference in the parent component. The itemValue property specifies the header value that CustomDataTable.vue should override and replace with a scoped slot. The scoped slot is then used in the parent component to correctly format the date in the 'Created At' column.
Parent Component that is implementing the table component:
<template>
<custom-data-table
:items="items"
:headers="headers"
:customColumns="customColumns"
>
<template v-slot:custom-column="slotProps">
<span>{{ formatDate(slotProps.item.createdAt) }}</span>
</template>
</custom-data-table>
</template>
<script>
import CustomDataTableVue from '#/components/table/CustomDataTable.vue'
export default {
data: () => ({
items: [
{
id: 0,
createdAt: new Date().toISOString(),
...
},
...
],
headers: [
{
text: 'Created At',
value: 'createdAt',
sortable: true
},
...
],
customColumns: [
{
slotName: 'custom-column',
itemValue: 'createdAt'
}
]
})
}
</script>
CustomDataTable.vue
<template>
<v-data-table
:items="items"
:headers="headers"
>
<template v-for="column in customColumns" v-slot:item[column.itemValue]="{ item }">
<slot :name="column.slotName" :item="item"/>
</template>
</v-data-table>
</template>
<script>
export default {
name: 'custom-data-table',
props: {
items: {
type: Array,
required: true
},
headers: {
type: Array,
required: true
},
customColumns: {
type: Array
}
}
}
</script>
Is there a way to achieve this? The example does not override the column values and just displays the createdAt ISO string unformatted. I believe the problem might be coming from how I'm assigning the template's slot in CustomDataTable.vue, but I'm sure how else you could dynamically specify a template's slot. Any ideas?
I think the syntax for dynamic slots should be:
<template
v-for="column in customColumns"
v-slot:[`item.${column.itemValue}`]="{ item }"
>
<slot :name="column.slotName" :item="item"/>
</template>

How to prevent parent component from reloading when changing a parameterised child component in Vue js

I have a page where a ClientPortfolio (parent component) containing a list of Securities (child component) are loaded in a v-data-table list.
The issue I have is that ClientPortfolio is fully reloaded every time I click on a security in the list causing the entire list to be refreshed causing scroll and selected class to reset, as well as unncessary performance overhead.
I have looked at the documentation of Vue and nothing seems to point out how to only refresh a child component when it has parameters, it looks like the parent component is being refreshed as the route is changing every time a security is selected, despite expecting that Vue would know that only sub (nested route) is changing hence need to only reload the child component
The closest answer I got was explained on https://github.com/vuejs/vue-router/issues/230 which does not explain in the code how to achieve this.
routes.js:
routes: [
{
path: '/client/:clientno/portfolios/:portfolioNo',
component: ClientPortfolios,
children: [
{ path: 'security/:securityNo', component: Security }
]
},
]
Router link in ClientPortfolios.vue:
<router-link tag="tr" style="cursor:pointer"
:to="`/client/${$route.params.clientno}/portfolios/${selectedPortfolioSequenceNo}/security/${props.item.SecurityNo}-${props.item.SequenceNo}`"
:key="props.item.SecurityNo+props.item.SequenceNo">
</router-link>
Router view (for Security component) in ClientPortfolios.vue:
<v-flex xs10 ml-2>
<v-layout>
<router-view :key="$route.fullPath"></router-view>
</v-layout>
</v-flex>
Any hint on how to prevent parent from getting reloaded is appreciated.
EDIT: Trying to get closer to the issue, I notice that the "Key" attr in ClientPortfolios changes (as shown in the Vue debug window above) whenever I change the Security, could that be the reason? Is there a way to assign a key to ClientPortfolios component although its not a child one? Or a way to not update its key when navigating to different securities?
UPDATE: Full code
ClientPortfolios.vue
<template>
<v-layout row fill-height>
<v-flex xs2>
<v-layout column class="ma-0 pa-0 elevation-1">
<v-flex>
<v-select v-model="selectedPortfolioSequenceNo" :items="clientPortfolios" box label="Portfolio"
item-text="SequenceNo" item-value="SequenceNo" v-on:change="changePortfolio">
</v-select>
</v-flex>
<v-data-table disable-initial-sort :items="securities" item-key="Id" hide-headers hide-actions
style="overflow-y: auto;display:block;height: calc(100vh - 135px);">
<template slot="items" slot-scope="props">
<router-link tag="tr" style="cursor:pointer"
:to="{ name: 'Security', params: { securityNo: props.item.SecurityNo+'-'+props.item.SequenceNo } }"
>
</router-link>
</template>
<template v-slot:no-data>
<v-flex class="text-xs-center">
No securities found
</v-flex>
</template>
</v-data-table>
</v-layout>
</v-flex>
<v-flex xs10 ml-2>
<v-layout>
<keep-alive>
<router-view></router-view>
</keep-alive>
</v-layout>
</v-flex>
</v-layout>
</template>
<script>
import Security from '#/components/Security'
export default {
components: {
security: Security
},
data () {
return {
portfoliosLoading: false,
selectedPortfolioSequenceNo: this.$route.params.portfolioNo,
selectedPortfolio: null,
securityNo: this.$route.params.securityNo
}
},
computed: {
clientPortfolios () {
return this.$store.state.ClientPortfolios
},
securities () {
if (this.clientPortfolios == null || this.clientPortfolios.length < 1) {
return []
}
let self = this
this.selectedPortfolio = global.jQuery.grep(this.clientPortfolios, function (portfolio, i) {
return portfolio.SequenceNo === self.selectedPortfolioSequenceNo
})[0]
return this.selectedPortfolio.Securities
}
},
mounted () {
this.getClientPortfolios()
},
activated () {
},
methods: {
changePortfolio () {
this.$router.push({
path: '/client/' + this.$route.params.clientno + '/portfolios/' + this.selectedPortfolioSequenceNo
})
},
getClientPortfolios: function () {
this.portfoliosLoading = true
let self = this
this.$store.dispatch('getClientPortfolios', {
clientNo: this.$route.params.clientno
}).then(function (serverResponse) {
self.portfoliosLoading = false
})
}
}
}
</script>
Security.vue
<template>
<v-flex>
<v-layout class="screen-header">
<v-flex class="screen-title">Security Details </v-flex>
</v-layout>
<v-divider></v-divider>
<v-layout align-center justify-space-between row class="contents-placeholder" mb-3 pa-2>
<v-layout column>
<v-flex class="form-group" id="security-portfolio-selector">
<label class="screen-label">Sequence</label>
<span class="screen-value">{{security.SequenceNo}}</span>
</v-flex>
<v-flex class="form-group">
<label class="screen-label">Security</label>
<span class="screen-value">{{security.SecurityNo}}-{{security.SequenceNo}}</span>
</v-flex>
<v-flex class="form-group">
<label class="screen-label">Status</label>
<span class="screen-value-code" v-if="security.Status !== ''">{{security.Status}}</span>
</v-flex>
</v-layout>
</v-layout>
</v-flex>
</template>
<script>
export default {
props: ['securityNo'],
data () {
return {
clientNo: this.$route.params.clientno,
securityDetailsLoading: false
}
},
computed: {
security () {
return this.$store.state.SecurityDetails
}
},
created () {
if (this.securityNo.length > 1) {
this.getSecurityDetails()
}
},
methods: {
getSecurityDetails: function () {
let self = this
this.securityDetailsLoading = true
this.$store.dispatch('getSecurityDetails', {
securityNo: this.securityNo,
clientNo: this.clientNo
}).then(function (serverResponse) {
self.securityDetailsLoading = false
})
}
}
}
</script>
router.js
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
component: Dashboard
},
{
path: '/client/:clientno/details',
component: Client,
props: true
},
{
path: '/client/:clientno/portfolios/:portfolioNo',
component: ClientPortfolios,
name: 'ClientPortfolios',
children: [
{ path: 'security/:securityNo',
component: Security,
name: 'Security'
}
]
}
]
})
UPDATE:
Just to update this as it’s been a while, I finally got to find out what the problem is, which is what #matpie indicated elsewhere, I have found out that my App.vue is the culprit where there is a :key add to the very root of the application: <router-view :key="$route.fullPath" /> this was a template I used from somewhere but never had to look at as it was "working", after removing the key, all is working as it should, marking matpie answer accepted.
Preventing component reload is the default behavior in Vue.js. Vue's reactivity system automatically maps property dependencies and only performs the minimal amount of work to ensure the DOM is current.
By using a :key attribute anywhere, you are telling Vue.js that this element or component should only match when the keys match. If the keys don't match, the old one will be destroyed and a new one created.
It looks like you're also pulling in route parameters on the data object (Security.vue). Those will not update when the route parameters change, you should pull them in to a computed property so that they will always stay up-to-date.
export default {
computed: {
clientNo: (vm) => vm.$route.params.clientno,
}
}
That will ensure that clientNo always matches what is found in the router, regardless of whether Vue decides to re-use this component instance. If you need to perform other side-effects when clientNo changes, you can add a watcher:
vm.$watch("clientNo", (clientNo) => { /* ... */ })
Could you please check again after removing the local registration of the security component? As it's not needed because this is being handled by the vue router itself.
components: { // delete this code
security: Security
},
Instead of using router here. Declare two variable at root level for selected security and portfolio,
list the securities based on the selected portfolio.
on selecting a security from displayed securities, update the root variable using,
this.$root.selectedSecurityId = id;
you can have watch at security component level.
In root,
<security selectedid="selectedSecurityId" />
In component security,
....
watch:{
selectedid:function(){
//fetch info and show
}
}
...
the components will be look like following,
<portfolio>
//active. list goes here
</portfolio>
........
<security selectedid="selectedSecurityId">
//info goes here
</security>
Above approach will help to avoid routers. hope this will help.
I had a similar issue once. IMO it was caused by path string parsing.
Try to set a name for your route. And replace your router-link to param with an object.
And remove router-view :key prop. It doesn't need to be there. It is used to force component update when a route changes. It is usually a sign of bad code. Your component (Security) should react to route params update. Not the parent component force it to.
So, try to change your code to:
routes: [
{
path: '/client/:clientno/portfolios/:portfolioNo',
component: ClientPortfolios,
name: "ClientPortfoliosName", // it can be anything you want. It`s just an alias for internal use.
children: [
{
path: 'security/:securityNo',
name: "PortfolioSecurities", // anyway, consider setting route names as good practice
component: Security
}
]
},
]
<router-link tag="tr" style="cursor:pointer"
:to="{ name: 'PortfolioSecurities', params: { clientno: $route.params.clientno, portfolioNo: selectedPortfolioSequenceNo, securityNo: props.item.SecurityNo+'-'+props.item.SequenceNo } }"
:key="props.item.SecurityNo+props.item.SequenceNo">
</router-link>
And it should work.
P.S. In your router-link you shall point to the route you want to navigate to. In this case PortfolioSecurities

Vue - passing params to route as props is undefined

I am passing params to a named route from a component:
<v-list-tile
:key="team.logo_url"
:to="{name: 'team', params: {
id: team.id,
name: team.name
}}"
avatar
>
The route is sett up likes so:
{
path: "/team",
name: "team",
component: TeamInfo,
props: {
id: true,
name: true
}
}
But the component does not render the props when referenced:
<template>
<v-container>
<p>{{ id }}</p>
</v-container>
</template>
<script>
import TeamService from '#/services/TeamService';
export default {
props: ['id', 'name'],
data: () => ({
players: [],
games: []
}),
mounted() {
console.log(this.id);
}
}
</script>
The log in the mounted method returns undefined.
However when I look in Vue dev-tools at the TeamInfo component I can see that both props are undefined but the params are populated.
I would like to be able to use the props in the component and also populate the URL with the team ID.
You have to use the boolean mode for to pass the params to both the URL and props. You also have to define the parameter inside the path to be able to access it. Here is an example that shows how to use it.
{
name: "team",
path: "/team/:id",
component: TeamInfo,
props: true,
}
https://router.vuejs.org/guide/essentials/passing-props.html#boolean-mode

Vue - Translation in single file component

I'm using vue-i18n for my translations and it works great! But when I'm using the this.$t() function inside the data function of a single file component the translation is not working.
<template>
<VFooter
app
height="auto"
color="secondary">
<VLayout
justify-center
row
wrap>
<VBtn
v-for="link in links"
:key="link.name"
:to="{ name: link.name }"
flat
round
active-class>
{{ link.label }}
</VBtn>
<VFlex
py-3
text-xs-center
xs12>
©{{ currentYear }} — <strong>{{ $t('site_name') }}</strong>
</VFlex>
</VLayout>
</VFooter>
</template>
<script>
export default {
name: 'TheSiteFooter',
data() {
return {
links: [
{ name: 'what-is-pinshop', label: this.$t('footer.what_is_pinshop') },
{ name: 'contact-us', label: this.$t('footer.contact_us') },
{ name: 'cookie-policy', label: this.$t('footer.cookie_policy') },
{ name: 'privacy-policy', label: this.$t('footer.privacy_policy') },
{ name: 'terms-and-conditions', label: this.$t('footer.terms_and_conditions') },
],
};
},
computed: {
currentYear() {
return new Date().getFullYear();
},
},
};
</script>
But, if I instead change data with only the key of translation and then in my template use e.g {{ $t('footer.what_is_pinshop') }} the translation loaded is correct. Why does this happen? I'm using the beforeEnter router guard to change the translation. I have followed this example.
UPDATE
If I put links as a computed property the translation works. So why it does not happen only in data property? I also tried with this.$i18n.t(), but nothing
This is, because of the vue lifecycle. Push your link data into the array by using the created hook. Keep you data(model) clean and do not call functions in it. You call this up before all events and reactivity mechanisms have ever been registered.
lifecycle: https://v2.vuejs.org/v2/guide/instance.html
if you're interested how it works: https://github.com/kazupon/vue-i18n/tree/dev/src
UPDATE
I just showered and thought again about it. In depth this is because of the reactivity mechanism. You initialize your data with a function and vue cannot detect if the returned value has changed. See how it works: https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty in vue 3 this is replaced by https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Proxy