3

Trying to make a Vuetify Tab component with nested routes for the tab panels, based off a dynamic route.

Starting HTML of

<div id="app">
  <v-app>
    <v-main>
      <v-container>
        <router-link to="/parent/123">Go To Parent</router-link>
        <router-view></router-view>
      </v-container>
    </v-main>
  </v-app>
</div>

with components of

const Parent = {
template: '<div><h1>Hello from parent <em>{{obj.name}}</em></h1>' +
    '<v-tabs>' +
        '<v-tab to="child1">child1</v-tab>' +
        '<v-tab to="child2">child2</v-tab>' +
        '<v-tab to="child3">child3</v-tab>' +   
    '</v-tabs>' +    
    '<router-view></router-view>' +
    '</div>',
    props: { obj : {type: Object, required: true}}
};

const Child1 = {
  template: '<h3>Child1</h3>'
};
const Child2 = {
  template: '<h3>Child2</h3>'
};
const Child3 = {
  template: '<h3>Child3</h3>'
};

and a router configuration of

const routes = [{
    path: '/parent/:id',
    component: Parent,
    props: true,    
    beforeEnter: function (routeTo, routeFrom, next) {
        routeTo.params.obj = mockObj;
      next();
    },
    children:[
    {    
      path: 'child1',      
      component: Child1    
    },
    {
      path: 'child2',
      component: Child2
    },
    {
      path: 'child3',
      component: Child3
     }    
    ]    
  }

does all sort of work, to the extent that clicking on each tab does navigate to child route correctly (as in if I remove the prop of [obj] all works, although I couldn't get the fiddle to reproduce this, but I can locally. The fiddle is not passing through the dynamic route param in the child routes and I don't know why) but as soon as I put the parent obj prop in they all error with vue.runtime.esm.js:639 [Vue warn]: Missing required prop: "obj" found in Parent when navigating to the children.

The obj is just used as a placeholder example, in the actual implementation obj is pulled from an API in the route guard beforeEnter and passed into the props. In this example its just var mockObj = {name: "Steve"}, so the parent component says 'Hello from parent steve'

I'm fairly new to Vue.js and Vuetify and just don't understand whats going on here, but it appears when navigating to the child routes its running the parent!?

Fiddle here

Michal Levý
  • 33,064
  • 4
  • 68
  • 86
OJay
  • 4,763
  • 3
  • 26
  • 47
  • What do you want to achieve exactly? As far as I know Vuetify's tabs component is not meant to redirect to another route. Rather it just loads new content in the current page. Like [this example](https://codepen.io/adelriosantiago/pen/GRZwZoN). – adelriosantiago Sep 21 '20 at 01:08
  • Objective is for the tabs to be child routes and make an API call for each tab. For instance, if you have a 'User' page, the first tab will be the simple details for the user (firstname,lastname etc), but the user also partipates in other relationships, for instance groups, that would be another tab, so when clicking on the tab 'groups' under the user would show all of the groups for that user. According to the vuetify [documentation](https://vuetifyjs.com/en/components/tabs/#api) v-tab component has a 'to' prop which is what I am trying to use. – OJay Sep 21 '20 at 01:30
  • as mentioned in this SO [article](https://stackoverflow.com/questions/49721710/how-to-use-vuetify-tabs-with-vue-router?rq=1) – OJay Sep 21 '20 at 01:30

2 Answers2

4

First because v-tab is using standard router-link, everything below applies to any app using Vue router and we can ignore Vuetify

All my observations and suggestions are demonstrated in the fiddle I created (based on yours)

Vue router and relative links

Relative links are sort of broken (you can find multiple issues like this). If you hover over the tabs in your example, you will see URL ending like this #/parent/child2 with the :id dynamic section completely omitted.

You can sort of fix it by changing your "Go To Parent" link from <router-link to="parent/123">Go To Parent</router-link> to <router-link to="parent/123/">Go To Parent</router-link> (notice the trailing slash) but that is not very reliable (user can type URL into browser bar a you app is suddenly broken)

Way to fix it properly is either by using absolute paths (<v-tab :to="{ path: '/parent/${$route.params.id}/child1' }">child1</v-tab>) (used also in official example of Nested routes feature) or give a name to your routes and use link like this <v-tab :to="{ name:'child1' }">child1</v-tab>

Nested routes vs components

Even it may seem unnecessary in this case, when navigating to child route, router will process whole route including parent. Because parent component doesn't change, router will reuse the existing Parent component instance (and not trigger beforeEnter guard) but it will still re-render the component and refresh its props. You can check this by creating beforeRouteUpdate hook on your Parent component (docs). And because beforeEnter guard is not executed, there is nothing to pass into the Parent obj prop.

You can sort of fix it by reassigning the param inside the beforeRouteUpdate hook of Parent component:

beforeRouteUpdate(to, from, next) {
    // HackyHack
    if(from.params.obj)
      to.params.obj = from.params.obj
      
    next()
  }

...but in my eyes its more hack than a real solution (and you should probably also check id param is same value)

Vue router and object params

Even tho it is technically possible to pass objects with Router (and therefore use it to share data between components), I do not recommend it. I'v already answered multiple questions here on SO of people being bitten when trying to pass objects as route params. In general params are good for things which can be fully encoded into URL iself (numbers, string etc). Passing "hidden" params (not just objects) lead to a problems with browser navigation (back button) or users copy pasting urls.

When considering solutions to share a state between components of your app, I recommend taking a look at Vuex - Vue official solution to this problem...

How to change your app to use Vuex

  1. Remove the beforeEnter guard and move the "fetch" logic into Parent component - see Router documentation Fetching After Navigation. Store result from API in Vuex. On every route change (change of :id) check the store - if the data for current :id are there, no need to fetch otherwise clear the store and data for new :id must be fetched
  2. Change your Child components to take data not from props but directly from Vuex
Michal Levý
  • 33,064
  • 4
  • 68
  • 86
0

For nested router-view, you need to enter the parent props again to route the child component along with parent.So you have to use routes.beforeEach hook instead of beforeEnter. As routes.beforeEach is called for rendering every child component. But beforeEnter is called only once when the router object initiated for parent component. Here is the solution.

var mockObj = {
  name: "Steve"
};

const Parent = {
  template: "<div><h1>Hello from parent <em>{{obj.name}}</em></h1>" +
    "<v-tabs>" +
    `<v-tab :to="'/parent/'+id">child1</v-tab>` +
    `<v-tab :to="'/parent/'+id+'/child2'">child2</v-tab>` +
    `<v-tab :to="'/parent/'+id+'/child3'">child3</v-tab>` +
    "</v-tabs>" +
    "<router-view></router-view>" +
    "</div>",
  props: {
    obj: {
      type: Object,
      required: true
    },
    id: {
      required: true
    }
  }
};

const Child1 = {
  template: "<h3>Child1</h3>"
};
const Child2 = {
  template: "<h3>Child2</h3>"
};
const Child3 = {
  template: "<h3>Child3</h3>"
};

const routes = [{
  path: "/parent/:id",
  component: Parent,
  props: true,
  children: [{
      path: "",
      component: Child1
    },
    {
      path: "child2",
      component: Child2
    },
    {
      path: "child3",
      component: Child3
    }
  ]
}];

const router = new VueRouter({
  routes
});
router.beforeEach((to, from, next) => {
  to.params.obj = mockObj;
  next();
});

new Vue({
  el: "#app",
  router,
  vuetify: new Vuetify()
});
<head>
  <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet" />
  <link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet" />
  <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet" />
  <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
  <script src="https://unpkg.com/vue-router@3.4.3/dist/vue-router.js"></script>
</head>
<div id="app">
  <v-app>
    <v-main>
      <v-container>
        <router-link to="/parent/123">Go To Parent</router-link>
        <router-view></router-view>
      </v-container>
    </v-main>
  </v-app>
</div>
tuhin47
  • 5,172
  • 4
  • 19
  • 29
  • 1
    Well he said *The obj is just used as a placeholder example, in the actual implementation obj is pulled from an API in the route guard beforeEnter* ...I really doubt he wants to hit API on every route change – Michal Levý Sep 21 '20 at 16:09
  • for that he can use vue store for state management easily – tuhin47 Sep 21 '20 at 16:38