Build a Cryptocurrency Comparison Site with Vue.js

In this article, I'll walk you through the basics of Vue.js, and in the process we'll build a very simple single page application that compares the prices of the top 10 cryptocurrencies. To keep things simple, we'll just be using plain old HTML, CSS, and Javascript for this – nothing fancy required.

In this article, I'll walk you through the basics of Vue.js, and in the process we'll build a very simple single page application that compares the prices of the top 10 cryptocurrencies. To keep things simple, we'll just be using plain old HTML, CSS, and Javascript for this – nothing fancy required.

Who is Vue Built For?

Vue.js is a framework designed to handle the view layer of your web applications. This means Vue will only handle things related to displaying the user interface on the page.

Vue uses plain old HTML, and doesn't require you to learn another language (like JSX with React), and can therefore be picked up by web designers and other front-end developers who are familiar with HTML already.

If you're looking for a simple Javascript framework to help you dynamically display data, you can't really go wrong with Vue:

Vue is simple, fast, and well documented.

Vue Basics

Let's take a look at a minimal Vue.js web page:

<html>
  <body>
    <!-- All that Vue cares about is what is inside this div. -->
    <div id="app">
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
      let app = new Vue({
        el: "#app"
      });
    </script>
  </body>
</html>

This minimal web page will initialize Vue.js fully, and have it manage everything inside of the app div. The el parameter in the Vue constructor tells Vue to bind itself to the element (in this case a div) whose id is app.

When Vue starts up, it will begin scanning and managing all the code inside this div.


Data Management

One of the core things Vue does is manage data. In order to tell Vue what data you want to manage, you need to explicitly declare every bit of data you expect Vue to dynamically manage for you upfront in a special data object, like so:

<html>
  <body>
    <div id="app">
      <h1>{{ message }}</h1>
    </div>
&lt;script src="https://unpkg.com/vue"&gt;&lt;/script&gt;
&lt;script&gt;
  let app = new Vue({
    el: "#app",
    data: {
      message: "Hi!"
    }
  });
&lt;/script&gt;

</body>
</html>

As you can see in the example above, I'm defining a new data object inside the Vueconstructor, and defining my data inside of it. In this case, I'm just telling Vue that I'd like it to manage a message for me.

You'll also notice that my HTML code now contains a variable: {{ message }}. After Vue starts up, and scans the app div for code, it will start replacing any variables it finds with the related data objects you've defined.

If you open the Javascript console in your browser, and attempt to modify the message value like so:

app.message = "yo";

You'll notice that the contents of the page will change!

This happens because Vue is managing that data for you. When the data changes, Vue re-renders the variable on the page.

This behavior makes it really easy to build dynamic web applications, since any time you change data in Javascript, the page is re-rendered on your behalf and you don't need to worry about any sort of UI management.

Conditionals

In addition to managing pieces of data, Vue also supports writing programming logic in your HTML templates.

Vue supports typical if, else if, and else conditionals using the v-if, v-else-if, and v-elsesyntax.

Let's take a look at a simple Vue application that uses an if-else conditional.

<html>
<body>
<div id="app">
<h1>{{ message }}</h1>
<p v-if="secretMessage">This is a secret HTML element.</p>
<p v-else>Welcome to the website.</p>
</div>

&lt;script src="https://unpkg.com/vue"&gt;&lt;/script&gt;
&lt;script&gt;
  let app = new Vue({
    el: "#app",
    data: {
      message: "Hi!",
      secretMessage: false
    }
  });
&lt;/script&gt;

</body>
</html>

As you can see above, we now have two paragraph tags, each with a different message. If you run this in your browser, you'll see the following:

Because the secretMessage variable is false, the conditional statement in HTML will fail to execute, and the else statement code will be ran, outputting the HTML element with the welcome message.

Since we know Vue is dynamic, we can now open the Javascript console, modify the value of secretMessage, and BAM, the page will re-render with the secret message being shown.

One important thing to note: when using Vue conditionals, the DOM will be modified. In the example above, when we enable the secretMessage flag and show the message, the previously shown paragraph tag will be completely removed from the DOM.


Looping

Vue also supports simple loops. Here's a small example application that uses Vue to loop through an array of shopping items, displaying them in an ordered list:

<html>
<body>
<div id="app">
<p>Shopping list</p>
<ol>
<li v-for="item in shoppingList">{{ item }}</li>
</ol>
</div>

&lt;script src="https://unpkg.com/vue"&gt;&lt;/script&gt;
&lt;script&gt;
  let app = new Vue({
    el: "#app",
    data: {
      shoppingList: [
        "milk",
        "eggs",
        "steak",
        "chicken"
      ]
    }
  });
&lt;/script&gt;

</body>
</html>

When this runs in the browser, you'll see that the v-for directive Vue provides will repeat itself, looping over the contents of the shoppingList array:

If you go modify the shoppingList array in the Javascript console, you'll notice the same behavior as before: Vue will re-render the shopping list for you automatically.


Two-Way Data Management

The next basic concept we'll cover is two-way data binding. Up until now, you've seen that whatever data you define in Vue will appropriately render when changed. This is really useful, but there's a little bit more we can do.

In some circumstances, like when accepting user input, you'll also want Vue to manage user controlled data. This is called two way data binding: when both the user (and you) can manage the same pieces of data in your application.

Let's take a look at a simple Vue application that uses two-way data binding:

<html>
<body>
<div id="app">
<p>What's your favorite color?</p>
<input v-model="color" type="text">
<p>Your favorite color is... {{ color }}</p>
</div>

&lt;script src="https://unpkg.com/vue"&gt;&lt;/script&gt;
&lt;script&gt;
  let app = new Vue({
    el: "#app",
    data: {
      color: ''
    }
  });
&lt;/script&gt;

</body>
</html>

As you can see if you try this out yourself, when you enter data into the input box, the colorvariable is changed, and then re-rendered on the page. This is two-way data binding.

If you go into the Javascript console and modify this directly yourself, you'll see it change again.


Doing Things with Methods

Now that we've covered some of the fundamental Vue.js properties, let's talk about something a little more interesting: Vue methods.

In addition to managing data, Vue also provides a convenient way to structure your Javascript actions.

Let's take a look at a very simple example application that uses a method to capitalize the color from the example above when clicked:

<html>
<body>
<div id="app">
<p>What's your favorite color?</p>
<input v-model="color" type="text">
<p>Your favorite color is... {{ color }}</p>
<input type="button" v-on:click="capitalizeColor" value="Capitalize">
</div>

&lt;script src="https://unpkg.com/vue"&gt;&lt;/script&gt;
&lt;script&gt;
  let app = new Vue({
    el: "#app",
    data: {
      color: ''
    },
    methods: {
      capitalizeColor: function() {
        this.color = this.color.toUpperCase();
      }
    }
  });
&lt;/script&gt;

</body>
</html>

In this example, we define a function in the methods object of Vue, which simply capitalizes the color variable from before. We then use the v-on:click attribute on out input tag to tell Vue that when the button is clicked, it should run that method.

Pretty straight-forward, right?


Build a Cryptocurrency Comparison Website with Vue

Now that we've had a chance to look at some basic Vue.js patterns and usage, let's try to build something with our new found knowledge!

I've been using Bitcoin for many years now, and enjoy working with it, so I thought it'd be fun to hack together a little single page app that displays the top 10 cryptocurrencies as well as their price data.

This could be a useful dashboard for someone who's considering purchasing cryptocurrencies for speculative purposes, as it gives you an idea of how these things are valued at any given time.

So… Let's get started!

NOTE: If you'd rather just look at the completed project on Github, you can check it out at github.com/rdegges/cryptocompare.


Bootstrapping

The first thing we'll do is get our page bootstrapped with some basic HTML, Javascript libraries, etc.

Because I'm not a web designer (and have poor visual taste!), we'll be using Twitter's Bootstraplibrary for basic styling. We'll also be using Vue.js, and a few other small helper libraries which will be explained later on:

  • vue2-filters, a simple Vue.js library that provides some useful template filters for displaying text. In this app, I only the filter for helping to display currency values nicely.
  • axios, a popular Javascript library for making HTTP requests

All in all, our basic HTML page with all our dependencies included will look like this:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CryptoCompare</title>

&lt;link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"&gt;
&lt;link rel="stylesheet" href="https://bootswatch.com/simplex/bootstrap.min.css"&gt;
&lt;link rel="stylesheet" href="/static/css/style.css"&gt;

&lt;!--[if lt IE 9]&gt;
  &lt;script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"&gt;&lt;/script&gt;
  &lt;script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"&gt;&lt;/script&gt;
&lt;![endif]--&gt;

</head>
<body class="container">
<h1>Crypto Compare</h1>

&lt;div class="row"&gt;
  &lt;div class="jumbotron col-xs-offset-2 col-xs-8"&gt;
    &lt;p&gt;
      This website indexes the top 10 cryptocurrencies by market cap (how
      much the sum of all coins is collectively worth), and gives you an easy
      way to compare cryptocurrency performance and rank over the last week.
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div id="app"&gt;
&lt;/div&gt;

&lt;script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"&gt;&lt;/script&gt;
&lt;script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"&gt;&lt;/script&gt;
&lt;script src="https://unpkg.com/vue"&gt;&lt;/script&gt;
&lt;script src="/static/js/vue2-filters.min.js"&gt;&lt;/script&gt;
&lt;script src="https://unpkg.com/axios/dist/axios.min.js"&gt;&lt;/script&gt;
&lt;script src="/static/js/app.js"&gt;&lt;/script&gt;

</body>
</html>

We've got all our dependencies included, we've got a basic HTML page, and we're ready to start doing some real development work.


Design Your Page

The first thing I like to do after bootstrapping my page is to lay out the elements where they belong. In this case, since we'll be building a ranked list of the top cryptocurrencies, we'll have a very simple design. It's just an HTML table that lists the top cryptocurrencies, and their associated stats.

Here's the modified HTML and CSS, and what it looks like after defining these basic elements:

<div id="app">
<table class="table table-hover">
<thead>
<tr>
<td>Rank</td>
<td>Name</td>
<td>Symbol</td>
<td>Price (USD)</td>
<td>1H</td>
<td>1D</td>
<td>1W</td>
<td>Market Cap (USD)</td>
</thead>
<tbody>
</tbody>
</table>
</div>


/* /static/css/style.css */

h1 {
text-align: center;
}

td img {
width: 25px;
}

.jumbotron p {
font-size: 1.2em;
}

.jumbotron {
margin-top: 5em;
margin-bottom: 5em;
}

We're off to a good start! We've now got our basic website layout done, and we have a good idea of what data we'll need to make this work, etc.


Fetch Cryptocurrency Data

In order to build the rest of this app, we need a way to retrieve a list of the top cryptocurrencies, as well as their stats. For this project, I thought it'd be fun to show the following:

  • The top 10 cryptocurrencies by market cap (the total worth of all coins for the given currency)
  • Each currency's name
  • Each currency's symbol (this is how people typically refer to the currencies on trading platforms)
  • The current price of a single coin in each currency, in USD
  • The percent change in value of the currency over the last hour, day, and week

Luckily for us, there's a great free API service that provides this exact data. It's called CoinMarketCap. It doesn't require any registration or setup, you can just read through their API documentation, figure out how the API endpoints work, and plug it into whatever you're building.

We'll need to use the ticker API provided by CoinMarketCap, which will allow us to grab a list of the top 10 cryptocurrencies, and display their stats. Here's what this API call looks like, and what it returns:

[
{
"id": "bitcoin",
"name": "Bitcoin",
"symbol": "BTC",
"rank": "1",
"price_usd": "2747.54",
"price_btc": "1.0",
"24h_volume_usd": "2242640000.0",
"market_cap_usd": "45223373666.0",
"available_supply": "16459587.0",
"total_supply": "16459587.0",
"percent_change_1h": "-2.83",
"percent_change_24h": "19.78",
"percent_change_7d": "17.2",
"last_updated": "1500596647"
},
...
]

As you can see above, the resulting JSON is an array of the top 10 cryptocurrencies. Each JSON object in the array represents one currency, and contains all the information we need.

There is one thing we're missing though. To make our table look nice, I'd also like to display the logo of each cryptocurrency next to their name.

Unfortunately, the CoinMarketCap API doesn't have any images, so I did a little Google-ing around and found another API we can use to retrieve metadata about tons of different cryptocurrencies, including their logos!

The CryptoCompare API provides us this data. Much like CoinMarketCap, there's no registration required.

The API call we'll use to retrieve data about all the cryptocurrencies looks like this:

$ curl https://www.cryptocompare.com/api/data/coinlist
{
...
"Data": {
"AVT": {
"Id": "138642",
"Url": "/coins/avt/overview",
"ImageUrl": "/media/1383599/avt.png",
"Name": "AVT",
"CoinName": "AventCoin",
"FullName": "AventCoin (AVT)",
"Algorithm": "N/A",
"ProofType": "N/A",
"FullyPremined": "0",
"TotalCoinSupply": "10000000",
"PreMinedValue": "N/A",
"TotalCoinsFreeFloat": "N/A",
"SortOrder": "1266"
},
...
}
}

With the new data we have from the API call above, we can correlate a cryptocurrency's symbol (for instance BTC, AVI, etc.) with an ImageUrl that shows a picture of that currency.

Using this new data, plus the data from CoinMarketCap, we now have everything we need!


Set Up Your Vue App

Now that we know how to grab the data we need, let's start building out our Vue app. To keep things simple, all our Vue code will be placed into the file static/js/app.js.

The first thing we need to do here is scaffold our Vue app, and tell it what variables we expect it to manage:

/**

  • Our Vue.js application.
  • This manages the entire front-end website.
    */

// The API we're using for grabbing metadata about each cryptocurrency
// (including logo images). The service can be found at:
// https://www.cryptocompare.com/api/
let CRYPTOCOMPARE_API_URI = "https://www.cryptocompare.com";

// The API we're using for grabbing cryptocurrency prices. The service can be
// found at: https://coinmarketcap.com/api/
let COINMARKETCAP_API_URI = "https://api.coinmarketcap.com";

// The amount of milliseconds (ms) after which we should update our currency
// charts.
let UPDATE_INTERVAL = 60 * 1000;

let app = new Vue({
el: "#app",
data: {
coins: [],
coinData: {}
},
methods: {

/**
 * Load up all cryptocurrency data.  This data is used to find what logos
 * each currency has, so we can display things in a friendly way.
 */
getCoinData: function() {
},

/**
 * Get the top 10 cryptocurrencies by value.  This data is refreshed each 5
 * minutes by the backing API service.
 */
getCoins: function() {
},

/**
 * Given a cryptocurrency ticket symbol, return the currency's logo
 * image.
 */
getCoinImage: function(symbol) {
}

}
});

The two variables we're going to have Vue manage for us to make this possible are:

  • coins - which will be an array of all the different types of cryptocurrencies (bitcoin, ethereum, etc.), and
  • coinData - which will be an object loaded from the CryptoCompare API service we looked at earlier. Using this we'll be able to cross reference cryptocurrency data to get a logo image. We're also going to define three stub methods that we'll need to implement:
  • getCoinData - which will retrieve the coin data from CryptoCompare
  • getCoins - which will load the coin data from CoinMarketCap
  • getCoinImage - which takes a currency symbol and returns a link to that currency's logo image

With these three methods in place, we can now begin writing some software!


Implement Your Coin Methods

Let's start by implementing the getCoins method that will talk to the CoinMarketCap API. We know from before that all we need to do is hit the API and store the resulting array of JSON data in our Vue variable.

To make API requests, I like to use the axios Javascript library (mentioned earlier on in this article). It's a very popular library for making HTTP requests in client side Javascript.

Since I've already loaded it up for us in the web scaffold, we can use it directly in our Vue code:

getCoins: function() {
let self = this;

axios.get(COINMARKETCAP_API_URI + "/v1/ticker/?limit=10")
.then((resp) => {
this.coins = resp.data;
})
.catch((err) => {
console.error(err);
});
},

As you can see, we can easily use axios to issue a GET request for us, grab the resulting array of data, and then store it as self.coins (which updates the Vue coins variable). Once that variable change occurs, Vue will re-render any part of our page that's dependent upon it.

Next, let's implement our other method: getCoinData, which retrieves metadata about many different types of cryptocurrencies, and will be used to find their logo images:

getCoinData: function() {
let self = this;

axios.get(CRYPTOCOMPARE_API_URI + "/api/data/coinlist")
.then((resp) => {
this.coinData = resp.data.Data;
this.getCoins();
})
.catch((err) => {
this.getCoins();
console.error(err);
});
}

This is another simple API call: we make the request and update the data. One thing we will do here, however, is call the getCoins method as well. We do this because we'll only need to call this method once when the page has loaded (to load up all the currency metadata), but will need to call the getCoins method many times to keep the data up-to-date, and we should only call it once the metadata about all coins has already been loaded.

The last major coin method we need to implement is getCoinImage, which takes in a coin's symbol and returns the full image URL. Here's how we do that:

getCoinImage: function(symbol) {
return CRYPTOCOMPARE_API_URI + this.coinData[symbol].ImageUrl;
}

This code takes care of the cross-referencing between both API services, and allows us to easily retrieve a cryptocurrency's image.


Load Data When the App Starts

Now that we've built some of our data-retrieval methods, we need to actually run them. Otherwise: oup app will open, but nothing will happen.

In Vue, we can bind actions to certain events that happen on the page. One of these lifecycle events is called “created”, and we can use this lifecycle hook to run our code once the Vue app has been fully initialized in the browser.

Here's what it looks like:

let app = new Vue({
// ...
created: function() {
this.getCoinData();
}
});

This is exactly what we need, because once the page is loaded, we'll call our getCoinDatamethod which will retrieve all coin image URls, then we'll call our getCoins method which will load the initial data we need to eventually display on the page.

Finally, we need to ensure that we actually keep the data on the page updating. The CryptoMarketCap API service says that their data is refreshed once every 5 minutes, so as to not be aggressive, we'll be querying their API once per minute to display new results to our users.

We can do this easily outside of our Vue app using a plain old call to Javascript's setIntervalfunction:

/**

  • Once the page has been loaded and all of our app stuff is working, we'll
  • start polling for new cryptocurrency data every minute.

/
setInterval(() => {
app.getCoins();
}, UPDATE_INTERVAL);

Notice how we're able to run our Vue method outside of our Vue app by calling it off the Vue app object. Vue publicly exports all of your data and methods so they can be used outside Vue.


Displaying Your Data

Now that we've built the data management side of things, let's hop back into our HTML code and actually display some of this shiny new data for our users.

Using the same constructs we learned earlier, we're going to loop through the cryptocurrency data, filling out our table:

<table class="table table-hover">
<thead>
<tr>
<td>Rank</td>
<td>Name</td>
<td>Symbol</td>
<td>Price (USD)</td>
<td>1H</td>
<td>1D</td>
<td>1W</td>
<td>Market Cap (USD)</td>
</thead>
<tbody>
<tr v-for="coin in coins">
<td>{{ coin.rank }}</td>
<td><img v-bind:src="getCoinImage(coin.symbol)"> {{ coin.name }}</td>
<td>{{ coin.symbol }}</td>
<td>{{ coin.price_usd | currency }}</td>
<td>
<span v-if="coin.percent_change_1h > 0">+</span>{{ coin.percent_change_1h }}%
</td>
<td>
<span v-if="coin.percent_change_24h > 0">+</span>{{ coin.percent_change_24h }}%
</td>
<td>
<span v-if="coin.percent_change_7d > 0">+</span>{{ coin.percent_change_7d }}%
</td>
<td>{{ coin.market_cap_usd | currency }}</td>
</tr>
</tbody>
</table>

Pretty straightforward, right?

The only new thing here is the v-bind directive. If you're wondering what that does, it tells Vue to run the getCoinImage function, grab the result, and use that result for the img tag's src attribute (this is how we're able to display the logo to the user).

The final thing we'll want to do now is clean the coloring up a bit:

  • If the percent change in a currency is positive, we should color it green
  • If the percent change in a currency is negative, we should color it red

This adds a bit more visual flair to the page, and makes it a little easier to eyeball the performance of a currency.

So, let's quickly build a method and plug it into our HTML:

/*

  • Return a CSS color (either red or green) depending on whether or
  • not the value passed in is negative or positive.
    */
    getColor: (num) => {
    return num > 0 ? "color:green;" : "color:red;";
    }


<tbody>
<tr v-for="coin in coins">
<td>{{ coin.rank }}</td>
<td><img v-bind:src="getCoinImage(coin.symbol)"> {{ coin.name }}</td>
<td>{{ coin.symbol }}</td>
<td>{{ coin.price_usd | currency }}</td>
<td v-bind:style="getColor(coin.percent_change_1h)">
<span v-if="coin.percent_change_1h > 0">+</span>{{ coin.percent_change_1h }}%
</td>
<td v-bind:style="getColor(coin.percent_change_24h)">
<span v-if="coin.percent_change_24h > 0">+</span>{{ coin.percent_change_24h }}%
</td>
<td v-bind:style="getColor(coin.percent_change_7d)">
<span v-if="coin.percent_change_7d > 0">+</span>{{ coin.percent_change_7d }}%
</td>
<td>{{ coin.market_cap_usd | currency }}</td>
</tr>
</tbody>

With these final changes, load the code up in your browser, and give it a go!

If you play around with the website a bit, you'll notice that every 60 seconds the data is updated as designed.

In the event one currency takes over another's ranking, all the items will shift seamlessly on the page (you can play around with this in the Javascript console by modifying app.coins directly if you want).

Put It All Together

I hope you had fun learning a bit about Vue, and seeing how to use it to build basic web apps.

If you're a web developer looking to organize your front-end logic in a simpler and maintainable way, I strongly recommend you give Vue.js a try.


Thank you!


Top Vue.js Developers in USA

Top Vue.js Developers in USA

Vue.js is an extensively popular JavaScript framework with which you can create powerful as well as interactive interfaces. Vue.js is the best framework when it comes to building a single web and mobile apps.

We, at HireFullStackDeveloperIndia, implement the right strategic approach to offer a wide variety through customized Vue.js development services to suit your requirements at most competitive prices.

Vue.js is an open-source JavaScript framework that is incredibly progressive and adoptive and majorly used to build a breathtaking user interface. Vue.js is efficient to create advanced web page applications.

Vue.js gets its strength from the flexible JavaScript library to build an enthralling user interface. As the core of Vue.js is concentrated which provides a variety of interactive components for the web and gives real-time implementation. It gives freedom to developers by giving fluidity and eases the integration process with existing projects and other libraries that enables to structure of a highly customizable application.

Vue.js is a scalable framework with a robust in-build stack that can extend itself to operate apps of any proportion. Moreover, vue.js is the best framework to seamlessly create astonishing single-page applications.

Our Vue.js developers have gained tremendous expertise by delivering services to clients worldwide over multiple industries in the area of front-end development. Our adept developers are experts in Vue development and can provide the best value-added user interfaces and web apps.

We assure our clients to have a prime user interface that reaches end-users and target the audience with the exceptional user experience across a variety of devices and platforms. Our expert team of developers serves your business to move ahead on the path of success, where your enterprise can have an advantage over others.

Here are some key benefits that you can avail when you decide to hire vue.js developers in USA from HireFullStackDeveloperIndia:

  • A team of Vue.js developers of your choice
  • 100% guaranteed client satisfaction
  • Integrity and Transparency
  • Free no-obligation quote
  • Portal development solutions
  • Interactive Dashboards over a wide array of devices
  • Vue.js music and video streaming apps
  • Flexible engagement model
  • A free project manager with your team
  • 24*7 communication with your preferred means

If you are looking to hire React Native developers in USA, then choosing HireFullStackDeveloperIndia would be the best as we offer some of the best talents when it comes to Vue.js.

How To Publish Your Vue.js Component On NPM - Vue.js Developers

How To Publish Your Vue.js Component On NPM - Vue.js Developers

How To Publish Your Vue.js Component On NPM. You’ve made an awesome component with Vue.js that you think other developers could use in their projects. How can you share it with them?

How to build Vue.js JWT Authentication with Vuex and Vue Router

How to build Vue.js JWT Authentication with Vuex and Vue Router

In this tutorial, we’re gonna build a Vue.js with Vuex and Vue Router Application that supports JWT Authentication

In this tutorial, we’re gonna build a Vue.js with Vuex and Vue Router Application that supports JWT Authentication. I will show you:

  • JWT Authentication Flow for User Signup & User Login
  • Project Structure for Vue.js Authentication with Vuex & Vue Router
  • How to define Vuex Authentication module
  • Creating Vue Authentication Components with Vuex Store & VeeValidate
  • Vue Components for accessing protected Resources
  • How to add a dynamic Navigation Bar to Vue App

Let’s explore together.

Contents

Overview of Vue JWT Authentication example

We will build a Vue application in that:

  • There are Login/Logout, Signup pages.
  • Form data will be validated by front-end before being sent to back-end.
  • Depending on User’s roles (admin, moderator, user), Navigation Bar changes its items automatically.

Screenshots

– Signup Page:

– Login Page & Profile Page (for successful Login):

– Navigation Bar for Admin account:

Demo

This is full Vue JWT Authentication App demo (with form validation, check signup username/email duplicates, test authorization with 3 roles: Admin, Moderator, User). In the video, we use Spring Boot for back-end REST APIs.

Flow for User Registration and User Login

For JWT Authentication, we’re gonna call 2 endpoints:

  • POST api/auth/signup for User Registration
  • POST api/auth/signin for User Login

You can take a look at following flow to have an overview of Requests and Responses Vue Client will make or receive.

Vue Client must add a JWT to HTTP Authorization Header before sending request to protected resources.

Vue App Component Diagram with Vuex & Vue Router

Now look at the diagram below.

Let’s think about it.

– The App component is a container with Router. It gets app state from Vuex store/auth. Then the navbar now can display based on the state. App component also passes state to its child components.

Login & Register components have form for submission data (with support of vee-validate). We call Vuex store dispatch() function to make login/register actions.

– Our Vuex actions call auth.service methods which use axios to make HTTP requests. We also store or get JWT from Browser Local Storage inside these methods.

Home component is public for all visitor.

Profile component get user data from its parent component and display user information.

BoardUser, BoardModerator, BoardAdmin components will be displayed by Vuex state user.roles. In these components, we use user.service to get protected resources from API.

user.service uses auth-header() helper function to add JWT to HTTP Authorization header. auth-header() returns an object containing the JWT of the currently logged in user from Local Storage.

Technology

We will use these modules:

  • vue: 2.6.10
  • vue-router: 3.0.3
  • vuex: 3.0.1
  • axios: 0.19.0
  • vee-validate: 2.2.15
  • bootstrap: 4.3.1
  • vue-fontawesome: 0.1.7
Project Structure

This is folders & files structure for our Vue application:

With the explaination in diagram above, you can understand the project structure easily.

Setup Vue App modules

Run following command to install neccessary modules:

npm install vue-router
npm install vuex
npm install [email protected]
npm install axios
npm install bootstrap jquery popper.js
npm install @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome

After the installation is done, you can check dependencies in package.json file.

"dependencies": {
  "@fortawesome/fontawesome-svg-core": "^1.2.25",
  "@fortawesome/free-solid-svg-icons": "^5.11.2",
  "@fortawesome/vue-fontawesome": "^0.1.7",
  "axios": "^0.19.0",
  "bootstrap": "^4.3.1",
  "core-js": "^2.6.5",
  "jquery": "^3.4.1",
  "popper.js": "^1.15.0",
  "vee-validate": "^2.2.15",
  "vue": "^2.6.10",
  "vue-router": "^3.0.3",
  "vuex": "^3.0.1"
},

Open src/main.js, add code below:

import Vue from 'vue';
import App from './App.vue';
import { router } from './router';
import store from './store';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import VeeValidate from 'vee-validate';
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import {
  faHome,
  faUser,
  faUserPlus,
  faSignInAlt,
  faSignOutAlt
} from '@fortawesome/free-solid-svg-icons';

library.add(faHome, faUser, faUserPlus, faSignInAlt, faSignOutAlt);

Vue.config.productionTip = false;

Vue.use(VeeValidate);
Vue.component('font-awesome-icon', FontAwesomeIcon);

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app');

You can see that we import and apply in Vue object:
store for Vuex (implemented later in src/store)
router for Vue Router (implemented later in src/router.js)
bootstrap with CSS
vee-validate
vue-fontawesome for icons (used later in nav)

Create Services

We create two services in src/services folder:


services

auth-header.js

auth.service.js (Authentication service)

user.service.js (Data service)


Authentication service

The service provides three important methods with the help of axios for HTTP requests & reponses:

  • login(): POST {username, password} & save JWT to Local Storage
  • logout(): remove JWT from Local Storage
  • register(): POST {username, email, password}
import axios from 'axios';

const API_URL = 'http://localhost:8080/api/auth/';

class AuthService {
  login(user) {
    return axios
      .post(API_URL + 'signin', {
        username: user.username,
        password: user.password
      })
      .then(this.handleResponse)
      .then(response => {
        if (response.data.accessToken) {
          localStorage.setItem('user', JSON.stringify(response.data));
        }

        return response.data;
      });
  }

  logout() {
    localStorage.removeItem('user');
  }

  register(user) {
    return axios.post(API_URL + 'signup', {
      username: user.username,
      email: user.email,
      password: user.password
    });
  }

  handleResponse(response) {
    if (response.status === 401) {
      this.logout();
      location.reload(true);

      const error = response.data && response.data.message;
      return Promise.reject(error);
    }

    return Promise.resolve(response);
  }
}

export default new AuthService();

If login request returns 401 status (Unauthorized), that means, JWT was expired or no longer valid, we will logout the user (remove JWT from Local Storage).

Data service

We also have methods for retrieving data from server. In the case we access protected resources, the HTTP request needs Authorization header.

Let’s create a helper function called authHeader() inside auth-header.js:

export default function authHeader() {
  let user = JSON.parse(localStorage.getItem('user'));

  if (user && user.accessToken) {
    return { Authorization: 'Bearer ' + user.accessToken };
  } else {
    return {};
  }
}

It checks Local Storage for user item.
If there is a logged in user with accessToken (JWT), return HTTP Authorization header. Otherwise, return an empty object.

Now we define a service for accessing data in user.service.js:

import axios from 'axios';
import authHeader from './auth-header';

const API_URL = 'http://localhost:8080/api/test/';

class UserService {
  getPublicContent() {
    return axios.get(API_URL + 'all');
  }

  getUserBoard() {
    return axios.get(API_URL + 'user', { headers: authHeader() });
  }

  getModeratorBoard() {
    return axios.get(API_URL + 'mod', { headers: authHeader() });
  }

  getAdminBoard() {
    return axios.get(API_URL + 'admin', { headers: authHeader() });
  }
}

export default new UserService();

You can see that we add a HTTP header with the help of authHeader() function when requesting authorized resource.

Define Vuex Authentication module

We put Vuex module for authentication in src/store folder.


store

auth.module.js (authentication module)

index.js (Vuex Store that contains also modules)


Now open index.js file, import auth.module to main Vuex Store here.

import Vue from 'vue';
import Vuex from 'vuex';

import { auth } from './auth.module';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    auth
  }
});

Then we start to define Vuex Authentication module that contains:

  • state: { status, user }
  • actions: { login, logout, register }
  • mutations: { loginSuccess, loginFailure, logout, registerSuccess, registerFailure }

We use AuthService which is defined above to make authentication requests.

auth.module.js

import AuthService from '../services/auth.service';

const user = JSON.parse(localStorage.getItem('user'));
const initialState = user
  ? { status: { loggedIn: true }, user }
  : { status: {}, user: null };

export const auth = {
  namespaced: true,
  state: initialState,
  actions: {
    login({ commit }, user) {
      return AuthService.login(user).then(
        user => {
          commit('loginSuccess', user);
          return Promise.resolve(user);
        },
        error => {
          commit('loginFailure');
          return Promise.reject(error.response.data);
        }
      );
    },
    logout({ commit }) {
      AuthService.logout();
      commit('logout');
    },
    register({ commit }, user) {
      return AuthService.register(user).then(
        response => {
          commit('registerSuccess');
          return Promise.resolve(response.data);
        },
        error => {
          commit('registerFailure');
          return Promise.reject(error.response.data);
        }
      );
    }
  },
  mutations: {
    loginSuccess(state, user) {
      state.status = { loggedIn: true };
      state.user = user;
    },
    loginFailure(state) {
      state.status = {};
      state.user = null;
    },
    logout(state) {
      state.status = {};
      state.user = null;
    },
    registerSuccess(state) {
      state.status = {};
    },
    registerFailure(state) {
      state.status = {};
    }
  }
};

You can find more details about Vuex at Vuex Guide.

Create Vue Authentication Components

Define User model

To make code clear and easy to read, we define the User model first.
Under src/models folder, create user.js like this.

export default class User {
  constructor(username, email, password) {
    this.username = username;
    this.email = email;
    this.password = password;
  }
}

Let’s continue with Authentication Components.
Instead of using axios or AuthService directly, these Components should work with Vuex Store:
– getting status with this.$store.state.auth
– making request by dispatching an action: this.$store.dispatch()


views

Login.vue

Register.vue

Profile.vue


Vue Login Page

In src/views folder, create Login.vue file with following code:

<template>
  <div class="col-md-12">
    <div class="card card-container">
      <img
        id="profile-img"
        src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
        class="profile-img-card"
      />
      <form name="form" @submit.prevent="handleLogin">
        <div class="form-group">
          <label for="username">Username</label>
          <input
            type="text"
            class="form-control"
            name="username"
            v-model="user.username"
            v-validate="'required'"
          />
          <div
            class="alert alert-danger"
            role="alert"
            v-if="errors.has('username')"
          >Username is required!</div>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input
            type="password"
            class="form-control"
            name="password"
            v-model="user.password"
            v-validate="'required'"
          />
          <div
            class="alert alert-danger"
            role="alert"
            v-if="errors.has('password')"
          >Password is required!</div>
        </div>
        <div class="form-group">
          <button class="btn btn-primary btn-block" :disabled="loading">
            <span class="spinner-border spinner-border-sm" v-show="loading"></span>
            <span>Login</span>
          </button>
        </div>
        <div class="form-group">
          <div class="alert alert-danger" role="alert" v-if="message">{{message}}</div>
        </div>
      </form>
    </div>
  </div>
</template>

<script>
import User from '../models/user';

export default {
  name: 'login',
  computed: {
    loggedIn() {
      return this.$store.state.auth.status.loggedIn;
    }
  },
  data() {
    return {
      user: new User('', ''),
      loading: false,
      message: ''
    };
  },
  mounted() {
    if (this.loggedIn) {
      this.$router.push('/profile');
    }
  },
  methods: {
    handleLogin() {
      this.loading = true;
      this.$validator.validateAll();

      if (this.errors.any()) {
        this.loading = false;
        return;
      }

      if (this.user.username && this.user.password) {
        this.$store.dispatch('auth/login', this.user).then(
          () => {
            this.$router.push('/profile');
          },
          error => {
            this.loading = false;
            this.message = error.message;
          }
        );
      }
    }
  }
};
</script>

<style scoped>
label {
  display: block;
  margin-top: 10px;
}

.card-container.card {
  max-width: 350px !important;
  padding: 40px 40px;
}

.card {
  background-color: #f7f7f7;
  padding: 20px 25px 30px;
  margin: 0 auto 25px;
  margin-top: 50px;
  -moz-border-radius: 2px;
  -webkit-border-radius: 2px;
  border-radius: 2px;
  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}

.profile-img-card {
  width: 96px;
  height: 96px;
  margin: 0 auto 10px;
  display: block;
  -moz-border-radius: 50%;
  -webkit-border-radius: 50%;
  border-radius: 50%;
}
</style>

This page has a Form with username & password. We use [VeeValidate 2.x](http://<a href=) to validate input before submitting the form. If there is an invalid field, we show the error message.

We check user logged in status using Vuex Store: this.$store.state.auth.status.loggedIn. If the status is true, we use Vue Router to direct user to Profile Page:

created() {
  if (this.loggedIn) {
    this.$router.push('/profile');
  }
},

In the handleLogin() function, we dispatch 'auth/login' Action to Vuex Store. If the login is successful, go to Profile Page, otherwise, show error message.

Vue Register Page

This page is similar to Login Page.

For form validation, we have some more details:

  • username: required|min:3|max:20
  • email: required|email|max:50
  • password: required|min:6|max:40

For form submission, we dispatch 'auth/register' Vuex Action.

src/views/Register.vue

<template>
  <div class="col-md-12">
    <div class="card card-container">
      <img
        id="profile-img"
        src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
        class="profile-img-card"
      />
      <form name="form" @submit.prevent="handleRegister">
        <div v-if="!successful">
          <div class="form-group">
            <label for="username">Username</label>
            <input
              type="text"
              class="form-control"
              name="username"
              v-model="user.username"
              v-validate="'required|min:3|max:20'"
            />
            <div
              class="alert-danger"
              v-if="submitted && errors.has('username')"
            >{{errors.first('username')}}</div>
          </div>
          <div class="form-group">
            <label for="email">Email</label>
            <input
              type="email"
              class="form-control"
              name="email"
              v-model="user.email"
              v-validate="'required|email|max:50'"
            />
            <div
              class="alert-danger"
              v-if="submitted && errors.has('email')"
            >{{errors.first('email')}}</div>
          </div>
          <div class="form-group">
            <label for="password">Password</label>
            <input
              type="password"
              class="form-control"
              name="password"
              v-model="user.password"
              v-validate="'required|min:6|max:40'"
            />
            <div
              class="alert-danger"
              v-if="submitted && errors.has('password')"
            >{{errors.first('password')}}</div>
          </div>
          <div class="form-group">
            <button class="btn btn-primary btn-block">Sign Up</button>
          </div>
        </div>
      </form>

      <div
        class="alert"
        :class="successful ? 'alert-success' : 'alert-danger'"
        v-if="message"
      >{{message}}</div>
    </div>
  </div>
</template>

<script>
import User from '../models/user';

export default {
  name: 'register',
  computed: {
    loggedIn() {
      return this.$store.state.auth.status.loggedIn;
    }
  },
  data() {
    return {
      user: new User('', '', ''),
      submitted: false,
      successful: false,
      message: ''
    };
  },
  mounted() {
    if (this.loggedIn) {
      this.$router.push('/profile');
    }
  },
  methods: {
    handleRegister() {
      this.message = '';
      this.submitted = true;
      this.$validator.validate().then(valid => {
        if (valid) {
          this.$store.dispatch('auth/register', this.user).then(
            data => {
              this.message = data.message;
              this.successful = true;
            },
            error => {
              this.message = error.message;
              this.successful = false;
            }
          );
        }
      });
    }
  }
};
</script>

<style scoped>
label {
  display: block;
  margin-top: 10px;
}

.card-container.card {
  max-width: 350px !important;
  padding: 40px 40px;
}

.card {
  background-color: #f7f7f7;
  padding: 20px 25px 30px;
  margin: 0 auto 25px;
  margin-top: 50px;
  -moz-border-radius: 2px;
  -webkit-border-radius: 2px;
  border-radius: 2px;
  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}

.profile-img-card {
  width: 96px;
  height: 96px;
  margin: 0 auto 10px;
  display: block;
  -moz-border-radius: 50%;
  -webkit-border-radius: 50%;
  border-radius: 50%;
}
</style>

Profile Page

This page gets current User from Vuex Store and show information. If the User is not logged in, it directs to Login Page.

src/views/Profile.vue

<template>
  <div class="container">
    <header class="jumbotron">
      <h3>
        <strong>{{currentUser.username}}</strong> Profile
      </h3>
    </header>
    <p>
      <strong>Token:</strong>
      {{currentUser.accessToken.substring(0, 20)}} ... {{currentUser.accessToken.substr(currentUser.accessToken.length - 20)}}
    </p>
    <p>
      <strong>Id:</strong>
      {{currentUser.id}}
    </p>
    <p>
      <strong>Email:</strong>
      {{currentUser.email}}
    </p>
    <strong>Authorities:</strong>
    <ul>
      <li v-for="(role,index) in currentUser.roles" :key="index">{{role}}</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'profile',
  computed: {
    currentUser() {
      return this.$store.state.auth.user;
    }
  },
  mounted() {
    if (!this.currentUser) {
      this.$router.push('/login');
    }
  }
};
</script>

Create Vue Components for accessing Resources

These components will use UserService to request data.


views

Home.vue

BoardAdmin.vue

BoardModerator.vue

BoardUser.vue


Home Page

This is a public page.

src/views/Home.vue

<template>
  <div class="container">
    <header class="jumbotron">
      <h3>{{content}}</h3>
    </header>
  </div>
</template>

<script>
import UserService from '../services/user.service';

export default {
  name: 'home',
  data() {
    return {
      content: ''
    };
  },
  mounted() {
    UserService.getPublicContent().then(
      response => {
        this.content = response.data;
      },
      error => {
        this.content = error.response.data.message;
      }
    );
  }
};
</script>

Role-based Pages

We have 3 pages for accessing protected data:

  • BoardUser page calls UserService.getUserBoard()
  • BoardModerator page calls UserService.getModeratorBoard()
  • BoardAdmin page calls UserService.getAdminBoard()

This is an example, other Page are similar to this Page.

src/views/BoardUser.vue

<template>
  <div class="container">
    <header class="jumbotron">
      <h3>{{content}}</h3>
    </header>
  </div>
</template>

<script>
import UserService from '../services/user.service';

export default {
  name: 'user',
  data() {
    return {
      content: ''
    };
  },
  mounted() {
    UserService.getUserBoard().then(
      response => {
        this.content = response.data;
      },
      error => {
        this.content = error.response.data.message;
      }
    );
  }
};
</script>

Define Routes for Vue Router

Now we define all routes for our Vue Application.

src/router.js

import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import Login from './views/Login.vue';
import Register from './views/Register.vue';

Vue.use(Router);

export const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/home',
      component: Home
    },
    {
      path: '/login',
      component: Login
    },
    {
      path: '/register',
      component: Register
    },
    {
      path: '/profile',
      name: 'profile',
      // lazy-loaded
      component: () => import('./views/Profile.vue')
    },
    {
      path: '/admin',
      name: 'admin',
      // lazy-loaded
      component: () => import('./views/BoardAdmin.vue')
    },
    {
      path: '/mod',
      name: 'moderator',
      // lazy-loaded
      component: () => import('./views/BoardModerator.vue')
    },
    {
      path: '/user',
      name: 'user',
      // lazy-loaded
      component: () => import('./views/BoardUser.vue')
    }
  ]
});

Add Navigation Bar to Vue App

This is the root container for our application that contains navigation bar. We will add router-view here.

src/App.vue

<template>
  <div id="app">
    <nav class="navbar navbar-expand navbar-dark bg-dark">
      <a href="#" class="navbar-brand">bezKoder</a>
      <div class="navbar-nav mr-auto">
        <li class="nav-item">
          <a href="/home" class="nav-link">
            <font-awesome-icon icon="home" /> Home
          </a>
        </li>
        <li class="nav-item" v-if="showAdminBoard">
          <a href="/admin" class="nav-link">Admin Board</a>
        </li>
        <li class="nav-item" v-if="showModeratorBoard">
          <a href="/mod" class="nav-link">Moderator Board</a>
        </li>
        <li class="nav-item">
          <a href="/user" class="nav-link" v-if="currentUser">User</a>
        </li>
      </div>

      <div class="navbar-nav ml-auto" v-if="!currentUser">
        <li class="nav-item">
          <a href="/register" class="nav-link">
            <font-awesome-icon icon="user-plus" /> Sign Up
          </a>
        </li>
        <li class="nav-item">
          <a href="/login" class="nav-link">
            <font-awesome-icon icon="sign-in-alt" /> Login
          </a>
        </li>
      </div>

      <div class="navbar-nav ml-auto" v-if="currentUser">
        <li class="nav-item">
          <a href="/profile" class="nav-link">
            <font-awesome-icon icon="user" />
            {{currentUser.username}}
          </a>
        </li>
        <li class="nav-item">
          <a href class="nav-link" @click="logOut">
            <font-awesome-icon icon="sign-out-alt" /> LogOut
          </a>
        </li>
      </div>
    </nav>

    <div class="container">
      <router-view />
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    currentUser() {
      return this.$store.state.auth.user;
    },
    showAdminBoard() {
      if (this.currentUser) {
        return this.currentUser.roles.includes('ROLE_ADMIN');
      }

      return false;
    },
    showModeratorBoard() {
      if (this.currentUser) {
        return this.currentUser.roles.includes('ROLE_MODERATOR');
      }

      return false;
    }
  },
  methods: {
    logOut() {
      this.$store.dispatch('auth/logout');
      this.$router.push('/login');
    }
  }
};
</script>

Our navbar looks more professional when using font-awesome-icon.
We also make the navbar dynamically change by current User’s roles which are retrieved from Vuex Store state.

Handle Unauthorized Access

If you want to check Authorized status everytime a navigating action is trigger, just add router.beforeEach() at the end of src/router.js like this:

router.beforeEach((to, from, next) => {
  const publicPages = ['/login', '/home'];
  const authRequired = !publicPages.includes(to.path);
  const loggedIn = localStorage.getItem('user');

  // try to access a restricted page + not logged in
  if (authRequired && !loggedIn) {
    return next('/login');
  }

  next();
});

Conclusion

Congratulation!

Today we’ve done so many interesting things. I hope you understand the overall layers of our Vue application, and apply it in your project at ease. Now you can build a front-end app that supports JWT Authentication with Vue.js, Vuex and Vue Router.

Happy learning, see you again!