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 then 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:
.
🛑 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.
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
<script setup>
import { onMounted, ref } from 'vue'
import EventService from '@/services/EventService.js'
const { id } = defineProps(['id'])
const event = ref(null)
onMounted(() => {
EventService.getEvent(id)
.then(response => {
event.value = response.data
})
.catch(error => {
console.log(error)
})
})
</script>
<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>
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
<script setup>
defineProps(['event'])
</script>
<template>
<p>{{ event.time }} on {{ event.date }} @ {{ event.location }}</p>
<p>{{ event.description }}</p>
</template>
Notice the event object gets passed in as a prop. Then there’s
📜 /src/views/event/Register.vue
<script setup>
defineProps(['event'])
</script>
<template>
<p>Register for the event here</p>
</template>
And
📜 /src/views/event/Edit.vue
<script setup>
defineProps(['event'])
</script>
<template>
<p>Edit the event here</p>
</template>
All that’s left is to map these Nested Routes together in our router file. We do that like so:
📜 /src/router/index.js
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
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
<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
<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.