How To Create A GitHub User Search Web App Using Vue.js
In this short article, we are going to show you how you can use a Vue.js to create a simple GitHub user finder. Because we think that everyone already knows how to write on React, Svelte, Angular framework/libraries.
Well, how can I do without Vue? It’s time to fill this gap.
So, today we will create the same application using Vue, write tests for it on Cypress and slightly affect Vue CLI 3.
Preparation
First, install the latest Vue CLI:
npm i -g @vue/cli
And run the creation of the project:
vue create vue-github-search
We follow the steps of the generator. For our project, I chose a Manual mode and the following configuration:
Additional modules
As styles, we’ll use Stylus, so we’ll need a stylus and stylus-loader. We also need Axios for requests to the network and Lodash, from which we will take the debounce function.
Let’s move to the project folder and install the necessary packages:
cd vue-github-search npm i stylus stylus-loader axios lodash
Checking
Run the project and make sure that everything works:
npm run serve
All changes in the code will be instantly applied in the browser without reloading the page.
Store
To begin with, let’s write a vuex store where all the application data will be. We need nothing more to store: a search query, user data, and a boot process flag.
Open store.js and describe the initial state of the application and the necessary mutations:
... const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY'; const SET_LOADING = 'SET_LOADING'; const SET_USER = 'SET_USER'; const RESET_USER = 'RESET_USER'; export default new Vuex.Store({ state: { searchQuery: '', loading: false, user: null }, mutations: { [SET_SEARCH_QUERY]: (state, searchQuery) => state.searchQuery = searchQuery, [SET_LOADING]: (state, loading) => state.loading = loading, [SET_USER]: (state, user) => state.user = user, [RESET_USER]: state => state.user = null } });
We add an action for uploading data from the GitHub API and to change the search query (we need it for the search line). As a result, our store will look like this:
import Vue from 'vue'; import Vuex from 'vuex'; import axios from 'axios'; Vue.use(Vuex); const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY'; const SET_LOADING = 'SET_LOADING'; const SET_USER = 'SET_USER'; const RESET_USER = 'RESET_USER'; export default new Vuex.Store({ state: { searchQuery: '', loading: false, user: null }, mutations: { [SET_SEARCH_QUERY]: (state, searchQuery) => state.searchQuery = searchQuery, [SET_LOADING]: (state, loading) => state.loading = loading, [SET_USER]: (state, user) => state.user = user, [RESET_USER]: state => state.user = null }, actions: { setSearchQuery({commit}, searchQuery) { commit(SET_SEARCH_QUERY, searchQuery); }, async search({commit, state}) { commit(SET_LOADING, true); try { const {data} = await axios.get(`https://api.github.com/users/${state.searchQuery}`); commit(SET_USER, data); } catch (e) { commit(RESET_USER); } commit(SET_LOADING, false); } } });
Search line
Create a new Search.vue component in the components folder. Add a computed property to associate the component with the store. When changing the search query, we will call the search with debouncing.
<template> <input v-model="query" @input="debouncedSearch" placeholder="Enter username" /> </template> <script> import {mapActions, mapState} from 'vuex'; import debounce from 'lodash/debounce'; export default { name: 'search', computed: { ...mapState(['searchQuery']), query: { get() { return this.searchQuery; }, set(val) { return this.setSearchQuery(val); } } }, methods: { ...mapActions(['setSearchQuery', 'search']), debouncedSearch: debounce(function () { this.search(); }, 500) } }; </script> <style lang="stylus" scoped> input width 100% font-size 16px text-align center </style>
Now we connect our search line to the main component of App.vue and simultaneously delete the extra lines created by the generator.
<template> <div id="app"> <Search /> </div> </template> <script> import Search from './components/Search'; export default { name: 'app', components: { Search } }; </script> <style lang="stylus"> #app font-family 'Avenir', Helvetica, Arial, sans-serif font-smoothing antialiased margin 10px </style>
Look at the result in the browser, making sure that everything works with vue-devtools:
As you can see, we already have all the logic of the application! We enter the username, the query is executed, and the profile data is stored in the store.
User Profile
Create a User.vue component and add logic to indicate the load, display the profile and an error when the user is not found. Also, we will add an animation of transitions.
<template> <div class="github-card"> <transition name="fade" mode="out-in"> <div v-if="loading" key="loading"> Loading </div> <div v-else-if="user" key="user"> <div class="background" :style="{backgroundImage: `url(${user.avatar_url})`}" /> <div class="content"> <a class="avatar" :href="`https://github.com/${user.login}`" target="_blank"> <img :src="user.avatar_url" :alt="user.login" /> </a> <h1>{{user.name || user.login}}</h1> <ul class="status"> <li> <a :href="`https://github.com/${user.login}?tab=repositories`" target="_blank"> <strong>{{user.public_repos}}</strong> <span>Repos</span> </a> </li> <li> <a :href="`https://gist.github.com/${user.login}`" target="_blank"> <strong>{{user.public_gists}}</strong> <span>Gists</span> </a> </li> <li> <a :href="`https://github.com/${user.login}/followers`" target="_blank"> <strong>{{user.followers}}</strong> <span>Followers</span> </a> </li> </ul> </div> </div> <div v-else key="not-found"> User not found </div> </transition> </div> </template> <script> import {mapState} from 'vuex'; export default { name: 'User', computed: mapState(['loading', 'user']) }; </script> <style lang="stylus" scoped> .github-card margin-top 50px padding 20px text-align center background #fff color #000 position relative h1 margin 16px 0 20px line-height 1 font-size 24px font-weight 500 .background filter blur(10px) opacity(50%) z-index 1 position absolute top 0 left 0 right 0 bottom 0 background-size cover background-position center background-color #fff .content position relative z-index 2 .avatar display inline-block overflow hidden background #fff border-radius 100% text-decoration none img display block width 80px height 80px .status background white ul text-transform uppercase font-size 12px color gray list-style-type none margin 0 padding 0 border-top 1px solid lightgray border-bottom 1px solid lightgray zoom 1 &:after display block content '' clear both li width 33% float left padding 8px 0 box-shadow 1px 0 0 #eee &:last-of-type box-shadow none strong display block color #292f33 font-size 16px line-height 1.6 a color #707070 text-decoration none &:hover color #4183c4 .fade-enter-active, .fade-leave-active transition opacity .5s .fade-enter, .fade-leave-to opacity 0 </style>
Connect our component in App.vue and enjoy the result:
<template> <div id="app"> <Search /> <User /> </div> </template> <script> import Search from './components/Search'; import User from './components/User'; export default { name: 'app', components: { User, Search } }; </script> <style lang="stylus"> #app font-family 'Avenir', Helvetica, Arial, sans-serif font-smoothing antialiased margin 10px </style>
Tests
Write simple tests for our application.
describe('Github User Search', () => { it('has input for username', () => { cy.visit('/'); cy.get('input'); }); it('has "User not found" caption', () => { cy.visit('/'); cy.contains('User not found'); }); it("finds Linus Torvalds' GitHub page", () => { cy.visit('/'); cy.get('input').type('torvalds'); cy.contains('Linus Torvalds'); cy.get('img'); cy.contains('span', 'Repos'); cy.contains('span', 'Gists'); cy.contains('span', 'Followers'); }); it("doesn't find nonexistent page", () => { cy.visit('/'); cy.get('input').type('_some_random_name_6m92msz23_2'); cy.contains('User not found'); }); });
Run the tests command
npm run test:e2e
In the opened window, click the button Run all specs and see that the tests pass:
Assembly
Vue CLI 3 supports a new mode of assembly of the application, modern mode. It creates 2 versions of scripts: lightweight for modern browsers that support the latest JavaScript features, and a full version with all the necessary polyphyly for older ones. The main advantage is that we absolutely do not need to bother with the release of such an application. It just works. If the browser supports <script type = “module”>, it will itself lighten the lightweight build. How it works, you can read more in this article.
Add the modern flag to the build command in package.json:
"build": "vue-cli-service build --modern"
We are collecting the project:
npm run build
Let’s look at the size of the resulting scripts:
8.0K ./app-legacy.cb7436d4.js 8.0K ./app.b16ff4f7.js 116K ./chunk-vendors-legacy.1f6dfb2a.js 96K ./chunk-vendors.a98036c9.js
As you can see, the new method really reduces the size of the assembly. The difference will be even more noticeable on large projects, so the feature definitely deserves attention.
References/code
GitHub repository