Friday, February 26, 2021

Vue.js - Routing

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 { createRoutercreateWebHistory } 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

import { createApp } from 'vue';
import { createRoutercreateWebHistory } 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: UsersListfooter: UserFooter }
        },
        {
            path: '/products',
            // setup components to each default view and named view
            components: { default: ProductsListfooter: ProductFooter }
        },
        {
            name: 'product-detail',
            path: '/products/:id',
            // setup components to each default view and named view
            components: { default: ProductDetailfooter: 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 { createRoutercreateWebHistory } 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


No comments:

Post a Comment