Image

touring-vue-router

7 min read
Last update: December 19, 2021

Nested Routes

Often when building web applications we need to be able to perform multiple actions (View, Edit, Register) for a single resource (Event). Each URL provides different information on that resource:

  • /event/2 - Event Details (information)
  • /event/2/register - Event Register (to signup for the event)
  • /event/2/edit - Event Edit (to edit the event)

Implementing this leads to a few problems to solve.

🛑 Problem: Where do we place these components?

There’s more than a few options.

Solution: In their own folder.

One best practice for placing these components in Vue would be in their own event folder, effectively organizing them by resource:

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1.opt.1606178737165.jpg?alt=media&token=5f8409bd-37d3-4675-b82f-a8d438e152e7

Great, that’s solves one issue.

🛑 Problem: How do we create and route to these views?

To solve this problem I’m first going to use a simple solution, using basic routing. There will be some duplicate code in this solution, which I’ll show you how to solve using Vue Router’s Nested Routes.

Solution: Basic Routing

First off, we’ll need to map these components in the router. Notice I’m calling them EventDetails, EventRegister, and EventEdit, so I don’t accidentally run into naming conflicts as my router grows:

import { createRouter, createWebHistory } from 'vue-router'
import About from '@/views/About.vue'
import EventList from '@/views/EventList.vue'
import EventDetails from '@/views/event/Details.vue'
import EventRegister from '@/views/event/Register.vue'
import EventEdit from '@/views/event/Edit.vue'

const routes = [
{
path: '/',
name: 'EventList',
component: EventList
props: route => ({ page: parseInt(route.query.page) || 1 })
},
{
path: '/event/:id',
name: 'EventDetails',
props: true,
component: EventDetails
},
{
path: '/event/:id/register',
name: 'EventRegister',
props: true,
component: EventRegister
},
{
path: '/event/:id/edit',
name: 'EventEdit',
props: true,
component: EventEdit
},
...

Notice how I’m importing the components, using a dynamic route, and specifying props: true to send the id part of the URL as a prop into the components. Let’s take a look at the one of the views.

📜 /src/views/event/Details.vue

<template>
    <div v-if="event">
        <h1>{{ event.title }}</h1>
        <div id="nav">
            <router-link :to="{ name: 'EventDetails', params: { id } }">Details</router-link>
            |
            <router-link :to="{ name: 'EventRegister', params: { id } }">Register</router-link>
            |
            <router-link :to="{ name: 'EventEdit', params: { id } }">Edit</router-link>
        </div>
        <p>{{ event.time }} on {{ event.date }} @ {{ event.location }}</p>
        <p>{{ event.description }}</p>
    </div>
</template>

<script>
    import EventService from '@/services/EventService.js'
    export default {
        props: ['id'],
        data() {
            return {
                event: null
            }
        },
        created() {
            EventService.getEvent(this.id)
                .then(response => {
                    this.event = response.data
                })
                .catch(error => {
                    console.log(error)
                })
        }
    }
</script>

Notice in here I display the event title, and then link to all of the event components. Let’s look at another view.

📜 /src/views/event/Register.vue

<template>
    <div v-if="event">
        <h1>{{ event.title }}</h1>
        <div id="nav">
            <router-link :to="{ name: 'EventDetails', params: { id } }">Details</router-link>
            |
            <router-link :to="{ name: 'EventRegister', params: { id } }">Register</router-link>
            |
            <router-link :to="{ name: 'EventEdit', params: { id } }">Edit</router-link>
        </div>
        <p>Regstration form here</p>
    </div>
</template>

<script>
    import EventService from '@/services/EventService.js'
    export default {
        props: ['id'],
        data() {
            return {
                event: null
            }
        },
        created() {
            EventService.getEvent(this.id)
                .then(response => {
                    this.event = response.data
                })
                .catch(error => {
                    console.log(error)
                })
        }
    }
</script>

I haven’t placed a registration form yet, and the edit page is similarly not fleshed out yet. Here’s what it looks like in the browser:

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F2.1606178737166.gif?alt=media&token=24ce1c97-e483-49ff-b280-4153cdb1b3ac

🛑 Problem: We’re repeating the header and navigation on each page.

You might notice that there’s some repetition going on. Specifically the header and the navigation. Wouldn’t it be nice if we could eliminate that duplication? We also have the same API calling code in each component. Let’s fix that too.

Solution: Nested Routes

In this app, we already have a top level <router-view> but we’ve stumbled upon an instance where we need another <router-view> or custom layout for all the event profile components. Let’s go ahead and create a new component called Layout.vue inside the /src/views/event/ directory for the event layout.

    📜 **/src/views/event/Layout.vue**

    ```html
    <template>
        <div v-if="event">
            <h1>{{ event.title }}</h1>
            <div id="nav">
                <router-link :to="{ name: 'EventDetails', params: { id } }">Details</router-link>
                |
                <router-link :to="{ name: 'EventRegister', params: { id } }">Register</router-link>
                |
                <router-link :to="{ name: 'EventEdit', params: { id } }">Edit</router-link>
            </div>
            <router-view :event="event" />
        </div>
    </template>
    <script>
        import EventService from '@/services/EventService.js'
        export default {
            props: ['id'],
            data() {
                return {
                    event: null
                }
            },
            created() {
                EventService.getEvent(this.id)
                    .then(response => {
                        this.event = response.data
                    })
                    .catch(error => {
                        console.log(error)
                    })
            }
        }
    </script>
    ```

    Notice that this layout has the duplicate code from the 3 components we listed. Notice also that the `router-view` shown as `
    <router-view :event="event" />`, so we’re passing down the event object from our API so we don’t have to fetch it again. Now those components are quite simple:

    📜 **/src/views/event/Details.vue**

    ```html
    <template>
        <p>{{ event.time }} on {{ event.date }} @ {{ event.location }}</p>
        <p>{{ event.description }}</p>
    </template>
    <script>
        export default {
            props: ['event']
        }
    </script>
    ```

    Notice the event object gets passed in as a prop. Then there’s

    📜 **/src/views/event/Register.vue**

    ```html
    <template>
        <p>Register for the event here</p>
    </template>
    <script>
        export default {
            props: ['event']
        }
    </script>
    ```

    And

    📜 **/src/views/event/Edit.vue**

    ```html
    <template>
        <p>Edit the event here</p>
    </template>
    <script>
        export default {
            props: ['event']
        }
    </script>
    ```

    All that’s left is to map these Nested Routes together in our router file. We do that like so:

    📜 **/src/router/index.js**

    ```javascript
    import { createRouter, createWebHistory } from 'vue-router'
    import EventList from '../views/EventList.vue'
    import EventLayout from '../views/event/Layout.vue'
    import EventDetails from '../views/event/Details.vue'
    import EventRegister from '../views/event/Register.vue'
    import EventEdit from '../views/event/Edit.vue'
    import About from '../views/About.vue'

    const routes = [
    {
    path: '/',
    name: 'EventList',
    component: EventList,
    props: route => ({ page: parseInt(route.query.page) || 1 })
    },
    {
    path: '/event/:id',
    name: 'EventLayout',
    props: true,
    component: EventLayout,
    children: [ //
    <----- { path: '' , name: 'EventDetails' , component: EventDetails }, { path: 'register' , name: 'EventRegister' , component: EventRegister }, { path: 'edit' , name: 'EventEdit' , component: EventEdit } ] }, ... ``` There’s a few things to pay close attention to here. The first is to notice our EventLayout route with the `children` option, which is sending in an another array of routes. Next, notice how the children are inheriting the path `/event/:id` from the parent route. Since EventDetails has a blank path, this is what gets loaded into `<router-view />` when `/event/:id` is visited. Then, as you might expect `/user/:id/register` and `/user/:id/edit` simply load up the proper routes. As you would expect, it all works great:

    ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F3.1606178748726.gif?alt=media&token=b5293ae0-ea71-40d7-bca8-0d79411fb897](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F3.1606178748726.gif?alt=media&token=b5293ae0-ea71-40d7-bca8-0d79411fb897)

    ## One more optimization

    After showing this code to Eduardo San Martin Morote (@posva) who maintains the Vue Router library, he suggested I make one more optimization to the code, with regards to the `layout.vue`. Take a look at the navigation links as they are currently in this file:

    📜 **/src/views/event/Layout.vue**

    ```html
    <template>
        <div v-if="event">
            <h1>{{ event.title }}</h1>
            <div id="nav">
                <router-link :to="{ name: 'EventDetails', params: { id } }">Details</router-link>
                |
                <router-link :to="{ name: 'EventRegister', params: { id } }">Register</router-link>
                |
                <router-link :to="{ name: 'EventEdit', params: { id } }">Edit</router-link>
            </div>
            ...
            ```

            Notice specifically `params: { id }` . Turns out that we can remove this, and the links still work perfectly:

            📜 **/src/views/event/Layout.vue**

            ```html
            <template>
                <div v-if="event">
                    <h1>{{ event.title }}</h1>
                    <div id="nav">
                        <router-link :to="{ name: 'EventDetails }">Details</router-link>
                        |
                        <router-link :to="{ name: 'EventRegister' }">Register</router-link>
                        |
                        <router-link :to="{ name: 'EventEdit' }">Edit</router-link>
                    </div>
                    ...
                    ```

                    How does this work? Well, since these links all require `:id` , when the `router-link` is rendered in the template (if it’s not sent in) it will look at the URL parameters, and if `:id` exists in the current route, it will use the `:id` in all of the link URLs. So, back in our browser, everything still works:

                    ![https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F4.1606178752025.gif?alt=media&token=4479111c-7d5e-42b5-abb1-4861b496e164](https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F4.1606178752025.gif?alt=media&token=4479111c-7d5e-42b5-abb1-4861b496e164)