DEV Community

Cover image for How to Build a Vue App To Show Your GitHub Repositories
Bukola Ogunleye
Bukola Ogunleye

Posted on

How to Build a Vue App To Show Your GitHub Repositories

Less than a year ago, I was a novice in programming with no background in computer science which is why building this app was challenging but fulfilling. So, if you’re a beginner like me and everything seems overwhelming, I hope this motivates you to keep reading, practising, and building!

Prerequisites for building along:

  1. Have at least a basic understanding of HTML, CSS, and JavaScript
  2. Know how to use a code editor
  3. Be familiar with the command line and terminal
  4. Have Node.js installed on your machine — Vite, the build tool we will use to build this project, runs on Node.js
  5. Know how to use a package manager like npm, pnpm, yarn, or bun

That said, if you are not a complete beginner with Vue.js, you can jump right into the section where we build the app.


A Comprehensive Guide to Setting Up a Vue Project

Vue.js is a flexible JavaScript framework that gives developers the freedom to build projects in different ways.

You can start writing Vue code or set up a Vue project using any of these methods:

  1. In an HTML file with the content delivery network (CDN)
  2. Through the Vue CLI (Webpack/Babel)
  3. Scaffolding a Vue project with Vite

Quick Start: Writing Vue Code in an HTML File

You can create an index.html file and add a CDN script to learn and get familiar with Vue syntax.

  • Create a folder in your pc
  • Create an index.html file
  • Open the file, add the HTML boilerplate, and an empty <script> tag that will contain your Vue code block
  • Then add the following script: <script src=“https://unpkg.com/vue@3/dist/vue.global.js”></script>

Adding this CDN script turns your HTML file into a kind of Vue file, so you can use it to play around with Vue code.

Sample Vue code in an HTML file:

<!-- CDN script to start using Vue without any build tools -->

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<div id="app">
  <p>{{ message }}</p>
  <p>My name is {{ name }}</p>
</div>

<script>
  Vue.createApp({
    data() {
      return {
        name: "Tolu",
        message: "Hello Vue!",
      };
    },
  }).mount("#app");
</script>
Enter fullscreen mode Exit fullscreen mode

Note: it’s not advisable to try to build a real Vue project using this approach because writing Vue in an HTML file has some limitations. One of which is that this approach doesn’t take advantage of the modularity Vue offers.

Writing Vue Code Using the Vue CLI (Webpack/Babel)

Screenshot of a message from Vue saying 'Vue CLI is in Maintenance Mode!'

Although the Vue CLI is currently in maintenance mode, you can use it to learn about creating and reusing components in a Vue project.

Here’s how to set up a Vue project using the Vue CLI:

  • For this, you have to install Vue globally into your machine. Use this command npm install -g @vue/cli OR yarn global add @vue/cli
  • Create a directory where you will set up the project. You can do this from your terminal with the following command — mkdir {insert a new folder} OR cd {an existing folder}
  • Open the directory using the following command — cd {insert the new folder} if you just created a new folder
  • Use this command in your terminal — vue create {insert-your-project-name}

This will scaffold a new Vue CLI project with the name you choose. Here’s the folder tree of a sample Vue CLI project with the name ‘vue-cli-app’.

The folder tree of a Vue app created with the Vue CLI

While your project directory is open in your terminal, use the command; npm run serve to see the newly created demo project in the browser.

Scaffolding a Vue Project With Vite

If you want to enjoy the full benefits of the Vue.js framework, particularly for building larger projects, it’s better to write Vue code in single file components (SFCs). Each SFC will be written inside a .vue file. Writing Vue code in .vue files will give you access to a better development environment.

Regardless of the Vue API you use to build a Vue project, all Vue files have the same elements:

  1. A <template> tag that will contain your typical HTML content
  2. A <script> tag that contains your Vue logic
  3. A <style> tag that will contain your CSS styling

You might be wondering why we need to use Vite to build a Vue SFCs project, and there’s a perfectly good answer for your curiosity. The browser can only understand .html, .css, and .js files. Any code not written in those three coding languages must be compiled into HTML, CSS and JavaScript for the browser to run the code. Vite is a build tool that helps us to compile our code before sending it to the browser.

Now, let’s scaffold a new Vue project using Vite. Similar to the Vue CLI approach, we will also do this from the terminal using the following commands:

  • mkdir {insert a new directory name} OR cd {insert an existing directory} in your terminal — this is where the project will live
  • cd {insert the new directory name} if you just created the directory
  • npm create vue@latest OR pnpm create vue@latest OR yarn create vue@latest OR bun create vue@latest
  • You will be prompted to provide the project name, and you can choose a name like ‘my-vue-app'
  • You will then be prompted to answer No or Yes questions for options such as ‘Add TypeScript?’ and ‘Add Pinia for state management?’. Once you become more familiar with Vue, you will know which extra technologies/tools to add to your Vue project. But for now, choose ‘No’ for all the options

Once that is done, your terminal will prompt you to run the following commands:

  • cd {insert-your-project-name} — this command opens your project folder
  • npm install — this command installs the required dependencies to make your Vue project work
  • npm run dev — and this command starts up a development server showing the scaffolded demo app provided by Vite in the browser, which you can see via localhost:5173

A screenshot of the terminal showing the progression of commands use to scaffold a new Vue project created with Vite

Ensure to run these commands in the same order. First, cd to your project folder, then npm install if you used npm to initiate the project. If you used yarn, bun, or pnpm, use their commands instead.

Your project directory will have a similar folder tree:

The folder tree of a Vue app created with Vite

You can look around and delete files you will not be using to build this project such as the files in the components and the assets folders. You can also delete the content of the App.vue and main.js files to write your code in them.


Options API vs Composition API

There are two different ways to write Vue code, the Options API and the Composition API. While you can use both the Options API and Composition API in the same project, it’s not conventional to mix both APIs. Besides, sticking to one of the approaches in a project will make your code more readable than switching back and forth between Options API and Composition API.

Example

A picture collage of two screenshots; the left frame is written with Options API while the right frame is written with Composition API

What difference do you notice between both code blocks?

First, you will see that the <script> tag in the Composition API code has a setup attribute, and we also imported ref and onErrorCaptured. On the other hand, the Options API code is wrapped in an export default code block. Additionally, the Options API code has a data property. There are other differences to note between both APIs, and you will see more as we build this GitHub repository project.

Which API Should You Use to Build Your Vue Project?

If you’re wondering which API is better to build your Vue project, the truth is that you can use anyone. Start by learning one of the APIs and build a simple project like the one we will build in this article. Then, learn the other API, and also build a simple project with it or recreate an existing Options API Vue project with Composition API and vice versa.

Another concern you might have, particularly about the Options API, is whether it will be deprecated in the near future. Here’s what Evan You, the creator of Vue.js, has to say about it.

A screenshot of a tweet from Evan You talking about the future of Options API

So, while you can use the Options API to build your project, it helps to be familiar with both APIs. This article will show you how to build this project with both APIs. And remember not to use both APIs in the same project.


Let’s Build Our Mobile Responsive Vue App

The elements and functionalities our Vue app will have are:

  • A home page
  • A ‘NavBar’ — for both large viewports and small viewports
  • A ‘NotFound’ page, which is similar to an error 404 page
  • Error boundary to catch and report errors
  • Pagination
  • Routing and nested routing using params
  • Fetching data from the GitHub API and displaying the content

Creating the Vue Instance and Mounting the App

We write the following code in the main.js file:

import { createApp } from vue
import App from ./App.vue
const app = createApp(App)
app.mount(‘#app)
Enter fullscreen mode Exit fullscreen mode

The first line imports the Vue createApp instance.

The second line imports the App.vue component. The App.vue is the main component that will house all other components because unlike HTML, Vue apps can only have one page. All other pages within a Vue app are displayed to the browser through routing.

The third line creates the Vue instance variable in our app, and the fourth line mounts the app. Note that we can create the Vue instance and mount our app in one line of code using this createApp(App).mount(‘#app’). But this can make our code messy as we add more code to the main.js file. Instead, we use lines three and four, which give us more flexibility.

The app.mount(‘#app’) line must always be the last line of code in the main.js file of a Vue project because the app gets mounted once you write that line. Any lines of code after it will not reflect in the app.

Create the Components for the App

This app will have the following components:

  • A HomePage.vue
  • A NavBar.vue
  • A NotFound.vue
  • An ErrorBoundary.vue
  • A component for displaying all the repositories — RepoCards.vue, you can use any name you like
  • A component for displaying a single repository — SingleRepo.vue, you can use any name you like. This component will be a nested route in the RepoCards component

Create all these files inside the components folder, located in the src folder of the project.

Note: if you don’t have enough GitHub repositories to build this project, fetching data from the GitHub API might be pointless. Instead, you can fetch data from a dummy API such as RandomDataAPI. You also don't have to create the RepoCards.vue and SingleRepo.vue components, you can create DemoCards.vue and singleContent.vue components.


Setting Up the Vue Router in Your App

The NavBar component will link to different URLs within the app, and the app will also have a page that has a nested route. We need to set up the Vue router to enable this routing functionality. To use the Vue router in your project, install it into the project directory using this command: npm install vue-router@4

Once installed, create a folder with the name 'router' inside your components folder, and create an index.js file inside the router folder.

Write the following code inside the index.js file:

import { createRouter, createWebHistory } from vue-router

import HomePage from '../components/HomePage.vue'
import ErrorBoundary from '../components/ErrorBoundary.vue'
import NotFound from '../components/NotFound.vue'
import RepoCards from '../components/RepoCards.vue'
import SingleRepo from ../components/SingleRepo.vue'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      alias: '/home',
      component: HomePage,
      name: 'Home',
      meta: { title: 'Home Page', description: 'Home page' }
    },
    {
      path: '/errorBoundary',
      component: ErrorBoundary,
      name: 'ErrorBoundary Page',
      meta: { title: 'ErrorBoundary', description: 'Test error boundary' }
    },
    {
      path: '/:pathMatch(.*)*',
      alias: '/error404',
      component: NotFound,
      name: 'NotFound',
      meta: { title: 'NotFound', description: `The page doesn't exist` }
    },
    {
      path: '/repoCards',
      component: RepoCards,
      name: 'Repository Cards',
      meta: {
        title: 'Repository Cards',
        description: 'All repositories'
      }
    },
    {
      path: '/singleRepo/:name',
      component: SingleRepo,
      name: 'Single Repository',
      meta: {
        title: 'Expanded Repository',
        description: 'A repository in view'
      }
    }
  ]
})

export default router
Enter fullscreen mode Exit fullscreen mode

Breakdown of the code:

  • The first line imports the createRouter and createWebHistory from the Vue router. createWebHistory allows the router to keep track of the web history which is how you can go back and forward within a web app on the browser.
  • Next, we import all the components we need to route with the Vue router.
  • Then, we create the router variable with all the routes it will house. The path is the URL for each page, and some paths have aliases. The name is the name we gave each component. The meta adds SEO for each component as a page in the browser.
  • The HomePage’s path is the default path — the default view, of the app, it will be the page users will land on upon visiting the web app URL.
  • The NotFound’s path is a params of regular expression (regex) that matches all page routing errors to capture when users try to visit a page that doesn’t exist.
  • You will notice that the path for the SingleRepo component is different from the rest, that’s because it’s a nested route that takes a params we defined as :name.
  • Finally, we export the router to make it accessible to other files in the project.

We can’t use our router just yet. We need to import it into the main.js file. Edit the main.js file with the following code:

import router from ./router
const app = createApp(App)
app.use(router)
app.mount(‘#app)
Enter fullscreen mode Exit fullscreen mode

Now, we can use the router across the components in the project.

Adding the Ionic Framework as a Dependency To Your Vue App

At this point, we need to bring in our UI framework, and I will be using the Ionic framework. So let’s add it to the project as a dependency. You can use any UI framework you prefer. Or you can skip this part and create the UI & functionality with HTML elements and pure CSS. Don’t worry, the app will function with or without a third-party UI framework. However, as a frontend developer, it helps to be familiar with using third-party UI frameworks like Ionic, Chakra, and ShadCN.

According to the Ionic framework documentation, here’s how to add Ionic to our existing Vue project:

  • Install Ionic into the project folder via the terminal using this command — npm install @ionic/vue @ionic/vue-router
  • Next, import it into your main.js file like so:
import { IonicVue } from '@ionic/vue';

import App from './App.vue';
import router from './router';

const app = createApp(App).use(IonicVue)
app.use(router);

router.isReady().then(() => {
  app.mount('#app');
});
Enter fullscreen mode Exit fullscreen mode
  • Since Ionic requires us to import routing dependencies from @ionic/vue-router instead of vue-router, we will also edit the existing routing code. Go to the index.js file in your router folder and edit it with the following:
import { createRouter, createWebHistory } from '@ionic/vue-router';

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes: [
      // routes go here
  ];
});

export default router;
Enter fullscreen mode Exit fullscreen mode

Note: leave your routes array unchanged from the setup earlier. The only changes: we now import the createRouter and createWebHistory from @ionic/vue-router, and we add process.env.BASE_URL as an argument in the createWebHistory.

Finally, let’s import the necessary CSS provided by Ionic into the main.js file

/* Core CSS required for Ionic components to work properly */
import '@ionic/vue/css/core.css';

/* Basic CSS for apps built with Ionic */
import '@ionic/vue/css/normalize.css';
import '@ionic/vue/css/structure.css';
import '@ionic/vue/css/typography.css';

/* Optional CSS utils that can be commented out */
import '@ionic/vue/css/padding.css';
import '@ionic/vue/css/float-elements.css';
import '@ionic/vue/css/text-alignment.css';
import '@ionic/vue/css/text-transformation.css';
import '@ionic/vue/css/flex-utils.css';
import @ionic/vue/css/display.css';
Enter fullscreen mode Exit fullscreen mode

I didn’t use any of the Optional CSS utils for this project, so you can delete them. I also didn’t use the import @ionic/vue/css/structure.css for this project, and you can also delete it. And we are set to use Ionic elements and styling going forward.

Remember, if you don’t want to use the Ionic framework, don’t do anything in this section to avoid breaking your code during build.


Building a Responsive NavBar For Your Vue App

The first component we will import into the App.vue is the 'NavBar'. Open the NavBar.vue component in the components folder, and add the <script>, <template> and <style> tags to the NavBar file. We will create the Options API and Composition API versions of this component.

Options API Version <script> Element of the NavBar Component

Write the following code inside the <script> tag of the NavBar

import { IonIcon } from '@ionic/vue'

export default {
  components: {
    IonIcon
  },
  data() {
    return {
      windowWidth: window.innerWidth,
      windowHeight: window.innerHeight,
      isOpen: false,
      ionIconStyle: {
        fontSize: '64px',
        color: '#000',
        '--ionicon-stroke-width': '16px'
      }
    }
  },
  mounted() {
    window.addEventListener('resize', this.handleWindowSizeChange)
  },
  beforeUnmount() {
    window.removeEventListener('resize', this.handleWindowSizeChange)
  },
  methods: {
    handleWindowSizeChange() {
      this.windowWidth = window.innerWidth
      this.windowHeight = window.innerHeight
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

A brief breakdown of the code:

  • The first line imports an icon from Ionic, which we will use in the <template>. Then we add it as a component to the export default code block.
  • The data() property contains logic that will help us create a responsive menu bar in place of the navbar when the user’s viewport is less than 768px.
  • Next, we use the mounted and beforeUnmount lifecycle hooks to add and remove eventListeners that will take effect as the window width changes.
  • In Vue Options API, the methods property contains functions, and in this code block, we added a function that handles the window size changes.

Composition API Version <script> Element of the NavBar Component

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { IonIcon } from '@ionic/vue'

const windowWidth = ref(window.innerWidth)
const windowHeight = ref(window.innerHeight)
const isOpen = ref(false)
const ionIconStyle = ref({
  fontSize: '64px',
  color: '#000',
  '--ionicon-stroke-width': '16px'
})

const handleWindowSizeChange = () => {
  windowWidth.value = window.innerWidth
  windowHeight.value = window.innerHeight
}

onMounted(() => {
  window.addEventListener('resize', handleWindowSizeChange)
})

onBeforeUnmount(() => {
  window.removeEventListener('resize', handleWindowSizeChange)
})
</script>
Enter fullscreen mode Exit fullscreen mode

While the Composition API is more flexible and remains the future of Vue.js, it doesn’t give us access to built-in features such as the lifecycle hooks without importing them. So, you will see that we had to import the lifecycle hooks in the Composition API version of the NavBar component. Additionally, we import a ref component that does the work of the data() property with a twist — we don’t use the ‘this’ keyword.

Finally, Let’s Add Content to the NavBar <template> Tag

<template>
  <div class="navBarContainer">
    <nav v-if="windowWidth > 768" class="largeViewportNav">
      <header>
        <h1>GitHub Repo Explorer</h1>
      </header>
      <ul class="navLink">
        <li><router-link class="navButton" to="/">Home</router-link></li>
        <li><router-link class="navButton" to="/error404">Test NotFound</router-link></li>
        <li>
          <router-link class="navButton" to="/errorBoundary">Test ErrorBoundary</router-link>
        </li>
      </ul>
    </nav>

    <nav v-else class="smallViewportNav">
      <header>
        <h1>GitHub Repo Explorer</h1>
        <ion-icon
          @click="isOpen = !isOpen"
          src="/menu-outline.svg"
          aria-label="MenuBar"
          aria-hidden="true"
          size="large"
          name="'menu-outline'"
          :style="ionIconStyle"
        ></ion-icon>
      </header>
      <transition name="fade" appear>
        <ul v-if="isOpen" class="navMenuItems">
          <li>
            <router-link class="navButton" to="/">Home</router-link>
          </li>

          <li>
            <router-link class="navButton" to="`/error404`">Test Not Found</router-link>
          </li>

          <li>
            <router-link class="navButton" to="/errorBoundary">Test Error Boundary</router-link>
          </li>
        </ul>
      </transition>
    </nav>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

You can see that instead of using the <a> tag to add relative paths, we used the <router-link> provided by the Vue-router. The Vue-router also gives us a <routerView> tag we can use to display the components on the browser, and we will see how it works when we edit the App.vue file next.

I used scoped styling in all the components and global styling in the App.vue file. If you want to see the style rulesets I used in the NavBar.vue component and the rest, you can check the source code on GitHub.

Add the NavBar Component to the App.vue

<!-- OPTIONS API VERSION -->
<script>
import NavBar from './components/NavBar.vue'

export default {
  components: {
    'nav-bar': NavBar
  }
}
</script>

<!-- COMPOSITION API VERSION -->
<!-- <script setup>
import NavBar from './components/NavBar.vue'
</script> -->

<template>
  <div>
    <nav-bar />
    <!-- <NavBar /> -->
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

We can’t see our NavBar in the browser just yet because the App.vue doesn’t have a default view. Remember we made the HomePage component the default page of the app. To make the App.vue display the default view in the browser, we need to add the routerView to the App.vue <template> tag like so:

<template>
  <div>
    <!-- OPTIONS API VERSION >
    <nav-bar />
    <router-view></router-view>

    <!-- COMPOSITION API VERSION >
    <!-- <NavBar /> >
    <!-- <routerView></routerView> -->
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Fetching and Displaying Data From the GitHub API in a Vue App

The RepoCards.vue component, which will display my public GitHub repositories, will be imported into the HomePage component. Before we do that, let’s write the code. We will also write the Options API and Composition API versions of this component.

Options API Version <script> Element of the RepoCards Component

<!-- OPTIONS API VERSION -->
<script>
import {
  IonButton,
  IonCard,
  IonCardContent,
  IonCardHeader,
  IonCardSubtitle,
  IonCardTitle
} from '@ionic/vue'

export default {
  components: { IonButton, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle },
  data() {
    return {
      repos: [],
      currentPage: 1,
      reposPerPage: 2,
      windowWidth: window.innerWidth,
      windowHeight: window.innerHeight,
      displayBlock: {
        display: 'block'
      },
      displayGrid: {
        display: 'grid',
        'grid-template-columns': '1fr 1fr'
      }
    }
  },
  methods: {
    async fetchRepos() {
      try {
        const response = await fetch('https://api.github.com/users/sheisbukki/repos')
        this.repos = await response.json()
      } catch (error) {
        console.log('Error fetching repositories:', error)
        throw error
      }
    },
    previousPageButton() {
      if (this.currentPage !== 1) this.currentPage--
    },
    nextPageButton() {
      if (this.currentPage !== Math.ceil(this.repos.length / this.reposPerPage)) this.currentPage++
    },
    paginationNumbers(pageNumber) {
      this.currentPage = pageNumber
    },
    handleWindowSizeChange() {
      this.windowWidth = window.innerWidth
      this.windowHeight = window.innerHeight
    }
  },
  mounted() {
    this.fetchRepos()
    window.addEventListener('resize', this.handleWindowSizeChange)
  },
  beforeUnmount() {
    window.removeEventListener('resize', this.handleWindowSizeChange)
  },
  computed: {
    paginatedRepos() {
      const indexOfLastRepo = this.currentPage * this.reposPerPage
      const indexOfFirstRepo = indexOfLastRepo - this.reposPerPage
      return this.repos.slice(indexOfFirstRepo, indexOfLastRepo)
    },
    pageNumbers() {
      const pageNumbers = []
      for (let i = 1; i <= Math.ceil(this.repos.length / this.reposPerPage); i++) {
        pageNumbers.push(i)
      }
      return pageNumbers
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

A brief breakdown of the code:

  • We import a few components from Ionic to create UI elements for this component such as the IonCard which will create card elements where we will display each repository.
  • The repos in the data() property is an empty array that will store the repositories fetched from the GitHub API.
  • The currentPage and reposPerPage variables help create the pagination element for the cards. The currentPage defines the page the pagination functionality will start from, while the reposPerPage defines the number of repositories each page should have.
  • The remaining variables in the data() property help us make the cards mobile responsive.
  • The async fetchRepos in the methods property is an asynchronous function that will try to fetch data from the GitHub API, particularly fetch my public repositories from GitHub. If successful, the data will be sent into the repos array, otherwise, it will throw an error.
  • The previousPageButton, nextPageButton functions handle the pagination buttons, while the paginationNumbers function defines the page number of the current page for the cards.
  • The handleWindowSizeChange function in the methods property handles the window size changes. The mounted and beforeUnmount lifecycle hooks add and remove eventListeners that will take effect as the window width changes. We also call the fetchRepos inside the mounted lifecycle hook.
  • The computed property contains two functions, paginatedRepos and pageNumbers which depend on the variables created in the data property — repos, currentPage, and reposPerPage. The paginatedRepos computed property returns a new array of paginated repositories, which we loop through to create the repository cards in the <template> element later. While the pageNumbers computed property returns an array of page numbers for each page of the pagination.

The computed property, although similar to the methods property, are used like the data property but is dynamic. The computed property updates automatically when a dependency changes. For example, if the size of the repos in this project increases or reduces, the pageNumbers returned from the computed property pageNumbers will also change. Similarly, if you have more than 15 public repositories, you can assign the reposPerPage key the value of 5, and this will reflect in both computed properties.

Composition API Version <script> Element of the RepoCards Component

<!-- COMPOSITION API VERSION -->
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import {
  IonButton,
  IonCard,
  IonCardContent,
  IonCardHeader,
  IonCardSubtitle,
  IonCardTitle
} from '@ionic/vue'

const repos = ref([])
const currentPage = ref(1)
const reposPerPage = ref(2)
const windowWidth = ref(window.innerWidth)
const windowHeight = ref(window.innerHeight)
const displayBlock = ref({
  display: 'block'
})
const displayGrid = ref({
  display: 'grid',
  'grid-template-columns': '1fr 1fr'
})

const fetchRepos = async function () {
  try {
    const response = await fetch('https://api.github.com/users/sheisbukki/repos')
    repos.value = await response.json()
  } catch (error) {
    console.log('Error fetching repositories:', error)
    throw error
  }
}

const previousPageButton = () => {
  if (currentPage.value !== 1) currentPage.value--
}

const nextPageButton = () => {
  if (currentPage.value !== Math.ceil(repos.value.length / reposPerPage.value)) currentPage.value++
}

const paginationNumbers = (pageNumber) => {
  currentPage.value = pageNumber
}

const handleWindowSizeChange = () => {
  windowWidth.value = window.innerWidth
  windowHeight.value = window.innerHeight
}

onMounted(() => {
  fetchRepos()
})

onBeforeUnmount(() => {
  window.removeEventListener('resize', handleWindowSizeChange)
})

const paginatedRepos = computed(() => {
  const indexOfLastRepo = currentPage.value * reposPerPage.value
  const indexOfFirstRepo = indexOfLastRepo - reposPerPage.value
  return repos.value.slice(indexOfFirstRepo, indexOfLastRepo)
})

const pageNumbers = computed(() => {
  const pageNumbers = []
  for (let i = 1; i <= Math.ceil(repos.value.length / reposPerPage.value); i++) {
    pageNumbers.push(i)
  }
  return pageNumbers
})
</script>
Enter fullscreen mode Exit fullscreen mode

Finally, Let’s Add Content to the RepoCards <template> Tag

<template>
  <main>
    <p v-if="!repos">Loading...</p>
    <div v-else>
      <section :style="windowWidth > 768 ? displayGrid : displayBlock" class="repoCardsContainer">
        <ion-card class="repoCard" color="dark" v-for="repo in paginatedRepos" :key="repo.id">
          <ion-card-header>
            <ion-card-title>{{ repo.name }}</ion-card-title>
            <ion-card-subtitle> Main language: {{ repo.language }} </ion-card-subtitle>
          </ion-card-header>
          <ion-card-content>{{ repo.description }}</ion-card-content>
          <ion-button fill="clear">
            <router-link :to="`/singleRepo/${repo.name}`">View more</router-link>
          </ion-button>
        </ion-card>
      </section>

      <section class="reposPagination">
        <ul class="paginationButtonsContainer">
          <ion-button
            class="paginationButton"
            aria-label="Previous page"
            fill="outline"
            shape="round"
            @click="previousPageButton"
            >&laquo;</ion-button
          >

          <li class="paginationButton" v-for="number in pageNumbers" :key="number">
            <ion-button fill="outline" shape="round" @click="paginationNumbers(number)">{{
              number
            }}</ion-button>
          </li>

          <ion-button
            class="paginationButton"
            aria-label="Next page"
            fill="outline"
            shape="round"
            @click="nextPageButton"
            >&raquo;</ion-button
          >
        </ul>
      </section>
    </div>
  </main>
</template>
Enter fullscreen mode Exit fullscreen mode

A brief breakdown of the code:

  • The v-if=“!repos” attribute and value is a v-if directive that will check if the repos array is null/falsy, and will return the <p> element if so. Otherwise, the v-else directive which is also added like an attribute in the <div> element will execute.
  • For the repository cards, I used the Ionic <ion-card> element and used the v-for directive to loop through the paginatedRepos created earlier and return each repository in a card. The shorthand v-bind : is used to bind the key attribute written as :key=“repo.id”, giving each repository card a unique ID, the same as the one provided by GitHub.
  • There’s an <ion-button> element in the <ion-card> element that contains a nested <router-link>. The <router-link> has a shorthand v-bind directive which binds the to attribute written as :to=“/singleRepo/${repo.name}. This is the nested route within each RepoCards component, defined in the router path: ‘/singleRepo/:name’, and the custom params for the nested route is ${repo.name}.
  • The <section class=“reposPagination> element creates the UI for the pagination functionality we created earlier. The <ul> element in it holds the pagination buttons using the Ionic <ion-button> elements which contain the previousPageButton and nextPageButton. The <ul> element also contains a <li> element which loops through the pageNumbers earlier.

Add the RepoCards Component to the HomePage.vue

<!-- OPTIONS API VERSION -->
<script>
import RepoCards from ./RepoCards.vue'
import ErrorBoundary from './ErrorBoundary.vue'

export default {
  components: {
    'repo-cards': RepoCards,
    errorBoundary: ErrorBoundary
  }
}
</script>

<!-- COMPOSITION API VERSION -->
<!-- <script setup>
import RepoCards from ‘./RepoCards.vue'
import ErrorBoundary from './ErrorBoundary.vue'
</script> -->

<template>
  <ErrorBoundary>
    <repo-cards />
  </ErrorBoundary>
</template>
Enter fullscreen mode Exit fullscreen mode

Note: we imported the ErrorBoundary component, why? We used it to wrap the RepoCards component, to monitor and report if there are any errors.

By now, your app should have a functional NavBar, and the HomePage should display the data you fetched from the GitHub API or RandomDataAPI in cards.

Implementing Nested Routes in a Vue Project

The SingleRepo component is already nested in the RepoCards but, it’s not functional yet, so let’s fix that. We will also write the Options API and Composition API versions.

Options API Version <script> Element of the SingleRepo Component

<!-- OPTIONS API VERSION -->

<script>
import {
  IonButton,
  IonCard,
  IonCardContent,
  IonCardHeader,
  IonCardSubtitle,
  IonCardTitle,
  IonLabel,
  IonItem,
  IonList
} from '@ionic/vue'
import ErrorBoundary from './ErrorBoundary.vue'

export default {
  components: {
    IonButton,
    IonCard,
    IonCardContent,
    IonCardHeader,
    IonCardSubtitle,
    IonCardTitle,
    IonLabel,
    IonItem,
    IonList,
    errorBoundary: ErrorBoundary
  },
  data() {
    return {
      repo: null
    }
  },
  methods: {
    fetchSingleRepo() {
      fetch(`https://api.github.com/repos/sheisbukki/${this.$route.params.name}`)
        .then((response) => response.json())
        .then((data) => {
          this.repo = data
        })
        .catch((error) => {
          console.error(error)
        })
    },
    ////THIS WORKS, JUST DECIDED TO USE THE ONE ABOVE
    // async fetchSingleRepo() {
    //   try {
    //     const response = await fetch(
    //       `https://api.github.com/repos/sheisbukki/${this.$route.params.name}`
    //     )
    //     this.repo = await response.json()
    //   } catch (error) {
    //     console.log('Error fetching repositories:', error)
    //     throw error
    //   }
    // },
    regularDate(dateValue) {
      return new Date(dateValue).toLocaleDateString('en-uk', {
        year: 'numeric',
        month: 'short',
        day: 'numeric'
      })
    }
  },
  mounted() {
    this.fetchSingleRepo()
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

A brief breakdown of the code:

  • The ErrorBoundary component is also imported to wrap the SingleRepo component, to monitor and report if there are any errors.
  • Any time the ‘View more’ button in the RepoCards component is clicked, the fetchSingleRepo function in the methods property of the SingleRepo component will indeed fetch the data of the specific repository clicked. Why two functions? Well, how else can we learn how to fetch API data using different approaches?
  • Notice any difference with the API the SingleRepo component is fetching from? It is also the GitHub API, but this time it uses the custom route params we defined in the router and specified in the RepoCards component to fetch data from my repositories.
  • The regularDate function inside the methods property converts the ISO date returned from GitHub to a date format people can easily understand.

Composition API Version <script> Element of the SingleRepo Component

<!-- COMPOSITION API VERSION -->
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import {
  IonButton,
  IonCard,
  IonCardContent,
  IonCardHeader,
  IonCardSubtitle,
  IonCardTitle,
  IonLabel,
  IonItem,
  IonList
} from '@ionic/vue'
import ErrorBoundary from './ErrorBoundary.vue'

const repo = ref(null)
const route = useRoute()

const fetchIndividualRepo = function () {
  fetch(`https://api.github.com/repos/sheisbukki/${route.params.name}`)
    .then((response) => response.json())
    .then((data) => {
      repo.value = data
    })
    .catch((error) => {
      console.error(error)
    })
}

onMounted(() => {
  fetchIndividualRepo()
})

const regularDate = (dateValue) => {
  return new Date(dateValue).toLocaleDateString('en-uk', {
    year: 'numeric',
    month: 'short',
    day: 'numeric'
  })
}
</script>
Enter fullscreen mode Exit fullscreen mode

Note: Unlike the Options API, you have to import the built-in useRoute component from Vue-router to enable custom route params in Composition API.

Finally, Let’s Add Content to the SingleRepo <template> Tag

<template>
  <ErrorBoundary>
    <main>
      <p v-if="!repo">Loading...</p>
      <div v-else>
        <h1>Repository</h1>
        <section>
          <ion-card color="dark">
            <ion-card-header>
              <ion-card-title>{{ repo.name }}</ion-card-title>
              <ion-card-subtitle>
                <strong>Main language:</strong> {{ repo.language }}
              </ion-card-subtitle>
            </ion-card-header>

            <ion-card-content>
              {{ repo.description }}
            </ion-card-content>

            <ion-card-content>
              <ion-list>
                <ion-item>
                  <em>Created on: </em>
                  <ion-label> {{ regularDate(repo.created_at) }}</ion-label>
                </ion-item>
                <ion-item>
                  <em>Pushed on: </em>
                  <ion-label> {{ regularDate(repo.pushed_at) }}</ion-label>
                </ion-item>
                <ion-item>
                  <em>Last updated on: </em>
                  <ion-label>{{ regularDate(repo.updated_at) }}</ion-label>
                </ion-item>
              </ion-list>
            </ion-card-content>

            <div class="cardFooter">
              <ion-button fill="clear">
                <a :href="repo.html_url">View source code</a>
              </ion-button>
              <em v-if="!repo.homepage">No live site</em>
              <ion-button v-else fill="clear">
                <a :href="repo.homepage">Visit live site</a>
              </ion-button>
            </div>
          </ion-card>
        </section>
        <footer :style="{ 'text-align': 'center' }">
          <ion-button fill="outline" shape="round" size="small"
            ><router-link to="/">Go back</router-link></ion-button
          >
        </footer>
      </div>
    </main>
  </ErrorBoundary>
</template>
Enter fullscreen mode Exit fullscreen mode

You can see that the <ErrorBoundary> element wraps the SingleRepo’s <template> element. This is enabled because the ErrorBoundary passes a <slot> in place of any components it is used to wrap.


Error Handling in Vue Using the ErrorBoundary.vue and NofFound.vue Components

First, let’s write the code for the ErrorBoundary Component, and this will also include the Options API and Composition API versions.

Options API Version <script> Element of the ErrorBoundary Component

<!-- OPTIONS API VERSION -->

<script>
export default {
  data() {
    return {
      error: null,
      errorInfo: '',
      errorInstance: null
    }
  },
  errorCaptured(error, instance, info) {
    this.error = error
    this.errorInfo = info
    this.errorInstance = instance
    console.log('error: ', error)
    console.log('component Instance: ', instance)
    console.log('errorSrcType: ', info)
    return false
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

The errorCaptured is a lifecycle hook we can use to track errors that happen in a child component, which is why the ErrorBoundary component uses <slot> to represent the child components. Developers can use create ErrorBoundary and errorHandler components such as this to log errors or display errors to users.

Composition API Version <script> Element of the ErrorBoundary Component

<!-- COMPOSITION API VERSION -->

<script setup>
import { ref, onErrorCaptured } from 'vue'

const error = ref(null)
const errorInfo = ref('')
const errorInstance = ref(null)

onErrorCaptured((error, instance, info) => {
  error.value = error
  errorInfo.value = info
  errorInstance.value = instance
  console.log('error: ', error)
  console.log('component Instance: ', instance)
  console.log('errorSrcType: ', info)
  return false
})
</script>
Enter fullscreen mode Exit fullscreen mode

Finally, Let’s Add Content to the ErrorBoundary <template> Tag

<template>
  <main>
    <div v-if="error">
      <h1>Something went wrong...</h1>
      <pre>{{ error }}</pre>
      <pre>{{ errorInstance }}</pre>
      <pre>{{ errorInfo }}</pre>
    </div>
    <div v-else>
      <slot></slot>
    </div>
  </main>
</template>
Enter fullscreen mode Exit fullscreen mode

The ErrorBoundary component uses <slot> to pass down content to the SingleRepo and RepoCards components. Hence, if there’s an error in either child component, the component will display the passed down content defined with the v-if=“error” directive in the ErrorBoundary <template>.

We can also track routing errors and general errors in the app by adding the following code to the main.js file:

router.onError((error) => {
  console.log('Router error:', error)
})

app.config.errorHandler = (error, compInstance, info) => {
  console.error('Error:', error)
  console.error('Component Instance:', compInstance)
  console.error('Error Info:', info)
}
Enter fullscreen mode Exit fullscreen mode

The NotFound Component

This component only has the <template> and <style scoped> elements. Check the source code of this project to see the styling for this component and the rest.

<template>
  <main>
    <h1>Error 404</h1>
    <p>Oops! Go back to <router-link to="/">Home</router-link></p>
  </main>
</template>
Enter fullscreen mode Exit fullscreen mode

And with this final touch, we have a fully functional Vue app.


Final Thoughts

Building a Vue app can be a rewarding experience, especially for beginners. This article guided you through the essentials, from setting up a Vue project from scratch using different methods to implementing both Options and Composition APIs.

To recap, these are the concepts this article covers:

  • Options API
  • Composition API
  • Scaffolding a Vue Project
  • Vue Router
  • Ionic Vue Framework
  • Pagination with Vue
  • Fetching data from API using JavaScript Fetch, and Async/Await
  • Creating Reusable Vue Components

By completing this project, you will not only build a functional, mobile-responsive Vue app but also gain valuable skills that will serve you in future development endeavours. I sure learnt a lot from building this project.

Shout out to you for building along with me. You can check out the live app here. 🎉

Top comments (0)