DEV Community

Justin Brooks
Justin Brooks

Posted on

Building a Cat Facts app with Vue Router in Vue 3!

The Vue 3 Composition API has open new possibilities for accessing the full power of the Vue Router. In addition to defining the URL structure for your app, it can improve performance by lazy loading pages, and provides navigation middleware to follow coding design principles like DRY.

Today we will be looking at using Vue Router with the composition API, to create a cat facts webpage with full typescript support. By the end of this video, you will hopefully have a full understanding of how to successfully use the newly added useRoute and useRouter composition functions. We'll also look at some changes between Vue 2 and Vue 3 and some more advanced features like:

  • lazyloaded routes,
  • dynamic segments,
  • navigation guards, and
  • adding a 404 error page.

Check out the youtube video that this article was created for:
Youtube Tn


If you are new around here don't forget to follow me and subscribe to my Youtube Channel. You can grab the full source code from the github.

Project Setup

I've already created a basic Vue 3 application and removed the boilerplate code. Don't forget to enable Vue router and typescript when setting up your project with the CLI tool, or you can install them manually using your favorite package manager.

Inspecting the project we can see the CLI created a router folder and a views folder. The router folder contains all the route paths and components in an array which is iterated over until the route is matched. We will come back to this file once we have created some components and views.

// router/index.ts
const routes: Array<RouteRecordRaw> = [
  // ...
]

const router = createRouter({
  history: createWebHistory(),
  routes
})
Enter fullscreen mode Exit fullscreen mode

Below this array, you’ll notice we created the Router itself and we also pass the router array and a called the createWebHistory. This function switches Vue from hash to history mode inside your browser, using the HTML5 history API. For Vue 2 users you probably notice the way the router was configured is a little different.

Homepage

We'll start by creating the home page since this will be the most straight forward. All we need to do is display some welcome information and then add it to the router at the base URL.

<template>
  <div class="card">
    <div class="card-body text-center">
      <h4>
        Welcome to the cat facts page
      </h4>
      <div>🐾</div>
      <span>
        Use the nav menu above to find new facts!
      </span>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

When adding it to the router it requires a path, which is the URL where the route can be found, and a component, that will be loaded when the route is called. We can also add an optional name that can be used when we link to this route.

// router/index.ts
const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: Home
  }
]
Enter fullscreen mode Exit fullscreen mode

There are additional properties we will be looking at in this tutorial but you can find the full list of available options in the Vue router docs.

Dynamic Routes

Next, we will create the Fact page. The URL will accept an id parameter that we will use as an index to display a specific fact. We can access this property by calling the useRoute function which will return a reactive object containing information about the current route.

export default defineComponent({
  components: { FactCard },
  setup() {
    const router = useRoute()
    const factId = computed(() => router.params.id) as ComputedRef<string>
    return { factId }
  }
})
Enter fullscreen mode Exit fullscreen mode

We can create a computed property from the params object which accesses the id value. All we have to do now is pass it to our FactCard which will display the fact's image and text.

I've already created an array that contains text and an image.

// assets/facts.ts
export const facts = [
  // ...
  {
    image:
      'https://cdn.pixabay.com/photo/2016/02/10/16/37/cat-1192026_960_720.jpg',
    text: "The world's largest cat measured 48.5 inches long."
  }
  // ...
]
Enter fullscreen mode Exit fullscreen mode

The FactCard will import the facts array and use the passed in id to determine which one to display. We can also add some validation to make sure the index is within the range. Once we get our fact we can display the image and the fact in the template.

We can finally add the Fact view to our router and you'll notice we are using a colon to indicate the id is a dynamic value. If we had a URL like /fact/3 this would result in the id property being set to 3, just like Vue 2. Instead of using useRoute we could of opt-in to have the dynamic segment passed into the component by props.

// router/index.ts
const routes: Array<RouteRecordRaw> = [
  // ...
  {
    path: '/fact/:id',
    name: 'Fact',
    component: () => import('../views/Fact.vue'),
    beforeEnter: (to, _, next) => {
      const { id } = to.params

      if (Array.isArray(id)) {
        next({ path: '/error' })
        return
      }

      // Is a valid index number
      const index = parseInt(id)
      if (index < 0 || index >= facts.length) {
        next({ path: '/error' })
        return
      }

      next()
    }
  }
  // ...
]
Enter fullscreen mode Exit fullscreen mode

We will also add a router guard so that if a user enters a number that is not within the range of the array it will not direct them to the error page.

Programmatic Routing

For the Fact List page, we will simply use a for loop and iterate over all the facts and display their information. When the HTML element is clicked we can call a function that programmatically redirects the user to the facts page.

To do this we can use the useRouter hook which will an object contain functions to manipulate the current Vue Router instance. We can call the push function and pass it an object telling it where we would like to go.

<script lang="ts">
import { defineComponent } from 'vue'
import { facts } from '@/assets/facts'
import { useRouter } from 'vue-router'
export default defineComponent({
  setup() {
    const router = useRouter()
    const goToFact = (id: number) => {
      router.push({ path: `/fact/${id}` })
    }
    return { facts, goToFact }
  }
})
</script>

<template>
  <div class="list-group">
    <a
      class="list-group-item list-group-item-action clickable"
      v-for="(fact, i) in facts"
      :key="i"
      @click="goToFact(i)"
    >
      <div class="row">
        <div class="col-2"><img :src="fact.image" height="40" /></div>
        <div class="col-10 align-self-center">{{ fact.text }}</div>
      </div>
    </a>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

We could have simply used a router link to direct the user to the location but I wanted to take a look at how we would do this programmatically. We will take a look at using the router-link component when we create the navigation links.

We can add this view to the router and it requires no special conditions.

<router-link>

For the navbar, we will need to create two components.

The HeaderLink, which uses the <router-link> component to redirect the user to the URL when clicked. The slot is simply used to render any nested HTML inside the component. It also applies some special class when the current URL is equal to or starts with the path value passed in.

<script>
import { computed, defineComponent } from 'vue'
import { useRoute } from 'vue-router'
export default defineComponent({
  props: {
    to: { type: String, required: true },
    exact: { type: Boolean, default: false }
  },
  setup(props) {
    const route = useRoute()
    const active = computed(() =>
      props.exact ? route.path === props.to : route.path.startsWith(props.to)
    )
    return { active }
  }
})
</script>

<template>
  <div style="width: 150px">
    <router-link
      :to="to"
      class="nav-link"
      :class="active ? 'font-weight-bold' : null"
    >
      <slot />
    </router-link>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

The Header will contain all our HeaderLinks. We could place the header inside each component but this would be extremely repetitive. Instead, we can add the component outside of the router-view so it's always rendered on every page.

<template>
  <div class="text-center my-3">
    <h4>Cat 🐱 Facts</h4>
    <div class="d-flex justify-content-center">
      <HeaderLink to="/" exact>Home</HeaderLink>
      <HeaderLink to="/facts">List</HeaderLink>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Error Page

Lastly, we’ll need to tell our router what to do when it doesn’t match any routes, and the way we do this is a little different in Vue 3. If we haven't found a component by the end, this means the pages are not found and we can add a custom 404-page component here. We can do this by using catchAll and regular expression in the dynamic segment which will match against everything.

// router/index.ts
const routes: Array<RouteRecordRaw> = [
  // ...
  {
    path: '/:catchAll(.*)',
    name: 'PageNotFound',
    component: () => import('../views/PageNotFound.vue')
  }
  // ...
]
Enter fullscreen mode Exit fullscreen mode

We are done! We have successfully created an application using Vue Router in Vue 3. I hope you gained an understanding of how to creating applications in Vue. If you like this content don't forget to follow me and subscribe to my channel for more content.

Top comments (0)