How to Build a Vue App To Show Your GitHub Repositories
Table of contents
- A Comprehensive Guide to Setting Up a Vue Project
- Options API vs Composition API
- Let’s Build Our Mobile Responsive Vue App
- Creating the Vue Instance and Mounting the App
- Create the Components for the App
- Setting Up the Vue Router in Your App
- Adding the Ionic Framework as a Dependency To Your Vue App
- Building a Responsive NavBar For Your Vue App
- Fetching and Displaying Data From the GitHub API in a Vue App
- Implementing Nested Routes in a Vue Project
- Error Handling in Vue Using the ErrorBoundary.vue and NotFound.vue Components
- Final Thoughts
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:
Have at least a basic understanding of HTML, CSS, and JavaScript
Know how to use a code editor
Be familiar with the command line and terminal
Have Node.js installed on your machine — Vite, the build tool we will use to build this project, runs on Node.js
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:
In an HTML file with the content delivery network (CDN)
Through the Vue CLI (Webpack/Babel)
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 the 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>
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)
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
ORyarn 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}
ORcd {an existing folder}
Open the directory using the following command —
cd {insert the new folder}
if you just created a new folderUse 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’.
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:
A
<template>
tag that will contain your typical HTML contentA
<script>
tag that contains your Vue logicA
<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}
ORcd {insert an existing directory}
in your terminal — this is where the project will livecd {insert the new directory name}
if you just created the directorynpm create vue@latest
ORpnpm create vue@latest
ORyarn create vue@latest
ORbun 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 foldernpm install
— this command installs the required dependencies to make your Vue project worknpm 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
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:
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
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.
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’)
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
Breakdown of the code:
The first line imports the
createRouter
andcreateWebHistory
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. Thename
is the name we gave each component. Themeta
adds SEO for each component as a page in the browser.The HomePage’s
path
is the defaultpath
— 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 aparams
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’)
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');
});
- Since Ionic requires us to import routing dependencies from
@ionic/vue-router
instead ofvue-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;
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';
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 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
}
}
}
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
andbeforeUnmount
lifecycle hooks to add and removeeventListeners
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>
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>
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>
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
component provided by the router 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>
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>
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 thedata()
property is an empty array that will store the repositories fetched from the GitHub API.The
currentPage
andreposPerPage
variables help create the pagination element for the cards. ThecurrentPage
defines the page the pagination functionality will start from, while thereposPerPage
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 themethods
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 therepos
array, otherwise, it will throw an error.The
previousPageButton
andnextPageButton
functions handle the pagination buttons, while thepaginationNumbers
function defines the page number of the current page for the cards.The
handleWindowSizeChange
function in themethods
property handles the window size changes. Themounted
andbeforeUnmount
lifecycle hooks add and removeeventListeners
that will take effect as the window width changes. We also call thefetchRepos
inside themounted
lifecycle hook.The
computed
property contains two functions,paginatedRepos
andpageNumbers
which depend on the variables created in thedata
property —repos
,currentPage
, andreposPerPage
. ThepaginatedRepos
computed property returns a new array of paginated repositories, which we loop through to create the repository cards in the<template>
element later. ThepageNumbers
computed property returns an array of page numbers for each page of the pagination.
The computed
property, although similar to the methods
property, is 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>
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"
>«</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"
>»</ion-button
>
</ul>
</section>
</div>
</main>
</template>
A brief breakdown of the code:
The
v-if=“!repos”
attribute and value is a v-if directive that will check if therepos
array is null/falsy, and will return the<p>
element if so. Otherwise, thev-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 thev-for
directive to loop through thepaginatedRepos
created earlier and return each repository in a card. The shorthand v-bind:
is used to bind thekey
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>
. We use the shorthand v-bind directive to bind theto
attribute of the<router-link>
element written as:to=“`/singleRepo/${repo.name}`”
. This is the nested route within each RepoCards component, defined in the routerpath: ‘/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 thepreviousPageButton
andnextPageButton
. The<ul>
element also contains a<li>
element which loops through thepageNumbers
created 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>
Note: we imported the ErrorBoundary component, why? We used it to wrap the RepoCards component, to monitor and report if there are any errors. We will also use it to wrap the SingleRepo component, and you will see how we do it next.
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>
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 themethods
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 themethods
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>
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>
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 NotFound.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>
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 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>
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>
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)
}
The NotFound Component
This component only has the <template>
and <style scoped>
elements. Check the source code for 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>
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 guides 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 well 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. 🎉
Subscribe to my newsletter
Read articles from Bukola Ogunleye directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Bukola Ogunleye
Bukola Ogunleye
Learning the ropes...