Why we need vue-router?
We are building Vue single page application.
Therefore, we only have one html file under public folder.
Based on this html file, browser will use JavaScript to render other pages.
Also, we have learned using 'dynamic component' to switch content.
Take below as an example.
Ex:
//Structure
src
├───components
│ ├─── nav
│ │ └─ TheNavigation.vue
│ ├─── products
│ │ └─ ProductsList.vue
│ └─── users
│ └─ UsersList.vue
├── App.vue
└── main.js
// App.vue
<template>
<the-navigation v-on:set-active-page="setActivePage"></the-navigation>
<main>
<!-- Using dynamic component -->
<component :is="activePage"></component>
</main>
</template>
<script>
import TheNavigation from './components/nav/TheNavigation.vue';
import UsersList from './components/users/UsersList.vue';
import ProductsList from './components/products/ProductsList.vue';
export default {
components: {
TheNavigation,
UsersList,
ProductsList,
},
data() {
return {
activePage: 'users-list',
users: [
{ id: 'u1', name: 'User 1' },
{ id: 'u2', name: 'User 2' },
],
products: [
{ id: 'p1', name: 'Product 1' },
{ id: 'p2', name: 'Product 2' },
{ id: 'p3', name: 'Product 3' },
],
};
},
provide() {
return {
users: this.users,
products: this.products,
};
},
methods: {
setActivePage(page) {
this.activePage = page;
},
},
};
</script>
// components/nav/TheNavigation.vue
<template>
<header>
<nav>
<ul>
<li>
<button v-on:click="onActivePageClicked('users-list')">
Users
</button>
</li>
<li>
<button v-on:click="onActivePageClicked('products-list')">
Products
</button>
</li>
</ul>
</nav>
</header>
</template>
<script>
export default {
emits: ['set-active-page'],
methods: {
onActivePageClicked(page) {
this.$emit('set-active-page', page);
},
},
};
</script>
This approach works, but it is not ideal solution because you might notice that those pages shared with the same URL.
Sometimes, you might want to share a specific URL to your friends for a quick access instead of homepage.
Then the officially-supported vue-router can help!
$ npm install vue-router@next
How to use vue-router
We can use vue-router to revise our example above.
Ex:
// main.js
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import UsersList from './components/users/UsersList.vue';
import ProductsList from './components/products/ProductsList.vue';
const router = createRouter({
history: createWebHistory(),
// Define routes
routes: [
{
path: '/users', component: UsersList
},
{
path: '/products', component: ProductsList
},
]
});
const app = createApp(App);
// Install a Vue.js plugin.
app.use(router);
app.mount('#app');
// App.vue
<template>
<the-navigation></the-navigation>
<main>
<!-- component matched by the route will render here -->
<router-view></router-view>
</main>
</template>
<script>
import TheNavigation from './components/nav/TheNavigation.vue';
export default {
components: {
TheNavigation,
},
data() {
return {
users: [
{ id: 'u1', name: 'User 1' },
{ id: 'u2', name: 'User 2' },
],
products: [
{ id: 'p1', name: 'Product 1' },
{ id: 'p2', name: 'Product 2' },
{ id: 'p3', name: 'Product 3' },
],
};
},
provide() {
return {
users: this.users,
products: this.products,
};
},
};
</script>
// components/nav/TheNavigation.vue
<template>
<header>
<nav>
<ul>
<li>
<!-- use router-link component for navigation. -->
<router-link to="/users">Users</router-link>
</li>
<li>
<router-link to="/products">Users</router-link>
</li>
</ul>
</nav>
</header>
</template>
Programmatic Navigation
Refer to this:
It is normal that you might want to navigate page programmatically such as navigate to login page once users logout.
Since we have added vue-router, in our all component scope, there is a built-in object called 'this.$router' you can use.
Ex:
// components/users/UsersList.vue
this.$router.push('/products');
Pass data with route params
Refer to this:
Imagine you are using a CMS to manage products.
So basically, you will have two pages: one is product list page for viewing all products; another is product detail page for viewing/editing one product.
We need to pass product id from product list page to product detail page.
Then product detail page will use that id to fetch product detail information through web api. (Or from Provide-Inject pattern)
Ex:
// main.js
// components/products/ProductsList.vue
// components/products/ProductDetail.vue
Refer to this:
According to this official document, we need to watch '$route.params'!
Ex:
// components/products/ProductDetail.vue
Refer to this:
In your home domain, you might want to direct to other route as default route.
Also, we can setup a fetch all route to let users know what happens instead of black page.
Ex:
// main.js
Refer to this:
It is the similar idea like top level route.
We need to add '<router-view>' for the children routes.
Ex:
// main.js
// components/products/ProductsList.vue
Refer to this:
Instead using path, we can use named routes!
Ex:
// main.js
// components/products/ProductDetail.vue
Refer to this:
In some complex UI, we might need different footer or header based on which page you are.
Ex:
// main.js
// App.vue
Refer to this:
We can use this feature to add some conditions before/after navigation.
Some cases:
Users don't have enough permissions to view that page.
There is un-save changes in the form, and you can add a confirmation UI to let user make a decision.
In order to determine which components are loaded by router, we will move those component to a new folder called 'pages'
Ex:
//Structure
Also normally, we will create a new file called 'router.js' to store all codes relating routing, which can make main.js cleaner.
Ex:
// router.js
So basically, you will have two pages: one is product list page for viewing all products; another is product detail page for viewing/editing one product.
We need to pass product id from product list page to product detail page.
Then product detail page will use that id to fetch product detail information through web api. (Or from Provide-Inject pattern)
Ex:
// main.js
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import UsersList from './components/users/UsersList.vue';
import ProductsList from './components/products/ProductsList.vue';
import ProductDetail from './components/products/ProductDetail.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/users', component: UsersList
},
{
path: '/products', component: ProductsList
},
{
// Dynamic segment
path: '/products/:id', component: ProductDetail
},
]
});
const app = createApp(App);
app.use(router);
app.mount('#app');
// components/products/ProductsList.vue
<template>
<ul>
<li v-for="product in products" :key="product.id">
<!-- Generate path -->
<router-link v-bind:to="'/products/' + product.id">
{{ product.name }}
</router-link>
</li>
</ul>
</template>
<script>
export default {
inject: ['products'],
};
</script>
// components/products/ProductDetail.vue
<template>
<h1>Product Detail</h1>
<div v-if="product">
<p>Name: {{ product.name }}</p>
<p>Price: {{ product.price }}</p>
</div>
</template>
<script>
export default {
inject: ['products'],
data() {
return {
product: null,
};
},
created() {
// Get data from route params
const productId = this.$route.params.id;
const selectedProduct = this.products.find(
(product) => product.id === productId
);
if (selectedProduct) {
this.product = selectedProduct;
}
},
};
</script>
Navigate to the same page with different route params?
Refer to this:
According to this official document, we need to watch '$route.params'!
Ex:
// components/products/ProductDetail.vue
<template>
<h1>Product Detail</h1>
<div v-if="product">
<p>Name: {{ product.name }}</p>
<p>Price: {{ product.price }}</p>
</div>
<!-- Exp only - we hard-coded to set it to 'p3' -->
<button v-on:click="this.$router.push('/products/p3')">
Go to Product - p3
</button>
</template>
<script>
export default {
inject: ['products'],
data() {
return {
product: null,
};
},
watch: {
// Since $route will keep updated, we can watch it to get the params changes
$route(value) {
this.getProductInfo(value);
},
},
methods: {
getProductInfo(route) {
const productId = route.params.id;
const selectedProduct = this.products.find(
(product) => product.id === productId
);
if (selectedProduct) {
this.product = selectedProduct;
}
},
},
created() {
this.getProductInfo(this.$route);
},
};
</script>
Redirecting
Refer to this:
In your home domain, you might want to direct to other route as default route.
Also, we can setup a fetch all route to let users know what happens instead of black page.
Ex:
// main.js
const router = createRouter({
history: createWebHistory(),
routes: [
{
// Redirect '/' to a default route
path: '/', redirect: '/users',
},
{
path: '/users', component: UsersList
},
{
path: '/products', component: ProductsList
},
{
path: '/products/:id', component: ProductDetail
},
{
// Redirect to a default route if it cannot match any route above
path: '/:notFound(.*)', component: NotFound,
},
]
});
Nested Routes
Refer to this:
It is the similar idea like top level route.
We need to add '<router-view>' for the children routes.
Ex:
// main.js
{
path: '/products',
component: ProductsList,
// Nested Routes
children: [
{
path: ':id', component: ProductDetail
},
]
},
// components/products/ProductsList.vue
<template>
<!-- Adding another router-view for children routes -->
<router-view></router-view>
<ul>
<li v-for="product in products" :key="product.id">
<router-link v-bind:to="'/products/' + product.id">
{{ product.name }}
</router-link>
</li>
</ul>
</template>
<script>
export default {
inject: ['products'],
};
</script>
Named Routes and Query Params
Refer to this:
Instead using path, we can use named routes!
Ex:
// main.js
routes: [
{
path: '/', redirect: '/users',
},
{
path: '/users', component: UsersList
},
{
path: '/products',
component: ProductsList,
},
{
// Adding a name for this route
name: 'product-detail',
path: '/products/:id', component: ProductDetail
},
{
path: '/:notFound(.*)', component: NotFound,
},
]
// components/products/ProductDetail.vue
<template>
<h1>Product Detail</h1>
<div v-if="product">
<p>Name: {{ product.name }}</p>
<p>Price: {{ product.price }}</p>
</div>
<!-- Exp only - we hard-coded to set it to 'p3' -->
<router-link v-bind:to="routeToP3"> Go to Product - p3 </router-link>
</template>
<script>
export default {
inject: ['products'],
data() {
return {
product: null,
};
},
computed: {
routeToP3() {
// Using path
// return '/products/p3';
// Using name
return {
name: 'product-detail',
params: { id: 'p3' },
// Passing data through Query params
query: { test: '123' },
};
},
},
watch: {
$route(value) {
this.getProductInfo(value);
},
},
methods: {
getProductInfo(route) {
const productId = route.params.id;
const selectedProduct = this.products.find(
(product) => product.id === productId
);
if (selectedProduct) {
this.product = selectedProduct;
}
},
},
created() {
this.getProductInfo(this.$route);
},
};
</script>
Named Router View
Refer to this:
In some complex UI, we might need different footer or header based on which page you are.
Ex:
// main.js
routes: [
{
path: '/', redirect: '/users',
},
{
path: '/users',
// setup components to each default view and named view
components: { default: UsersList, footer: UserFooter }
},
{
path: '/products',
// setup components to each default view and named view
components: { default: ProductsList, footer: ProductFooter }
},
{
name: 'product-detail',
path: '/products/:id',
// setup components to each default view and named view
components: { default: ProductDetail, footer: ProductFooter }
},
{
path: '/:notFound(.*)', component: NotFound,
},
]
// App.vue
<template>
<the-navigation></the-navigation>
<main>
<router-view></router-view>
<!-- named view -->
<router-view name="footer"></router-view>
</main>
</template>
Navigation Guard
Refer to this:
We can use this feature to add some conditions before/after navigation.
Some cases:
Users don't have enough permissions to view that page.
There is un-save changes in the form, and you can add a confirmation UI to let user make a decision.
Organize router codes
In order to determine which components are loaded by router, we will move those component to a new folder called 'pages'
Ex:
//Structure
src
├───components
│ ├─── nav
│ │ └─ TheNavigation.vue
│ └─── products
│ └─ ProductDetail.vue
├── pages
│ ├─── NotFound.vue
│ ├─── ProductsList.vue
│ └─── UsersList.vue
├── App.vue
└── main.js
Also normally, we will create a new file called 'router.js' to store all codes relating routing, which can make main.js cleaner.
Ex:
// router.js
import { createRouter, createWebHistory } from 'vue-router';
import UsersList from './pages/UsersList.vue';
import ProductsList from './pages/ProductsList.vue';
import ProductDetail from './components/products/ProductDetail.vue';
import NotFound from './pages/NotFound.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/', redirect: '/users',
},
{
path: '/users',
component: UsersList,
},
{
path: '/products',
component: ProductsList
},
{
name: 'product-detail',
path: '/products/:id',
component: ProductDetail,
},
{
path: '/:notFound(.*)', component: NotFound,
},
]
});
export default router