vuetify.jsvue-routervuetify-tabs

Vuetify Tabs with Router and dynamic nested routes


I'm pretty new to Vue. My app has a pretty standard layout with top nav, side nav, footer and a content area. Content area is divided into two with a tree on the left and a Tabbed interface on the right. I'm using vue router with nested dynamic routes.

TreeAndTab.vue

import Vue from 'vue'
import VueRouter from 'vue-router'
/* import DefaultLayout from '../layout/Default.vue' */
/* import TreeAndTabLayout from '../layout/TreeAndTab.vue' */

Vue.use(VueRouter)

const routes = [
    {
        path: '/home',
        name: 'home',
        meta: { layout: 'default' },
        component: () => import('../pages/Home.vue')
    },
    {
        path: '/about',
        name: 'about',
        meta: { layout: 'default' },
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import('../pages/About.vue')
    },
    {
        path: '/dashboard',
        name: 'dashboard',
        meta: { layout: 'default' },
        component: () => import('../pages/dashboard/dashboard.vue')
    },
    {
        // Top level requirement goes to epic
        path: '/r/:epic_id?',
        //name: 'requirement',
        meta: { layout: 'default' },
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import('../pages/requirement/Requirement.vue'),
        children: [
            {
                path: '',
                name: 'epic',
                component: () => import('../pages/About.vue'),
                props: true,
                children: [
                    {
                        path: '/r/:epic_id/s/:story_id',
                        name: 'story',
                        component: () => import('../pages/Home.vue'),
                        props: true
                    }
                ]
            }
        ]
    }
]

const router = new VueRouter({
    mode: 'history',
    base: process.env.BASE_URL,
    routes
})

export default router

I have two layouts - Default.vue and TreeAndTab.vue. When the app loads, Default.vue is used and the page further loads TreeAndTab.vue layout.

TreeAndTab.vue

<template>
    <tree-and-tab-layout
        :treeProps="treeProps"
        :tabProps="tabProps"
        :treeOptions="treeOptions"
    >
    </tree-and-tab-layout>
</template>

<script>
import TreeAndTabLayout from "../../layout/TreeAndTab.vue";
import RequirementService from "./RequirementService.js";

export default {
    components: {
        TreeAndTabLayout,
    },
    data: () => ({
        treeProps: {},
        tabProps: {
            tabs: [
                {
                    id: 1,
                    title: "epic",
                    route: { name: "epic" },
                },
                {
                    id: 2,
                    title: "story",
                    route: { name: "story" },
                },
                {
                    id: 3,
                    title: "mapping",
                    /* route: `/requirement/mapping/${this.$route.params.map_id}` */
                },
            ],
        },
        treeOptions: {
            propertyNames: {
                text: "title",
            },
        },
    }),
    methods: {
        getTabProps() {
            return {};
        },
    },
    created() {
        this.treeProps = RequirementService.getAllRequirementsForApp();
        //this.tabProps = this.getTabProps();
        this.treeProps.activeNode = [
            this.$route.params.epic_id || this.$route.params.story_id,
        ];
    },
};
</script>

Requirement.vue

<template>
    <tree-and-tab-layout
        :treeProps="treeProps"
        :tabProps="tabProps"
        :treeOptions="treeOptions"
    >
    </tree-and-tab-layout>
</template>

<script>
import TreeAndTabLayout from "../../layout/TreeAndTab.vue";
import RequirementService from "./RequirementService.js";

export default {
    components: {
        TreeAndTabLayout,
    },
    data: () => ({
        treeProps: {},
        tabProps: {
            tabs: [
                {
                    id: 1,
                    title: "epic",
                    route: { name: "epic" },
                },
                {
                    id: 2,
                    title: "story",
                    route: { name: "story" },
                },
                {
                    id: 3,
                    title: "mapping",
                    /* route: `/requirement/mapping/${this.$route.params.map_id}` */
                },
            ],
        },
        treeOptions: {
            propertyNames: {
                text: "title",
            },
        },
    }),
    methods: {
        getTabProps() {
            return {};
        },
    },
    created() {
        this.treeProps = RequirementService.getAllRequirementsForApp();
        //this.tabProps = this.getTabProps();
        this.treeProps.activeNode = [
            this.$route.params.epic_id || this.$route.params.story_id,
        ];
    },
};
</script>

The flow I want is as follows:

  1. When the page loads, the first item in the tree is selected.
  2. When user clicks on a parent node in the tree, first tab on the right should be selected and appropriate content loaded. It is the parent route in the router.
  3. When user clicks on a child load, second tab should be loaded based on the router.

I see that the tree is behaving correctly and correct route is displayed on address bar. Component for the first tab also loads correctly. However, when I click on a leaf node, even though the route is created correctly, the tab does not get updated. Neither does the tab change nor does the appropriate component get loaded.I have tried various options including using route names in tabs :to etc but nothing seems to work.

Any help is much appreciated. If required I can post the code on GitHub.


Solution

  • Finally I was able to fix it.Looks like route on the tabs were not setup correctly. Here's what I changed:

    1. Moved tabProps to compute() to update route on the fly.
    2. Fired an event from child component to update the route which gets caught by the parent that updates the route.
    3. I did not use this.$route to update the tab route dynamically since I wanted to retain the state of the second tab if a child node is selected on the tree but the user switches to the first tab which contains data for the parent. (I know its confusing). So its like a file explorer where first tab shows details of the folder and second tab shows details of the selected child in that folder.

    Tab states are now maintained.

    Here's the relevant code (not the most efficient but it works). Hopefully, it helps someone facing similar issue.

    route.js

    /* eslint-disable no-unused-vars */
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    /* import DefaultLayout from '../layout/Default.vue' */
    /* import TreeAndTabLayout from '../layout/TreeAndTab.vue' */
    
    Vue.use(VueRouter)
    
    const routes = [
        {
            path: '/home',
            name: 'home',
            meta: { layout: 'default' },
            component: () => import('../pages/Home.vue')
        },
        {
            path: '/about',
            name: 'about',
            meta: { layout: 'default' },
            // route level code-splitting
            // this generates a separate chunk (about.[hash].js) for this route
            // which is lazy-loaded when the route is visited.
            component: () => import('../pages/About.vue')
        },
        {
            path: '/dashboard',
            name: 'dashboard',
            meta: { layout: 'default' },
            component: () => import('../pages/dashboard/dashboard.vue')
        },
        {
            // Top level requirement goes to epic
            path: '/plan',
            //name: 'requirement',
            meta: { layout: 'default' },
            // route level code-splitting
            // this generates a separate chunk (about.[hash].js) for this route
            // which is lazy-loaded when the route is visited.
            component: () => import('../pages/requirement/Requirement.vue'),
            children: [
                {
                    path: 'e/:epic_id',
                    name: 'epic',
                    component: () => import('../pages/About.vue'),
                    props: true
                },
                {
                    path: 'e/:epic_id/s/:story_id',
                    name: 'story',
                    component: () => import('../pages/Home.vue'),
                    props: true
                }
            ]
        }
    ]
    
    const router = new VueRouter({
        mode: 'history',
        base: process.env.BASE_URL,
        routes
    })
    
    export default router
    
    

    pages/Requirement.vue

    <template>
        <tree-and-tab-layout
            :treeProps="treeProps"
            :tabProps="tabProps"
            :treeOptions="treeOptions"
            v-on:activateTreeNode="handleTreeNodeActivate"
        >
        </tree-and-tab-layout>
    </template>
    
    <script>
    //
    import TreeAndTabLayout from "../../layout/TreeAndTab.vue";
    import RequirementService from "./RequirementService.js";
    
    export default {
        name: "RequirementPage",
        components: {
            TreeAndTabLayout,
        },
        data: () => ({
            base_path: "/plan",
            epic_id: "",
            story_id: "",
            epic_base_path: "/e/",
            story_base_path: "/s/",
            treeProps: {},
            treeOptions: {
                propertyNames: {
                    text: "title",
                },
            },
        }),
        computed: {
            tabProps() {
                return {
                    tabs: [
                        {
                            id: 1,
                            title: "Epic",
                            route:
                                this.base_path + this.epic_base_path + this.epic_id,
                        },
                        {
                            id: 2,
                            title: "Story",
    
                            route:
                                this.base_path +
                                this.epic_base_path +
                                this.epic_id +
                                this.story_base_path +
                                this.story_id,
                        },
                    ],
                };
            },
        },
        methods: {
            handleTreeNodeActivate(child_id, parent_id) {
                this.story_id = child_id;
                this.epic_id = parent_id;
            },
        },
        created() {
            this.treeProps = RequirementService.getAllRequirementsForApp();
            // Does not work somehow. Handling it from the template
            //this.$on("activateTreeNode", this.handleTreeNodeActivate);
        },
    };
    </script>
    
    

    TreeAndTab.vue

    <template>
        <splitpanes>
            <pane size="30">
                <tree
                    :data="treeProps.items"
                    :options="treeOptions"
                    ref="tree"
                    @node:selected="onActive"
                    @node:expanded="onExpand"
                />
            </pane>
            <pane size="70">
                <v-tabs v-model="activeTab" light>
                    <!-- <v-tabs-slider></v-tabs-slider> -->
                    <v-tab
                        v-for="tab in tabProps.tabs"
                        :key="tab.id"
                        :to="tab.route"
                        exact
                        >{{ tab.title }}</v-tab
                    >
                </v-tabs>
    
                <v-card flat tile>
                    <keep-alive>
                        <router-view />
                    </keep-alive>
                </v-card>
            </pane>
        </splitpanes>
    </template>
    
    <script>
    import { Splitpanes, Pane } from "splitpanes";
    import LiquorTree from "liquor-tree";
    
    import "splitpanes/dist/splitpanes.css";
    
    export default {
        name: "TreeAndTab",
        components: {
            Splitpanes,
            Pane,
            tree: LiquorTree,
        },
        props: {
            treeProps: {
                type: Object,
                required: true,
            },
            tabProps: {
                type: Object,
                required: true,
            },
    
            treeOptions: {
                type: Object,
            },
        },
        data: () => ({
            newEpic: {
                id: "e_new",
                title: "New Epic",
                isFolder: true,
            },
            newStory: {
                id: "s_new",
                title: "New Story",
            },
            newCounter: 0,
            activeTab: null,
        }),
        mounted() {
            
            console.log("mounted Tree and Tab");
        },
        computed: {},
    
        methods: {
            onActive(node) {
                
    
                if (node.parent != null) {
                    this.$emit("activateTreeNode", node.id, node.parent.id);
                    this.$router.push({
                        name: "story",
                        params: { epic_id: node.parent.id, story_id: node.id },
                    });
    
                } else {
                    
                    this.$emit("activateTreeNode", null, node.id);
    
                    this.$router.push({
                        name: "epic",
                        params: { epic_id: node.id },
                    });
    
                    
                }
            },
            onExpand(node) {
                console.log("expand=", node);
            },
            
            getCurrentActiveNode() {
                return this.$refs.tree.selected()[0];
            },
        },
    };
    </script>
    
    <style scoped>
    
    </style>