Categories
JavaScript Answers

How to Take a Screenshot of a div with JavaScript?

Sometimes, we may want to take a screenshot of a div with JavaScript.

In this article, we’ll look at how to take a screenshot of a div with JavaScript.

Use the html2canvas Library

We can use the html2canvas library to capture the div as an image and put it into a canvas element.

To use it, we can add the library with a script tag by writing:

<script src='https://html2canvas.hertzen.com/dist/html2canvas.min.js'></script>

We can also install it with NPM by running:

npm install --save html2canvas

Or we can install it with Yarn by running:

yarn add html2canvas

Next, we can add the div we want to capture into an image by writing:

<div id="capture" style="padding: 10px; background: #f5da55">
  <h4 style="color: #000; ">Hello world!</h4>
</div>

Then to select and capture the element, we can write:

const getScreenshotOfElement = async (element) => {
  const canvas = await html2canvas(element)
  document.body.appendChild(canvas)
}

const div = document.querySelector('div')
getScreenshotOfElement(div)

We have the getScreenOfElement function that takes the element we want to capture into an image and put into a canvas.

In the function, we just call the html2canvas function with the element .

This function returns a promise that resolves to a canvas element with the element converted to an image in the canvas.

Then we call document.body.appendChild with the canvas to append it to the body.

Next, we select the div with querySelector .

And then we call the getScreenOfElement function that we just created.

Now we should see the canvas with the image of our div displayed below the actual div.

Conclusion

We can use the html2canvas library to capture an element into an image inside a canvas element.

This lets us take a screenshot of an element easily.

Categories
JavaScript Answers

How to Set the Keyboard Caret Position of an HTML Text Box with JavaScript?

Sometimes, we want to set the keyboard carte position of an HTML text box with JavaScript.

In this article, we’ll look at how to set the keyboard caret position of an HTML text box with JavaScript.

Use the HTML Element’s createTextRange, move and select Methods

We can use the createTextRange method to create a selection, then we can move the cursor of the selection to the location we want.

For instance, if we have the following HTML:

<input>

Then we can write the following JavaScript code to move the cursor.

For instance, we can write:

const setCaretPosition = (elem, caretPos) => {
  if (elem !== null) {
    if (elem.createTextRange) {
      const range = elem.createTextRange();
      range.move('character', caretPos);
      range.select();
    } else {
      if (elem.selectionStart) {
        elem.focus();
        elem.setSelectionRange(caretPos, caretPos);
      } else
        elem.focus();
    }
  }
}

const input = document.querySelector('input')
input.value = 'hello world'
setCaretPosition(input, 10)

We create the setCaretPosition function with the elem and caretPos parameters.

elem is the input element that we want to move the cursor of.

caretPos is the position of the cursor starting from 0 for the first position.

We check if elem isn’t null with the if statement.

If it’s not null , then we can check if the createTextRange method exists.

If it does, then we call it to create the range object.

Next, we call move with 'character' and caretPos to move the cursor.

Then we call select to apply the move.

Otherwise, we can also use focus method to focus on the input.

Then we call setSelectionRange to move to the position given by caretPos .

Then if we set a value of the input that’s longer than caretPos , then we can call setCaretPosition to move the cursor to where we want.

Conclusion

We can set the keyboard caret position of an input by creating a selection and setting the end position of the cursor with the selection range object.

Categories
Vue

How to Add Graphs and Charts to a Vue.js App

Graphs and charts are often a require feature for web apps. With Vue.js and third party libraries made for it, adding graphs and charts is easy. The most poplar library for adding charts and graphs is Chart.js. There is a Vue.js binding for Chart.js called vue-chartjs .

In this story, we will build a currency converter. To get started building our app, we need the Vue CLI, which contains a development server, and scripts to generate boilerplate code. Run npm install -g @vue/cli to install Vue CLI globally.

Then run vue create currency-converter to generate the boilerplate code.

Now we are ready to start writing code. Since we are building a single-page app, we need a router to route URLs to our pages. Vue Router is the most commonly used router for Vue.js apps. In addition, we need form validation and a good looking UI. Vee-validate is a well-known form-validation library for Vue.js apps. It works with many form-validation cases, like checking for required fields and checking for required types of data like numbers.

We also will be displaying graphs in our app, so we want to use a library to make this an easy task. Vue-chartjs is a Vue.js wrapper for the well-known Chart.js library. We also need to get exchange rates from the internet to display and use in our app, so we need an HTTP client. SuperAgent is an HTTP client that fulfills our needs.

After that, we can run the development server to display our app by running npm run serve. It will refresh the browser automatically as we are updating our code.

We can install all of them by running npm i chart.js vue-chartjs vue-material vue-router.

Now that we have all our libraries installed, we can start writing the logic code. First, we start by writing some constants we will use in multiple places by adding a urls.js in the src folder.

Add the following to the file:

const APIURL = 'https://api.exchangeratesapi.io%27/';
export { APIURL };

In main.js, put this code in:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'router.beforeEach((to, from, next) => {
  document.title = to.meta.title
  next()
})Vue.config.productionTip = false/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  template: '<App/>',
  components: { App }
})

This is the entry point of the app.

This block changes the title as we navigate to different pages by listening to Vue router’s navigation events:

router.beforeEach((to, from, next) => {
  document.title = to.meta.title
  next()
})

Also, we need a list of currencies for people to select:

export default {
	"USD": {
		"symbol": "$",
		"name": "US Dollar",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "USD",
		"name_plural": "US dollars"
	},
	"CAD": {
		"symbol": "CA$",
		"name": "Canadian Dollar",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "CAD",
		"name_plural": "Canadian dollars"
	},
	"EUR": {
		"symbol": "€",
		"name": "Euro",
		"symbol_native": "€",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "EUR",
		"name_plural": "euros"
	},
	"AED": {
		"symbol": "AED",
		"name": "United Arab Emirates Dirham",
		"symbol_native": "د.إ.‏",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "AED",
		"name_plural": "UAE dirhams"
	},
	"AFN": {
		"symbol": "Af",
		"name": "Afghan Afghani",
		"symbol_native": "؋",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "AFN",
		"name_plural": "Afghan Afghanis"
	},
	"ALL": {
		"symbol": "ALL",
		"name": "Albanian Lek",
		"symbol_native": "Lek",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "ALL",
		"name_plural": "Albanian lekë"
	},
	"AMD": {
		"symbol": "AMD",
		"name": "Armenian Dram",
		"symbol_native": "դր.",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "AMD",
		"name_plural": "Armenian drams"
	},
	"ARS": {
		"symbol": "AR$",
		"name": "Argentine Peso",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "ARS",
		"name_plural": "Argentine pesos"
	},
	"AUD": {
		"symbol": "AU$",
		"name": "Australian Dollar",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "AUD",
		"name_plural": "Australian dollars"
	},
	"AZN": {
		"symbol": "man.",
		"name": "Azerbaijani Manat",
		"symbol_native": "ман.",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "AZN",
		"name_plural": "Azerbaijani manats"
	},
	"BAM": {
		"symbol": "KM",
		"name": "Bosnia-Herzegovina Convertible Mark",
		"symbol_native": "KM",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "BAM",
		"name_plural": "Bosnia-Herzegovina convertible marks"
	},
	"BDT": {
		"symbol": "Tk",
		"name": "Bangladeshi Taka",
		"symbol_native": "৳",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "BDT",
		"name_plural": "Bangladeshi takas"
	},
	"BGN": {
		"symbol": "BGN",
		"name": "Bulgarian Lev",
		"symbol_native": "лв.",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "BGN",
		"name_plural": "Bulgarian leva"
	},
	"BHD": {
		"symbol": "BD",
		"name": "Bahraini Dinar",
		"symbol_native": "د.ب.‏",
		"decimal_digits": 3,
		"rounding": 0,
		"code": "BHD",
		"name_plural": "Bahraini dinars"
	},
	"BIF": {
		"symbol": "FBu",
		"name": "Burundian Franc",
		"symbol_native": "FBu",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "BIF",
		"name_plural": "Burundian francs"
	},
	"BND": {
		"symbol": "BN$",
		"name": "Brunei Dollar",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "BND",
		"name_plural": "Brunei dollars"
	},
	"BOB": {
		"symbol": "Bs",
		"name": "Bolivian Boliviano",
		"symbol_native": "Bs",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "BOB",
		"name_plural": "Bolivian bolivianos"
	},
	"BRL": {
		"symbol": "R$",
		"name": "Brazilian Real",
		"symbol_native": "R$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "BRL",
		"name_plural": "Brazilian reals"
	},
	"BWP": {
		"symbol": "BWP",
		"name": "Botswanan Pula",
		"symbol_native": "P",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "BWP",
		"name_plural": "Botswanan pulas"
	},
	"BYN": {
		"symbol": "Br",
		"name": "Belarusian Ruble",
		"symbol_native": "руб.",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "BYN",
		"name_plural": "Belarusian rubles"
	},
	"BZD": {
		"symbol": "BZ$",
		"name": "Belize Dollar",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "BZD",
		"name_plural": "Belize dollars"
	},
	"CDF": {
		"symbol": "CDF",
		"name": "Congolese Franc",
		"symbol_native": "FrCD",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "CDF",
		"name_plural": "Congolese francs"
	},
	"CHF": {
		"symbol": "CHF",
		"name": "Swiss Franc",
		"symbol_native": "CHF",
		"decimal_digits": 2,
		"rounding": 0.05,
		"code": "CHF",
		"name_plural": "Swiss francs"
	},
	"CLP": {
		"symbol": "CL$",
		"name": "Chilean Peso",
		"symbol_native": "$",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "CLP",
		"name_plural": "Chilean pesos"
	},
	"CNY": {
		"symbol": "CN¥",
		"name": "Chinese Yuan",
		"symbol_native": "CN¥",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "CNY",
		"name_plural": "Chinese yuan"
	},
	"COP": {
		"symbol": "CO$",
		"name": "Colombian Peso",
		"symbol_native": "$",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "COP",
		"name_plural": "Colombian pesos"
	},
	"CRC": {
		"symbol": "₡",
		"name": "Costa Rican Colón",
		"symbol_native": "₡",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "CRC",
		"name_plural": "Costa Rican colóns"
	},
	"CVE": {
		"symbol": "CV$",
		"name": "Cape Verdean Escudo",
		"symbol_native": "CV$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "CVE",
		"name_plural": "Cape Verdean escudos"
	},
	"CZK": {
		"symbol": "Kč",
		"name": "Czech Republic Koruna",
		"symbol_native": "Kč",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "CZK",
		"name_plural": "Czech Republic korunas"
	},
	"DJF": {
		"symbol": "Fdj",
		"name": "Djiboutian Franc",
		"symbol_native": "Fdj",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "DJF",
		"name_plural": "Djiboutian francs"
	},
	"DKK": {
		"symbol": "Dkr",
		"name": "Danish Krone",
		"symbol_native": "kr",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "DKK",
		"name_plural": "Danish kroner"
	},
	"DOP": {
		"symbol": "RD$",
		"name": "Dominican Peso",
		"symbol_native": "RD$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "DOP",
		"name_plural": "Dominican pesos"
	},
	"DZD": {
		"symbol": "DA",
		"name": "Algerian Dinar",
		"symbol_native": "د.ج.‏",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "DZD",
		"name_plural": "Algerian dinars"
	},
	"EEK": {
		"symbol": "Ekr",
		"name": "Estonian Kroon",
		"symbol_native": "kr",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "EEK",
		"name_plural": "Estonian kroons"
	},
	"EGP": {
		"symbol": "EGP",
		"name": "Egyptian Pound",
		"symbol_native": "ج.م.‏",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "EGP",
		"name_plural": "Egyptian pounds"
	},
	"ERN": {
		"symbol": "Nfk",
		"name": "Eritrean Nakfa",
		"symbol_native": "Nfk",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "ERN",
		"name_plural": "Eritrean nakfas"
	},
	"ETB": {
		"symbol": "Br",
		"name": "Ethiopian Birr",
		"symbol_native": "Br",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "ETB",
		"name_plural": "Ethiopian birrs"
	},
	"GBP": {
		"symbol": "£",
		"name": "British Pound Sterling",
		"symbol_native": "£",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "GBP",
		"name_plural": "British pounds sterling"
	},
	"GEL": {
		"symbol": "GEL",
		"name": "Georgian Lari",
		"symbol_native": "GEL",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "GEL",
		"name_plural": "Georgian laris"
	},
	"GHS": {
		"symbol": "GH₵",
		"name": "Ghanaian Cedi",
		"symbol_native": "GH₵",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "GHS",
		"name_plural": "Ghanaian cedis"
	},
	"GNF": {
		"symbol": "FG",
		"name": "Guinean Franc",
		"symbol_native": "FG",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "GNF",
		"name_plural": "Guinean francs"
	},
	"GTQ": {
		"symbol": "GTQ",
		"name": "Guatemalan Quetzal",
		"symbol_native": "Q",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "GTQ",
		"name_plural": "Guatemalan quetzals"
	},
	"HKD": {
		"symbol": "HK$",
		"name": "Hong Kong Dollar",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "HKD",
		"name_plural": "Hong Kong dollars"
	},
	"HNL": {
		"symbol": "HNL",
		"name": "Honduran Lempira",
		"symbol_native": "L",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "HNL",
		"name_plural": "Honduran lempiras"
	},
	"HRK": {
		"symbol": "kn",
		"name": "Croatian Kuna",
		"symbol_native": "kn",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "HRK",
		"name_plural": "Croatian kunas"
	},
	"HUF": {
		"symbol": "Ft",
		"name": "Hungarian Forint",
		"symbol_native": "Ft",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "HUF",
		"name_plural": "Hungarian forints"
	},
	"IDR": {
		"symbol": "Rp",
		"name": "Indonesian Rupiah",
		"symbol_native": "Rp",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "IDR",
		"name_plural": "Indonesian rupiahs"
	},
	"ILS": {
		"symbol": "₪",
		"name": "Israeli New Sheqel",
		"symbol_native": "₪",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "ILS",
		"name_plural": "Israeli new sheqels"
	},
	"INR": {
		"symbol": "Rs",
		"name": "Indian Rupee",
		"symbol_native": "টকা",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "INR",
		"name_plural": "Indian rupees"
	},
	"IQD": {
		"symbol": "IQD",
		"name": "Iraqi Dinar",
		"symbol_native": "د.ع.‏",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "IQD",
		"name_plural": "Iraqi dinars"
	},
	"IRR": {
		"symbol": "IRR",
		"name": "Iranian Rial",
		"symbol_native": "﷼",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "IRR",
		"name_plural": "Iranian rials"
	},
	"ISK": {
		"symbol": "Ikr",
		"name": "Icelandic Króna",
		"symbol_native": "kr",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "ISK",
		"name_plural": "Icelandic krónur"
	},
	"JMD": {
		"symbol": "J$",
		"name": "Jamaican Dollar",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "JMD",
		"name_plural": "Jamaican dollars"
	},
	"JOD": {
		"symbol": "JD",
		"name": "Jordanian Dinar",
		"symbol_native": "د.أ.‏",
		"decimal_digits": 3,
		"rounding": 0,
		"code": "JOD",
		"name_plural": "Jordanian dinars"
	},
	"JPY": {
		"symbol": "¥",
		"name": "Japanese Yen",
		"symbol_native": "¥",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "JPY",
		"name_plural": "Japanese yen"
	},
	"KES": {
		"symbol": "Ksh",
		"name": "Kenyan Shilling",
		"symbol_native": "Ksh",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "KES",
		"name_plural": "Kenyan shillings"
	},
	"KHR": {
		"symbol": "KHR",
		"name": "Cambodian Riel",
		"symbol_native": "៛",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "KHR",
		"name_plural": "Cambodian riels"
	},
	"KMF": {
		"symbol": "CF",
		"name": "Comorian Franc",
		"symbol_native": "FC",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "KMF",
		"name_plural": "Comorian francs"
	},
	"KRW": {
		"symbol": "₩",
		"name": "South Korean Won",
		"symbol_native": "₩",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "KRW",
		"name_plural": "South Korean won"
	},
	"KWD": {
		"symbol": "KD",
		"name": "Kuwaiti Dinar",
		"symbol_native": "د.ك.‏",
		"decimal_digits": 3,
		"rounding": 0,
		"code": "KWD",
		"name_plural": "Kuwaiti dinars"
	},
	"KZT": {
		"symbol": "KZT",
		"name": "Kazakhstani Tenge",
		"symbol_native": "тңг.",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "KZT",
		"name_plural": "Kazakhstani tenges"
	},
	"LBP": {
		"symbol": "LB£",
		"name": "Lebanese Pound",
		"symbol_native": "ل.ل.‏",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "LBP",
		"name_plural": "Lebanese pounds"
	},
	"LKR": {
		"symbol": "SLRs",
		"name": "Sri Lankan Rupee",
		"symbol_native": "SL Re",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "LKR",
		"name_plural": "Sri Lankan rupees"
	},
	"LTL": {
		"symbol": "Lt",
		"name": "Lithuanian Litas",
		"symbol_native": "Lt",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "LTL",
		"name_plural": "Lithuanian litai"
	},
	"LVL": {
		"symbol": "Ls",
		"name": "Latvian Lats",
		"symbol_native": "Ls",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "LVL",
		"name_plural": "Latvian lati"
	},
	"LYD": {
		"symbol": "LD",
		"name": "Libyan Dinar",
		"symbol_native": "د.ل.‏",
		"decimal_digits": 3,
		"rounding": 0,
		"code": "LYD",
		"name_plural": "Libyan dinars"
	},
	"MAD": {
		"symbol": "MAD",
		"name": "Moroccan Dirham",
		"symbol_native": "د.م.‏",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "MAD",
		"name_plural": "Moroccan dirhams"
	},
	"MDL": {
		"symbol": "MDL",
		"name": "Moldovan Leu",
		"symbol_native": "MDL",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "MDL",
		"name_plural": "Moldovan lei"
	},
	"MGA": {
		"symbol": "MGA",
		"name": "Malagasy Ariary",
		"symbol_native": "MGA",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "MGA",
		"name_plural": "Malagasy Ariaries"
	},
	"MKD": {
		"symbol": "MKD",
		"name": "Macedonian Denar",
		"symbol_native": "MKD",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "MKD",
		"name_plural": "Macedonian denari"
	},
	"MMK": {
		"symbol": "MMK",
		"name": "Myanma Kyat",
		"symbol_native": "K",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "MMK",
		"name_plural": "Myanma kyats"
	},
	"MOP": {
		"symbol": "MOP$",
		"name": "Macanese Pataca",
		"symbol_native": "MOP$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "MOP",
		"name_plural": "Macanese patacas"
	},
	"MUR": {
		"symbol": "MURs",
		"name": "Mauritian Rupee",
		"symbol_native": "MURs",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "MUR",
		"name_plural": "Mauritian rupees"
	},
	"MXN": {
		"symbol": "MX$",
		"name": "Mexican Peso",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "MXN",
		"name_plural": "Mexican pesos"
	},
	"MYR": {
		"symbol": "RM",
		"name": "Malaysian Ringgit",
		"symbol_native": "RM",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "MYR",
		"name_plural": "Malaysian ringgits"
	},
	"MZN": {
		"symbol": "MTn",
		"name": "Mozambican Metical",
		"symbol_native": "MTn",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "MZN",
		"name_plural": "Mozambican meticals"
	},
	"NAD": {
		"symbol": "N$",
		"name": "Namibian Dollar",
		"symbol_native": "N$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "NAD",
		"name_plural": "Namibian dollars"
	},
	"NGN": {
		"symbol": "₦",
		"name": "Nigerian Naira",
		"symbol_native": "₦",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "NGN",
		"name_plural": "Nigerian nairas"
	},
	"NIO": {
		"symbol": "C$",
		"name": "Nicaraguan Córdoba",
		"symbol_native": "C$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "NIO",
		"name_plural": "Nicaraguan córdobas"
	},
	"NOK": {
		"symbol": "Nkr",
		"name": "Norwegian Krone",
		"symbol_native": "kr",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "NOK",
		"name_plural": "Norwegian kroner"
	},
	"NPR": {
		"symbol": "NPRs",
		"name": "Nepalese Rupee",
		"symbol_native": "नेरू",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "NPR",
		"name_plural": "Nepalese rupees"
	},
	"NZD": {
		"symbol": "NZ$",
		"name": "New Zealand Dollar",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "NZD",
		"name_plural": "New Zealand dollars"
	},
	"OMR": {
		"symbol": "OMR",
		"name": "Omani Rial",
		"symbol_native": "ر.ع.‏",
		"decimal_digits": 3,
		"rounding": 0,
		"code": "OMR",
		"name_plural": "Omani rials"
	},
	"PAB": {
		"symbol": "B/.",
		"name": "Panamanian Balboa",
		"symbol_native": "B/.",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "PAB",
		"name_plural": "Panamanian balboas"
	},
	"PEN": {
		"symbol": "S/.",
		"name": "Peruvian Nuevo Sol",
		"symbol_native": "S/.",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "PEN",
		"name_plural": "Peruvian nuevos soles"
	},
	"PHP": {
		"symbol": "₱",
		"name": "Philippine Peso",
		"symbol_native": "₱",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "PHP",
		"name_plural": "Philippine pesos"
	},
	"PKR": {
		"symbol": "PKRs",
		"name": "Pakistani Rupee",
		"symbol_native": "₨",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "PKR",
		"name_plural": "Pakistani rupees"
	},
	"PLN": {
		"symbol": "zł",
		"name": "Polish Zloty",
		"symbol_native": "zł",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "PLN",
		"name_plural": "Polish zlotys"
	},
	"PYG": {
		"symbol": "₲",
		"name": "Paraguayan Guarani",
		"symbol_native": "₲",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "PYG",
		"name_plural": "Paraguayan guaranis"
	},
	"QAR": {
		"symbol": "QR",
		"name": "Qatari Rial",
		"symbol_native": "ر.ق.‏",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "QAR",
		"name_plural": "Qatari rials"
	},
	"RON": {
		"symbol": "RON",
		"name": "Romanian Leu",
		"symbol_native": "RON",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "RON",
		"name_plural": "Romanian lei"
	},
	"RSD": {
		"symbol": "din.",
		"name": "Serbian Dinar",
		"symbol_native": "дин.",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "RSD",
		"name_plural": "Serbian dinars"
	},
	"RUB": {
		"symbol": "RUB",
		"name": "Russian Ruble",
		"symbol_native": "₽.",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "RUB",
		"name_plural": "Russian rubles"
	},
	"RWF": {
		"symbol": "RWF",
		"name": "Rwandan Franc",
		"symbol_native": "FR",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "RWF",
		"name_plural": "Rwandan francs"
	},
	"SAR": {
		"symbol": "SR",
		"name": "Saudi Riyal",
		"symbol_native": "ر.س.‏",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "SAR",
		"name_plural": "Saudi riyals"
	},
	"SDG": {
		"symbol": "SDG",
		"name": "Sudanese Pound",
		"symbol_native": "SDG",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "SDG",
		"name_plural": "Sudanese pounds"
	},
	"SEK": {
		"symbol": "Skr",
		"name": "Swedish Krona",
		"symbol_native": "kr",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "SEK",
		"name_plural": "Swedish kronor"
	},
	"SGD": {
		"symbol": "S$",
		"name": "Singapore Dollar",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "SGD",
		"name_plural": "Singapore dollars"
	},
	"SOS": {
		"symbol": "Ssh",
		"name": "Somali Shilling",
		"symbol_native": "Ssh",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "SOS",
		"name_plural": "Somali shillings"
	},
	"SYP": {
		"symbol": "SY£",
		"name": "Syrian Pound",
		"symbol_native": "ل.س.‏",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "SYP",
		"name_plural": "Syrian pounds"
	},
	"THB": {
		"symbol": "฿",
		"name": "Thai Baht",
		"symbol_native": "฿",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "THB",
		"name_plural": "Thai baht"
	},
	"TND": {
		"symbol": "DT",
		"name": "Tunisian Dinar",
		"symbol_native": "د.ت.‏",
		"decimal_digits": 3,
		"rounding": 0,
		"code": "TND",
		"name_plural": "Tunisian dinars"
	},
	"TOP": {
		"symbol": "T$",
		"name": "Tongan Paʻanga",
		"symbol_native": "T$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "TOP",
		"name_plural": "Tongan paʻanga"
	},
	"TRY": {
		"symbol": "TL",
		"name": "Turkish Lira",
		"symbol_native": "TL",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "TRY",
		"name_plural": "Turkish Lira"
	},
	"TTD": {
		"symbol": "TT$",
		"name": "Trinidad and Tobago Dollar",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "TTD",
		"name_plural": "Trinidad and Tobago dollars"
	},
	"TWD": {
		"symbol": "NT$",
		"name": "New Taiwan Dollar",
		"symbol_native": "NT$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "TWD",
		"name_plural": "New Taiwan dollars"
	},
	"TZS": {
		"symbol": "TSh",
		"name": "Tanzanian Shilling",
		"symbol_native": "TSh",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "TZS",
		"name_plural": "Tanzanian shillings"
	},
	"UAH": {
		"symbol": "₴",
		"name": "Ukrainian Hryvnia",
		"symbol_native": "₴",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "UAH",
		"name_plural": "Ukrainian hryvnias"
	},
	"UGX": {
		"symbol": "USh",
		"name": "Ugandan Shilling",
		"symbol_native": "USh",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "UGX",
		"name_plural": "Ugandan shillings"
	},
	"UYU": {
		"symbol": "$U",
		"name": "Uruguayan Peso",
		"symbol_native": "$",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "UYU",
		"name_plural": "Uruguayan pesos"
	},
	"UZS": {
		"symbol": "UZS",
		"name": "Uzbekistan Som",
		"symbol_native": "UZS",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "UZS",
		"name_plural": "Uzbekistan som"
	},
	"VEF": {
		"symbol": "Bs.F.",
		"name": "Venezuelan Bolívar",
		"symbol_native": "Bs.F.",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "VEF",
		"name_plural": "Venezuelan bolívars"
	},
	"VND": {
		"symbol": "₫",
		"name": "Vietnamese Dong",
		"symbol_native": "₫",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "VND",
		"name_plural": "Vietnamese dong"
	},
	"XAF": {
		"symbol": "FCFA",
		"name": "CFA Franc BEAC",
		"symbol_native": "FCFA",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "XAF",
		"name_plural": "CFA francs BEAC"
	},
	"XOF": {
		"symbol": "CFA",
		"name": "CFA Franc BCEAO",
		"symbol_native": "CFA",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "XOF",
		"name_plural": "CFA francs BCEAO"
	},
	"YER": {
		"symbol": "YR",
		"name": "Yemeni Rial",
		"symbol_native": "ر.ي.‏",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "YER",
		"name_plural": "Yemeni rials"
	},
	"ZAR": {
		"symbol": "R",
		"name": "South African Rand",
		"symbol_native": "R",
		"decimal_digits": 2,
		"rounding": 0,
		"code": "ZAR",
		"name_plural": "South African rand"
	},
	"ZMK": {
		"symbol": "ZK",
		"name": "Zambian Kwacha",
		"symbol_native": "ZK",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "ZMK",
		"name_plural": "Zambian kwachas"
	},
	"ZWL": {
		"symbol": "ZWL$",
		"name": "Zimbabwean Dollar",
		"symbol_native": "ZWL$",
		"decimal_digits": 0,
		"rounding": 0,
		"code": "ZWL",
		"name_plural": "Zimbabwean Dollar"
	}
}

We will use this in the currency conversion page.

In index.html, we add:

<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,400italic|Material+Icons">

This displays Roboto so our app looks good with the Material widgets.

Then in src/router/index.js, we add:

import Vue from 'vue'
import Router from 'vue-router'
import VeeValidate from 'vee-validate';
import Quotes from '@/components/Quotes'
import Convert from '@/components/Convert'
import Drawer from '@/components/Drawer'
import Historical from '@/components/Historical'
import HistoricalRatesInPeriod from '@/components/HistoricalRatesInPeriod'
import HistoricalRatesInPeriodGraph from '@/components/HistoricalRatesInPeriodGraph'
import LineGraph from '@/components/LineGraph'
import currencies from '../currencies';
import VueMaterial from 'vue-material'
import 'vue-material/dist/vue-material.min.css'
import 'vue-material/dist/theme/default.css'Vue.component('drawer', Drawer);
Vue.component('line-graph', LineGraph);
Vue.use(VueMaterial)
Vue.use(Router)
Vue.use(VeeValidate);Vue.filter('currencyName', function (value) {
  if (currencies[value]) {
    return currencies[value].name;
  }
  else {
    return value;
  }})export default new Router({
  routes: [
    {
      path: '/',
      name: 'quotes',
      component: Quotes,
      meta: { title: 'Quotes' }
    },
    {
      path: '/convert',
      name: 'convert',
      component: Convert,
      meta: { title: 'Convert' }
    },
    {
      path: '/historical',
      name: 'historical',
      component: Historical,
      meta: { title: 'Historical Rates' }
    },
    {
      path: '/historicalratesinperiod',
      name: 'historicalratesinperiod',
      component: HistoricalRatesInPeriod,
      meta: { title: 'Historical Rates in a Period' }
    },
    {
      path: '/historicalratesinperiodgraph',
      name: 'historicalratesinperiodgraph',
      component: HistoricalRatesInPeriodGraph,
      meta: { title: 'Historical Rates in a Period' }
    }
  ]
})

This block is where we register our external components with Vue so we can use it in our templates:

Vue.component('drawer', Drawer);
Vue.component('line-graph', LineGraph);
Vue.use(VueMaterial)
Vue.use(Router)
Vue.use(VeeValidate);

Vue also has an element called filter that allows us to map one value to another in our template. We also register the LineGraph component here to allow us to use the component in our app.

This block gets the currency code and gets the name:

Vue.filter('currencyName', function (value) {
  if (currencies[value]) {
    return currencies[value].name;
  }
  else {
    return value;
  }})

These are the routes for each page of our app. The titles of each page are in the title property of the meta object in each route. The path is the relative URL of each page. We will have to add the components we imported in the file above.

Next, we create the page for the currency-conversion form. We add a file called Convert.vue in thesrc/components folder. The vue extension indicates it is a single-file component, which means that the logic, the style, and the template of the component are all included in one file.

The Convert.vue code looks like this:

<template>
  <div class="page-container md-layout-column">
    <drawer></drawer>
    <div class="container">
      <h1 class="center">Convert Currency</h1>
      <form @submit.prevent="convert">
        <div class="md-layout-item">
          <md-field :class="{'md-invalid': errors.first('price') }">
            <label>Amount of Currency to Convert From</label>
            <md-input v-model="form.price" v-validate="'required'" name="price"></md-input>
            <span class="md-error">{{ errors.first('price') }}</span>
          </md-field>
          <md-field :class="{'md-invalid': errors.first('from') }">
            <label for="from">Currency to Convert From</label>
            <md-select v-model="form.from" name="from" v-validate="'required'">
              <md-option :value="s.code" v-for="s in fromSymbols" :key="s.code">{{s.name}}</md-option>
            </md-select>
            <span class="md-error">{{ errors.first('from') }}</span>
          </md-field>
          <md-field :class="{'md-invalid': errors.first('to') }">
            <label for="to">Currency to Convert To</label>
            <md-select v-model="form.to" name="to" v-validate="'required'">
              <md-option :value="s.code" v-for="s in toSymbols" :key="s.code">{{s.name}}</md-option>
            </md-select>
            <span class="md-error">{{ errors.first('to') }}</span>
          </md-field>
        </div>
        <md-button class="md-dense md-raised md-primary" type="submit">Convert</md-button>
      </form>
      <div v-if="hasResult" class="center">
        <h2
          v-if="isNumber(result)"
        >{{form.price | currencyName}} {{form.from}} is {{result}} {{form.to | currencyName}}</h2>
        <h2 v-if="!isNumber(result)">Exchange rate not found.</h2>
      </div>
    </div>
  </div>
</template><script>
import currencies from "../currencies";
import { APIURL } from "../urls";
import * as moment from "moment";
const request = require("superagent");export default {
  name: "convert",
  data() {
    return {
      form: {
        price: 0,
        from: "",
        to: ""
      },
      result: null,
      currencies,
      currentTime: "",
      err: null,
      hasResult: false,
      symbols: [],
      fromSymbols: [],
      toSymbols: []
    };
  },
  watch: {
    form: {
      handler(val) {
        if (!this.form) {
          this.toSymbols = this.symbols;
          this.fromSymbols = this.symbols;
          return;
        }this.hasResult = false;
        if (this.form.from) {
          this.toSymbols = this.symbols.filter(s => s.code != this.form.from);
        }if (this.form.to) {
          this.fromSymbols = this.symbols.filter(s => s.code != this.form.to);
        }
      },
      deep: true
    }
  },
  methods: {
    convert(evt) {
      this.hasResult = false;
      evt.preventDefault();
      if (this.errors.items.length > 0) {
        return;
      }request.get(`${APIURL}/latest?base=${this.form.from}`).end((err, res) => {
        let body = res.body;
        this.hasResult = true;
        this.result = (+this.form.price * +body.rates[this.form.to]).toFixed(2);
        this.currentTime = moment(this.result.timestamp * 1000).format(
          "YYYY-MM-DD HH:mm:ss a"
        );
      });
    },
    isNumber(number) {
      return !isNaN(number);
    }
  },
  beforeMount() {
    this.symbols = Object.keys(this.currencies).map(k => {
      return {
        symbol: k,
        ...this.currencies[k]
      };
    });
    this.toSymbols = JSON.parse(JSON.stringify(this.symbols));
    this.fromSymbols = JSON.parse(JSON.stringify(this.symbols));
  }
};
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.result {
  margin-top: 30px;
}
</style>

Note that it uses the currencyName filter that we defined earlier.

The part between the template tags is the form. There is an input and two select dropdowns for the currency we are converting from and to respectively.

In each form field, we check if it has been filled with the v-validate="'required'" attribute. The options are imported from the JSON file with all the currencies. We remove the currency that has already been selected in one of the dropdowns in the watch block in the object that we are exporting in the script tags:

watch: {
    form: {
      handler(val) {
        if (!this.form) {
          this.toSymbols = this.symbols;
          this.fromSymbols = this.symbols;
          return;
        }this.hasResult = false;
        if (this.form.from) {
          this.toSymbols = this.symbols.filter(s => s.code != this.form.from);
        }if (this.form.to) {
          this.fromSymbols = this.symbols.filter(s => s.code != this.form.to);
        }
      },
      deep: true
    }
  },

This makes it so we don’t have the same selection in both dropdowns.

In the code above, we’re watching for changes to all properties of the form object. If the new currency symbol is selected in either the to or from dropdown, then that symbol will be excluded from selection in the to or from field, respectively.

Once the user clicks the submit button, the convert function runs, which gets the exchange rate and computes the converted amount. The this in the object is where the template variables are bound to the variables in the logic. We use moment to format the time, so we have to install that by running npm i moment.

We display the result by assigning to the fields of this. The templates will automatically update.

To create a side menu for navigation, we add a file called Drawer.vue, which we import and register it in index.js so we can include it our template.

The code looks like this:

<template>
  <div>
    <md-drawer :md-active.sync="showNavigation" md-swipeable>
      <md-toolbar class="md-transparent" md-elevation="0">
        <span class="md-title">Currency Info</span>
      </md-toolbar><md-list>
        <md-list-item>
          <router-link to="/">
            <span class="md-list-item-text">Exchange Rates</span>
          </router-link>
        </md-list-item><md-list-item>
          <router-link to="/convert">
            <span class="md-list-item-text">Convert Currency</span>
          </router-link>
        </md-list-item><md-list-item>
          <router-link to="/historical">
            <span class="md-list-item-text">Historical Rates</span>
          </router-link>
        </md-list-item><md-list-item>
          <router-link to="/historicalratesinperiod">
            <span class="md-list-item-text">Historical Rates in a Period</span>
          </router-link>
        </md-list-item><md-list-item>
          <router-link to="/historicalratesinperiodgraph">
            <span class="md-list-item-text">Historical Rates in a Period Graph</span>
          </router-link>
        </md-list-item>
      </md-list>
    </md-drawer>
    <md-toolbar class="md-primary">
      <md-button class="md-icon-button" @click="showNavigation = true">
        <md-icon>menu</md-icon>
      </md-button>
      <h3 class="md-title">Currency Info</h3>
    </md-toolbar>
  </div>
</template><script>
export default {
  name: "nav-bar",
  data() {
    return {
      msg: "Welcome to Your Vue.js App",
      showNavigation: false
    };
  }
};
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

Now we create the rest of the component files in a similar fashion. They are all in src/components.

We create a file called Historical.vue and add the following:

<template>
  <div class="page-container md-layout-column">
    <drawer></drawer>
    <div class="container">
      <h1 class="center">Historical Exchange Rates</h1>
      <form @submit.prevent="getHistoricalRate">
        <div class="md-layout-item">
          <label>Date</label>
          <md-datepicker
            v-model="form.date"
            v-validate="'required'"
            name="date"
            :md-disabled-dates="futureDates"
          ></md-datepicker>
          <md-field :class="{'md-invalid': errors.first('baseCurrency') }">
            <label for="baseCurrency">Currency Symbol</label>
            <md-select v-model="form.baseCurrency" name="baseCurrency" v-validate="'required'">
              <md-option :value="s.code" v-for="s in symbols" :key="s.code">{{s.name}}</md-option>
            </md-select>
            <span class="md-error">{{ errors.first('baseCurrency') }}</span>
          </md-field>
        </div>
        <md-button class="md-dense md-raised md-primary" type="submit">Find Rate</md-button>
      </form>
      <div>
        <md-content class="md-layout-item" v-for="(r, index) in rates" :key="index">
          <h2>{{form.baseCurrency | currencyName}} - {{r.symbol| currencyName}}</h2>
          <p>{{r.rate}}</p>
        </md-content>
      </div>
    </div>
  </div>
</template><script>
import currencies from "../currencies";
import { APIURL } from "../urls";
import * as moment from "moment";
const request = require("superagent");export default {
  data() {
    return {
      form: {
        date: new Date(),
        baseCurrency: ""
      },
      rates: [],
      symbols: [],
      currencies
    };
  },
  methods: {
    getHistoricalRate(evt) {
      this.hasResult = false;
      evt.preventDefault();
      if (this.errors.items.length > 0) {
        return;
      }request
        .get(
          `${APIURL}/${moment(this.form.date).format("YYYY-MM-DD")}?base=${
            this.form.baseCurrency
          }`
        )
        .end((err, res) => {
          let body = res.body;
          this.hasResult = true;
          this.rates = Object.keys(body.rates).map(k => {
            return {
              symbol: k,
              rate: body.rates[k]
            };
          });
        });
    },
    futureDates(evt) {
      return +evt > +new Date();
    }
  },
  beforeMount() {
    this.symbols = Object.keys(this.currencies).map(k => {
      return {
        symbol: k,
        ...this.currencies[k]
      };
    });
  }
};
</script><style>
</style>

In the component, thegetHistoricalRate function runs code to make a GET request to the API to exchange with our Superagent HTTP client library. After the request is done, the then function that is chained to the getfunction takes the exchange rate result and does calculations.

Then we create HistoricalRatesInPeriod.vue in the same folder and add the following:

<template>
  <div class="page-container md-layout-column">
    <drawer></drawer>
    <div class="container">
      <h1 class="center">Historical Exchange Rates in a Period</h1>
      <form @submit.prevent="getHistoricalRate">
        <div class="md-layout-item">
          <label>Start Date</label>
          <md-datepicker
            v-model="form.startDate"
            v-validate="'required'"
            name="date"
            :md-disabled-dates="invalidStartDates"
          ></md-datepicker>
          <label>End Date</label>
          <md-datepicker
            v-model="form.endDate"
            v-validate="'required'"
            name="date"
            :md-disabled-dates="invalidEndDates"
          ></md-datepicker>
          <md-field :class="{'md-invalid': errors.first('baseCurrency') }">
            <label for="baseCurrency">Currency Symbol</label>
            <md-select v-model="form.baseCurrency" name="baseCurrency" v-validate="'required'">
              <md-option :value="s.code" v-for="s in symbols" :key="s.code">{{s.name}}</md-option>
            </md-select>
            <span class="md-error">{{ errors.first('baseCurrency') }}</span>
          </md-field>
        </div>
        <md-button class="md-dense md-raised md-primary" type="submit">Find Rate</md-button>
      </form>
      <div>
        <md-content class="md-layout-item" v-for="(r, index) in rates" :key="index">
          <h2>{{r.date}}</h2>
          <md-content class="md-layout-item" v-for="(rr, index) in r.rates" :key="index">
            <h3>{{form.baseCurrency | currencyName}} - {{rr.symbol| currencyName}}</h3>
            <p>{{rr.rate}}</p>
          </md-content>
        </md-content>
      </div>
    </div>
  </div>
</template><script>
import currencies from "../currencies";
import { APIURL } from "../urls";
import * as moment from "moment";
const request = require("superagent");export default {
  data() {
    return {
      form: {
        startDate: new Date(),
        endDate: new Date(),
        baseCurrency: ""
      },
      rates: [],
      symbols: [],
      currencies
    };
  },
  methods: {
    getHistoricalRate(evt) {
      this.hasResult = false;
      evt.preventDefault();
      if (this.errors.items.length > 0) {
        return;
      }request
        .get(
          `${APIURL}/history?start_at=${moment(this.form.startDate).format(
            "YYYY-MM-DD"
          )}&end_at=${moment(this.form.endDate).format("YYYY-MM-DD")}&base=${
            this.form.baseCurrency
          }`
        )
        .end((err, res) => {
          let body = res.body;
          this.hasResult = true;
          this.rates = Object.keys(body.rates)
            .map(date => {
              return {
                date: moment(date).format("YYYY-MM-DD"),
                rates: Object.keys(body.rates[date]).map(k => {
                  return {
                    symbol: k,
                    rate: body.rates[date][k]
                  };
                })
              };
            })
            .sort((a, b) => +new Date(a.date) - +new Date(b.date));
        });
    },
    invalidStartDates(evt) {
      return +evt > +new Date();
    },
    invalidEndDates(evt) {
      return +evt < +this.form.startDate || +evt > +new Date();
    }
  },
  beforeMount() {
    this.symbols = Object.keys(this.currencies).map(k => {
      return {
        symbol: k,
        ...this.currencies[k]
      };
    });
  }
};
</script><style>
</style>

The date picker and other components are all registered in index.js so we can include them in our templates.

Next, we add a page to “Historical Exchange Rates in a Period” in a graph by adding a file called HistoreicalRatesInPeriodGraph.vue and add the following code:

<template>
  <div class="page-container md-layout-column">
    <drawer></drawer>
    <div class="container">
      <h1 class="center">Historical Exchange Rates in a Period Graph</h1>
      <form @submit.prevent="getHistoricalRate">
        <div class="md-layout-item">
          <label>Start Date</label>
          <md-datepicker
            v-model="form.startDate"
            v-validate="'required'"
            name="date"
            :md-disabled-dates="invalidStartDates"
          ></md-datepicker>
          <label>End Date</label>
          <md-datepicker
            v-model="form.endDate"
            v-validate="'required'"
            name="date"
            :md-disabled-dates="invalidEndDates"
          ></md-datepicker>
          <md-field :class="{'md-invalid': errors.first('from') }">
            <label for="from">Currency to Convert From</label>
            <md-select v-model="form.from" name="from" v-validate="'required'">
              <md-option :value="s.code" v-for="s in fromSymbols" :key="s.code">{{s.name}}</md-option>
            </md-select>
            <span class="md-error">{{ errors.first('from') }}</span>
          </md-field>
          <md-field :class="{'md-invalid': errors.first('to') }">
            <label for="to">Currency to Convert To</label>
            <md-select v-model="form.to" name="to" v-validate="'required'">
              <md-option :value="s.code" v-for="s in toSymbols" :key="s.code">{{s.name}}</md-option>
            </md-select>
            <span class="md-error">{{ errors.first('to') }}</span>
          </md-field>
        </div>
        <md-button class="md-dense md-raised md-primary" type="submit">Find Historical Rates</md-button>
      </form>
      <div>
        <line-graph v-if="hasResult && !resultNotFound" :chartdata="graphData" :options="options"></line-graph>
        <h2 v-if="hasResult && resultNotFound">Exchange rates not found.</h2>
      </div>
    </div>
  </div>
</template><script>
import currencies from "../currencies";
import { APIURL } from "../urls";
import * as moment from "moment";
const request = require("superagent");export default {
  data() {
    return {
      form: {
        startDate: new Date(),
        endDate: new Date(),
        from: "",
        to: ""
      },
      rates: [],
      symbols: [],
      fromSymbols: [],
      toSymbols: [],
      currencies,
      graphData: {},
      options: {
        responsive: true,
        maintainAspectRatio: false
      },
      hasResult: false,
      resultNotFound: false
    };
  },
  watch: {
    form: {
      handler(val) {
        if (!this.form) {
          this.toSymbols = this.symbols;
          this.fromSymbols = this.symbols;
          return;
        }

        if (this.form.from) {
          this.toSymbols = this.symbols.filter(s => s.code != this.form.from);
        }if (this.form.to) {
          this.fromSymbols = this.symbols.filter(s => s.code != this.form.to);
        }
      },
      deep: true
    }
  },
  methods: {
    getHistoricalRate(evt) {
      this.hasResult = false;
      evt.preventDefault();
      if (this.errors.items.length > 0) {
        return;
      }request
        .get(
          `${APIURL}/history?start_at=${moment(this.form.startDate).format(
            "YYYY-MM-DD"
          )}&end_at=${moment(this.form.endDate).format("YYYY-MM-DD")}&base=${
            this.form.from
          }`
        )
        .end((err, res) => {
          let body = res.body;
          this.hasResult = true;
          if (!body.rates) {
            this.resultNotFound = true;
            return;
          } else {
            this.resultNotFound = false;
          }
          let rates = Object.keys(body.rates)
            .map(date => {
              return {
                date: moment(date).format("YYYY-MM-DD"),
                rates: Object.keys(body.rates[date]).map(k => {
                  return {
                    symbol: k,
                    rate: body.rates[date][k]
                  };
                })
              };
            })
            .sort((a, b) => +new Date(a.date) - +new Date(b.date));
          let exchangeRates = [];
          rates.forEach(r => {
            let currency = r.rates.find(rr => rr.symbol == this.form.to);
            let rate;
            if (currency) {
              rate = currency.rate;
            }
            if (rate) {
              exchangeRates.push(rate);
            }
          });

          this.graphData = {
            labels: rates.map(r => r.date),
            datasets: [
              {
                label: `Exchange Rate ${this.form.from} - ${this.form.to}`,
                backgroundColor: "#f87979",
                data: exchangeRates,
                fill: false
              }
            ]
          };
          if (exchangeRates.length == 0) {
            this.resultNotFound = true;
          }
        });
    },
    invalidStartDates(evt) {
      return +evt > +new Date();
    },
    invalidEndDates(evt) {
      return +evt < +this.form.startDate || +evt > +new Date();
    }
  },
  beforeMount() {
    this.symbols = Object.keys(this.currencies).map(k => {
      return {
        symbol: k,
        ...this.currencies[k]
      };
    });
    this.toSymbols = JSON.parse(JSON.stringify(this.symbols));
    this.fromSymbols = JSON.parse(JSON.stringify(this.symbols));
  }
};
</script>

We massage the data returned by the API to a structure that can be used by the vue-chartjs library’s LineGraph component. We put the x-axis labels in an array in the labels property. In the datasets array of the graphData object, we have one object in the array with the label field, which contains the title of the graph. backgroundColor has the background color. The data field is an array with the y coordinates of the graph. The fill property sets the fill of the line.

To display our line graph in our app, we have to create a component required by Vue Chart.js. Make a file calledLineGraph.vue, and add the following:

<script>
import { Line } from 'vue-chartjs'export default {
  extends: Line,
  props: ['chartdata', 'options'],
  mounted () {
    this.renderChart(this.chartdata, this.options)
  }
}
</script><style>
</style>

Then we make a page to display our quotes. Call it Quotes.vue, and add the following:

<template>
  <div class="page-container md-layout-column">
    <drawer></drawer>
    <md-content>
      <h1 class="center">Quotes as of {{currentTime}}</h1>
      <div class="container">
        <div class="md-layout-item">
          <md-field>
            <label for="movie">Base Currency</label>
            <md-select v-model="baseCurrency" name="baseCurrency">
              <md-option value="EUR">Euro</md-option>
              <md-option value="USD">US Dollar</md-option>
              <md-option value="CAD">Canadian Dollar</md-option>
            </md-select>
          </md-field>
        </div>
        <div>
          <md-content class="md-layout-item" v-for="(q, index) in quoteData.rates" :key="index">
            <h2>{{quoteData.base}} - {{q.symbol| currencyName}}</h2>
            <p>Price: {{q.rate}}</p>
          </md-content>
        </div>
      </div>
    </md-content>
  </div>
</template><script>
import currencies from "../currencies";
import { APIURL } from "../urls";
import * as moment from "moment";
const request = require("superagent");export default {
  name: "quotes",
  data() {
    return {
      quoteData: {},
      currentTime: moment().format("YYYY-MM-DD HH:mm:ss a"),
      baseCurrency: null,
      showNavigation: false
    };
  },
  methods: {
    getQuotes() {
      let url = `${APIURL}/latest`;
      if (this.baseCurrency) {
        url = `${APIURL}/latest?base=${this.baseCurrency}`;
      }
      request.get(url).end((err, res) => {
        this.quoteData = res.body;
        this.quoteData.rates = Object.keys(this.quoteData.rates).map(k => {
          return {
            symbol: k,
            rate: this.quoteData.rates[k]
          };
        });
        this.currentTime = moment().format("YYYY-MM-DD HH:mm:ss a");
      });
    }
  },
  beforeMount() {
    this.getQuotes();
  },
  watch: {
    baseCurrency: function() {
      this.getQuotes();
    }
  }
};
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style></style>

In the end, we get the working currency converter app written in Vue:

Categories
Bootstrap React

How to Build a New York Times App with React and Bootstrap

React is a great framework for building interactive web apps. It comes with a bare bones set of features. It can render your page when you update your data and provides a convenient syntax for you to write your code easily. We can easily use it to build apps that uses public APIs, like the New York Times API.

In this story, we will build an app that uses the New York Times API with multilingual capability. You can view the static text in the app in English or French. The app can get the news from different sections and also do searches. It supports cross domain HTTP requests so that we can write client side apps that uses the API.

Before building the app, you need to register for an API key at https://developer.nytimes.com/

To start building the app, we use the Create React App command line utility to generate the scaffolding code. To use it, we run npx create-react-app nyt-app to create the code in the nyt-app folder. After that we need to install some libraries. We need the Axios HTTP client, a library to convert objects into query strings, Bootstrap library to make everything look better, React Router for routing and create forms easily with Formik and Yup. For translation and localization, we use the React-i18next library to allow us to translate our text into English and French. To install the libraries, we run npm i axios bootstrap formik i18next i18next-browser-languagedetector i18next-xhr-backend querystring react-bootstrap react-i18next react-router-dom yup .

Now that we have all the libraries installed, we can start writing code. For simplicity’s sake, we put everything in the src folder. We start by modifying App.js . We replace the existing code with:

import React from "react";
import { Router, Route, Link } from "react-router-dom";
import HomePage from "./HomePage";
import TopBar from "./TopBar";
import { createBrowserHistory as createHistory } from "history";
import "./App.css";
import SearchPage from "./SearchPage";
import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react";
const history = createHistory();

function App() {
  const { t, i18n } = useTranslation();
  const [initialized, setInitialized] = useState(false);
  const changeLanguage = lng => {
    i18n.changeLanguage(lng);
  };

useEffect(() => {
    if (!initialized) {
      changeLanguage(localStorage.getItem("language") || "en");
      setInitialized(true);
    }
  });

return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route path="/" exact component={HomePage} />
        <Route path="/search" exact component={SearchPage} />
      </Router>
    </div>
  );
}

export default App;

This is the root component of our app and is the component that is loaded when the app first loads. We use the useTranslation function from the react-i18next library returns an object with the t property and the i18n property,. Here, we destructured the returned object’s properties into its own variables. we will use the t , which takes a translation key, to get the English or French text depending on the language set. In this file, we use the i18n function to set the language with the provided i18n.changeLanguage function. We also set the language from local storage when provided so that the chosen language will be persisted after refresh.

We also add the routes for our pages here used by the React router.

In App.css ,we put:

.center {
  text-align: center;
}

to center some text.

Next we make the home page. we createHomePage.js and in the file, we put:

import React from "react";
import { useState, useEffect } from "react";
import Form from "react-bootstrap/Form";
import ListGroup from "react-bootstrap/ListGroup";
import Card from "react-bootstrap/Card";
import Button from "react-bootstrap/Button";
import { getArticles } from "./requests";
import { useTranslation } from "react-i18next";
import "./HomePage.css";

const sections = `arts, automobiles, books, business, fashion, food, health,
home, insider, magazine, movies, national, nyregion, obituaries,
opinion, politics, realestate, science, sports, sundayreview,
technology, theater, tmagazine, travel, upshot, world`
  .replace(/ /g, "")
  .split(",");

function HomePage() {
  const [selectedSection, setSelectedSection] = useState("arts");
  const [articles, setArticles] = useState([]);
  const [initialized, setInitialized] = useState(false);
  const { t, i18n } = useTranslation();

const load = async section => {
    setSelectedSection(section);
    const response = await getArticles(section);
    setArticles(response.data.results || []);
  };

const loadArticles = async e => {
    if (!e || !e.target) {
      return;
    }
    setSelectedSection(e.target.value);
    load(e.target.value);
  };

const initializeArticles = () => {
    load(selectedSection);
    setInitialized(true);
  };

useEffect(() => {
    if (!initialized) {
      initializeArticles();
    }
  });

return (
    <div className="HomePage">
      <div className="col-12">
        <div className="row">
          <div className="col-md-3 d-none d-md-block d-lg-block d-xl-block">
            <ListGroup className="sections">
              {sections.map(s => (
                <ListGroup.Item
                  key={s}
                  className="list-group-item"
                  active={s == selectedSection}
                >
                  <a
                    className="link"
                    onClick={() => {
                      load(s);
                    }}
                  >
                    {t(s)}
                  </a>
                </ListGroup.Item>
              ))}
            </ListGroup>
          </div>
          <div className="col right">
            <Form className="d-sm-block d-md-none d-lg-none d-xl-none">
              <Form.Group controlId="section">
                <Form.Label>{t("Section")}</Form.Label>
                <Form.Control
                  as="select"
                  onChange={loadArticles}
                  value={selectedSection}
                >
                  {sections.map(s => (
                    <option key={s} value={s}>{t(s)}</option>
                  ))}
                </Form.Control>
              </Form.Group>
            </Form>
            <h1>{t(selectedSection)}</h1>
            {articles.map((a, i) => (
              <Card key={i}>
                <Card.Body>
                  <Card.Title>{a.title}</Card.Title>
                  <Card.Img
                    variant="top"
                    className="image"
                    src={
                      Array.isArray(a.multimedia) &&
                      a.multimedia[a.multimedia.length - 1]
                        ? a.multimedia[a.multimedia.length - 1].url
                        : null
                    }
                  />
                  <Card.Text>{a.abstract}</Card.Text>
                  <Button
                    variant="primary"
                    onClick={() => (window.location.href = a.url)}
                  >
                    {t("Go")}
                  </Button>
                </Card.Body>
              </Card>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

export default HomePage;

In this file, we display a responsive layout where there is a left bar is the screen is wide and the a drop down on the right pane if it’s not. We display the items in the chosen section we choose from the left pane or the drop down. To display the items, we use the Card widget from React Bootstrap. We also use the t function provided by react-i18next to load our text from our translation file, which we will create. To load the initial article entries, we run a function in the callback of the useEffect function to show load the items once from the New York Times API. We need the initialized flag so that the function in the callback won’t load on every re-render. In the drop down, we added code to load articles whenever the selection changes.

The we createHomePage.css , and add:

.link {
  cursor: pointer;
}

.right {
  padding: 20px;
}

.image {
  max-width: 400px;
  text-align: center;
}

.sections {
    margin-top: 20px;
}

We change the cursor style for the Go button and add some padding to the right pane.

Next we create a file for loading the translations and setting the default language. Create a file called i18n.js and add:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { resources } from "./translations";
import Backend from "i18next-xhr-backend";
import LanguageDetector from "i18next-browser-languagedetector";

i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    lng: "en",
    fallbackLng: "en",
    debug: true,

interpolation: {
      escapeValue: false,
    },
  });

export default i18n;

In this file, we load the translations from a file, and set the default language to English. Since react-i18next escapes everything, we can set escapeValue to false for interpolation since it’s redundant.

We need a file to put the code for making HTTP requests. To do so, we create a file called requests.js and add:

const APIURL = "https://api.nytimes.com/svc";
const axios = require("axios");
const querystring = require("querystring");

export const search = data => {
  Object.keys(data).forEach(key => {
    data["api-key"] = process.env.REACT_APP_APIKEY;
    if (!data[key]) {
      delete data[key];
    }
  });
  return axios.get(
    `${APIURL}/search/v2/articlesearch.json?${querystring.encode(data)}`
  );
};
export const getArticles = section =>
  axios.get(
    `${APIURL}/topstories/v2/${section}.json?api-key=${process.env.REACT_APP_APIKEY}`
  );

We load the API key from the process.env.REACT_APP_APIKEY variable, which is provided by an environment variable in the .env file located at the root folder. You have to create it yourself, and in there, put:

REACT_APP_APIKEY='you New York Times API key'

Replace the value on the right side with the API key you got after registering in the New York Times API website.

Next we create the search page. Create a file called SearchPage.js and add:

import React from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import "./SearchPage.css";
import * as yup from "yup";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { Trans } from "react-i18next";
import { search } from "./requests";
import Card from "react-bootstrap/Card";

const schema = yup.object({
  keyword: yup.string().required("Keyword is required"),
});

function SearchPage() {
  const { t } = useTranslation();
  const [articles, setArticles] = useState([]);
  const [count, setCount] = useState(0);

const handleSubmit = async e => {
    const response = await search({ q: e.keyword });
    setArticles(response.data.response.docs || []);
  };

return (
    <div className="SearchPage">
      <h1 className="center">{t("Search")}</h1>
      <Formik validationSchema={schema} onSubmit={handleSubmit}>
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
          <Form noValidate onSubmit={handleSubmit} className="form">
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="keyword">
                <Form.Label>{t("Keyword")}</Form.Label>
                <Form.Control
                  type="text"
                  name="keyword"
                  placeholder={t("Keyword")}
                  value={values.keyword || ""}
                  onChange={handleChange}
                  isInvalid={touched.keyword && errors.keyword}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.keyword}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              {t("Search")}
            </Button>
          </Form>
        )}
      </Formik>
      <h3 className="form">
        <Trans i18nKey="numResults" count={articles.length}>
          There are <strong>{{ count }}</strong> results.
        </Trans>
      </h3>
      {articles.map((a, i) => (
        <Card key={i}>
          <Card.Body>
            <Card.Title>{a.headline.main}</Card.Title>
            <Card.Text>{a.abstract}</Card.Text>
            <Button
              variant="primary"
              onClick={() => (window.location.href = a.web_url)}
            >
              {t("Go")}
            </Button>
          </Card.Body>
        </Card>
      ))}
    </div>
  );
}

export default SearchPage;

This is where we create a search form with the keyword field used for searching the API. When the use clicks Search, then it will search the New York Times API for articles using the keyword. We use Formik to handle the form value changes and make the values available in the e object in the handleSubmit parameter for us to use. We use React Bootstrap for the buttons, form elements, and the cards. After clicking Search, the articles variable get set and load the cards for the articles.

We use the Trans component provided by react-i18next for translating text that have some dynamic components, like in the example above. We have a variable in the text for the number of results. Whenever you have something like, you wrap it in the Trans component and then pass in the variables like in the example above by passing in the variables as props. Then you will show the variable in the text between the Trans tags. We we will also make interpolation available in the translations by putting “There are <1>{{count}}</1> results.” in the English translation and “Il y a <1>{{count}}</1> résultats.” in French translation. The 1 tag corresponds to the strong tag. The number in this case is arbitrary. As long as the pattern consistent with the component’s pattern, it will work, so strong tag in this case should always be 1 in the translation string.

To add the translations mentioned above along with the rest of the translations, create a file called translations.js and add:

const resources = {
  en: {
    translation: {
      "New York Times App": "New York Times App",
      arts: "Arts",
      automobiles: "Automobiles",
      books: "Books",
      business: "Business",
      fashion: "Fashion",
      food: "Food",
      health: "Health",
      home: "Home",
      insider: "Inside",
      magazine: "Magazine",
      movies: "Movies",
      national: "National",
      nyregion: "New York Region",
      obituaries: "Obituaries",
      opinion: "Opinion",
      politics: "Politics",
      realestate: "Real Estate",
      science: "Science",
      sports: "Sports",
      sundayreview: "Sunday Review",
      technology: "Technology",
      theater: "Theater",
      tmagazine: "T Magazine",
      travel: "Travel",
      upshot: "Upshot",
      world: "World",
      Search: "Search",
      numResults: "There are <1>{{count}}</1> results.",
      Home: "Home",
      Search: "Search",
      Language: "Language",
      English: "English",
      French: "French",
      Keyword: "Keyword",
      Go: "Go",
      Section: "Section",
    },
  },
  fr: {
    translation: {
      "New York Times App": "App New York Times",
      arts: "Arts",
      automobiles: "Les automobiles",
      books: "Livres",
      business: "Entreprise",
      fashion: "Mode",
      food: "Aliments",
      health: "Santé",
      home: "Maison",
      insider: "Initiée",
      magazine: "Magazine",
      movies: "Films",
      national: "Nationale",
      nyregion: "La région de new york",
      obituaries: "Notices nécrologiques",
      opinion: "Opinion",
      politics: "Politique",
      realestate: "Immobilier",
      science: "Science",
      sports: "Des sports",
      sundayreview: "Avis dimanche",
      technology: "La technologie",
      theater: "Théâtre",
      tmagazine: "Magazine T",
      travel: "Voyage",
      upshot: "Résultat",
      world: "Monde",
      Search: "Search",
      numResults: "Il y a <1>{{count}}</1> résultats.",
      Home: "Page d'accueil",
      Search: "Chercher",
      Language: "La langue",
      English: "Anglais",
      French: "Français",
      Keyword: "Mot-clé",
      Go: "Aller",
      Section: "Section",
    },
  },
};

export { resources };

We have the static text translations, and the interpolated text we mentioned above in this file.

Finally, we create the top bar by creating TopBar.js and add:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import NavDropdown from "react-bootstrap/NavDropdown";
import "./TopBar.css";
import { withRouter } from "react-router-dom";
import { useTranslation } from "react-i18next";

function TopBar({ location }) {
  const { pathname } = location;
  const { t, i18n } = useTranslation();
  const changeLanguage = lng => {
    localStorage.setItem("language", lng);
    i18n.changeLanguage(lng);
  };

return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">{t("New York Times App")}</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={pathname == "/"}>
            {t("Home")}
          </Nav.Link>
          <Nav.Link href="/search" active={pathname.includes("/search")}>
            {t("Search")}
          </Nav.Link>
          <NavDropdown title={t("Language")} id="basic-nav-dropdown">
            <NavDropdown.Item onClick={() => changeLanguage("en")}>
              {t("English")}
            </NavDropdown.Item>
            <NavDropdown.Item onClick={() => changeLanguage("fr")}>
              {t("French")}
            </NavDropdown.Item>
          </NavDropdown>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}

export default withRouter(TopBar);

We use the NavBar component provided by React Boostrap and we add a drop down for users to select a language and when they click those items, they can set the language. Notice that we wrapped the TopBar component with the withRouter function so that we get the current route’s value with the location prop, and use it to set which link is active by setting the active prop in the Nav.Link components.

Finally, we replace the existing code in index.html with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/`
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React New York Times App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

to add the Bootstrap CSS and change the title of the app.

After all the work is done, when we run npm start , we get:

Categories
React

Use React Refs to Manipulate the DOM and Konva to Add Graphics

React is a flexible framework that provides structured UI code while allowing the flexibility to manipulate DOM elements directly. All React components can be accessed by their refs. A React ref provides access to the underlying HTML DOM element that you can manipulate directly.

To use refs, we use the React.createRef function the useRef hook to create a ref object and then assign it to a variable. he variable is set as the value of the ref prop.

For example, we define a ref with:

const inputRef = React.createRef();

Then in the input element, we add the following:

<input type="text" ref={inputRef} />

Then we can access the input element by adding:

inputRef.current

This provides access to the HTML element, and we can utilize native DOM functionality. In our app, we will use the useRef hook, and you will see how to implement that below.

What we are building

In this article, we will build a whiteboard app that allows users to add shapes, texts, lines, and images to a whiteboard. In addition, users can undo their work and erase stuff from the screen.

We use the Konva library to let us add the shapes/text and erase them. The Konva library abstracts the hard work of adding items to the canvas. It allows you to add many shapes by simply writing a few lines of code. There are also React bindings for Konva, which abstracts some functionality even further for React. However, the feature set of React Konva is rather limited, so in order to meet the requirements of most apps, React Konva should be used as a companion to Konva.

We also want to allow users to move and transform your shapes easily, which you would have to write yourself if you wanted to do it in the HTML Canvas API.

Konva works by creating a stage and a layer in the stage which will allow you to add the lines, shapes, and text that you want.

Getting started

To start, we will create a React app with the Create React App command line program. Run npx create-react-app whiteboard-app to create the initial files for our app. Next, we need to add some packages. We want to use Bootstrap for styling, in addition to the Konva packages, and helper package for creating unique IDs for our shapes, lines, and text. We also need React Router for routing.

To install the libraries, we run:

npm i bootstrap react-bootstrap konva react-konva react-router-dom use-image uuid

use-image is a package to convert image URLs into image objects that can be displayed on canvas. The UUID package generates unique IDs for our shapes.

With the packages installed, we can start writing code. First we start with the entry point of our app, which is App.js. Replace the existing code of the file with:

import React from "react";
import { Router, Route, Link } from "react-router-dom";
import HomePage from "./HomePage";
import TopBar from "./TopBar";
import { createBrowserHistory as createHistory } from "history";
import "./App.css";
const history = createHistory();function App() {
  return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route path="/" exact component={HomePage} />
      </Router>
    </div>
  );
}export default App;

All we added is a top navigation bar and our only route which is the home page.

Next we add the code for the shapes. React Konva has libraries for common shapes like rectangles and circles. We first start with a circle. In the src folder, create a file called Circle.js and add:

import React from "react";
import { Circle, Transformer } from "react-konva";const Circ = ({ shapeProps, isSelected, onSelect, onChange }) => {
  const shapeRef = React.useRef();
  const trRef = React.useRef();React.useEffect(() => {
    if (isSelected) {
      trRef.current.setNode(shapeRef.current);
      trRef.current.getLayer().batchDraw();
    }
  }, [isSelected]);return (
    <React.Fragment>
      <Circle
        onClick={onSelect}
        ref={shapeRef}
        {...shapeProps}
        draggable
        onDragEnd={e => {
          onChange({
            ...shapeProps,
            x: e.target.x(),
            y: e.target.y(),
          });
        }}
        onTransformEnd={e => {
          // transformer is changing scale
          const node = shapeRef.current;
          const scaleX = node.scaleX();
          const scaleY = node.scaleY();
          node.scaleX(1);
          node.scaleY(1);
          onChange({
            ...shapeProps,
            x: node.x(),
            y: node.y(),
            width: node.width() * scaleX,
            height: node.height() * scaleY,
          });
        }}
      />
      {isSelected && <Transformer ref={trRef} />}
    </React.Fragment>
  );
};export default Circ;

This code returns the Circle shape which can be added onto the canvas at will. In the React.useEffect’s callback function, we detect if the shape is selected and then draw a handle for the shape so that it can be resized and moved.

In this file, we added refs to the Circle component so that we can access it in the useEffect callback. The setNode function takes an HTML DOM element.

The component in the return statement is the main code for the Circle . We have an onClick handler that gets the ID of the selected shape. The draggable prop makes the Circle draggable. onDragEnd handles the event when the user stops dragging. The position is updated there. onTransformEnd scales the shape as the user drags the handles that are available. The width and height are changed as the handles are dragged. {isSelected && <Transformer ref={trRef} />} create the Transformer object, which is a Konva object that lets you change the size of a shape when you select.

Next we add a component for the image. Create a file called Image.js in the src folder and add the following:

import React from "react";
import { Image, Transformer } from "react-konva";
import useImage from "use-image";const Img = ({ shapeProps, isSelected, onSelect, onChange, imageUrl }) => {
  const shapeRef = React.useRef();
  const trRef = React.useRef();
  const [image] = useImage(imageUrl); React.useEffect(() => {
    if (isSelected) {
      trRef.current.setNode(shapeRef.current);
      trRef.current.getLayer().batchDraw();
    }
  }, [isSelected]); return (
    <React.Fragment>
      <Image
        onClick={onSelect}
        image={image}
        ref={shapeRef}
        draggable
        onDragEnd={e => {
          onChange({
            ...shapeProps,
            x: e.target.x(),
            y: e.target.y(),
          });
        }}
        onTransformEnd={e => {
          const node = shapeRef.current;
          const scaleX = node.scaleX();
          const scaleY = node.scaleY();
          onChange({
            ...shapeProps,
            x: node.x(),
            y: node.y(),
            width: node.width() * scaleX,
            height: node.height() * scaleY,
          });
        }}
      />
      {isSelected && <Transformer ref={trRef} />}
    </React.Fragment>
  );
};export default Img;

This is very similar to the Circle component except we have the useImage function provided by the use-image library to convert the given imageUrl prop to an image that is displayed on the canvas.

In this file, we added refs to the Image component so that we can access it in the useEffect callback. The setNode function takes an HTML DOM element.

Next we create a free drawing line. Create a file called line.js in the src folder and add:

import Konva from "konva";
export const addLine = (stage, layer, mode = "brush") => {
  let isPaint = false;
  let lastLine; stage.on("mousedown touchstart", function(e) {
    isPaint = true;
    let pos = stage.getPointerPosition();
    lastLine = new Konva.Line({
      stroke: mode == "brush" ? "red" : "white",
      strokeWidth: mode == "brush" ? 5 : 20,
      globalCompositeOperation:
        mode === "brush" ? "source-over" : "destination-out",
      points: [pos.x, pos.y],
      draggable: mode == "brush",
    });
    layer.add(lastLine);
  }); stage.on("mouseup touchend", function() {
    isPaint = false;
  }); stage.on("mousemove touchmove", function() {
    if (!isPaint) {
      return;
    } const pos = stage.getPointerPosition();
    let newPoints = lastLine.points().concat([pos.x, pos.y]);
    lastLine.points(newPoints);
    layer.batchDraw();
  });
};

In this file, we use plain Konva since React Konva does not have a convenient way to make free drawing a line where a user drags the mouse. When the mousedown and touchstart is triggered, we set the color of the line depending on what the mode is. When it is brush, we draw a red line. If it’s erase we draw a thick white line so that users can draw it over their content, letting users erase their changes.

When the mousemove and touchend events are triggered, we set isPaint to false so we stop drawing the line. When the mousemove and touchmove events are triggered, we add dots along the way to draw the line in the direction the user wants when the user moves the mouse when clicking or touching the touchscreen.

stage and layer are the Konva Stage and Layer objects which we pass in when the addLine function is called.

Next we create the Rectangle component for drawing free form rectangles. In the src folder, create a file called Rectangle.js and add:

import React from "react";
import { Rect, Transformer } from "react-konva";
const Rectangle = ({ shapeProps, isSelected, onSelect, onChange }) => {
  const shapeRef = React.useRef();
  const trRef = React.useRef();

  React.useEffect(() => {
    if (isSelected) {
      // we need to attach transformer manually
      trRef.current.setNode(shapeRef.current);
      trRef.current.getLayer().batchDraw();
    }
  }, [isSelected]); return (
    <React.Fragment>
      <Rect
        onClick={onSelect}
        ref={shapeRef}
        {...shapeProps}
        draggable
        onDragEnd={e => {
          onChange({
            ...shapeProps,
            x: e.target.x(),
            y: e.target.y(),
          });
        }}
        onTransformEnd={e => {
          // transformer is changing scale
          const node = shapeRef.current;
          const scaleX = node.scaleX();
          const scaleY = node.scaleY();
          node.scaleX(1);
          node.scaleY(1);
          onChange({
            ...shapeProps,
            x: node.x(),
            y: node.y(),
            width: node.width() * scaleX,
            height: node.height() * scaleY,
          });
        }}
      />
      {isSelected && <Transformer ref={trRef} />}
    </React.Fragment>
  );
};export default Rectangle;

This component is similar to Circle component. We have the drag handles to move and resize the rectangle by adding the onDragEnd and onTransformEnd callbacks, change the x and y coordinates in the onDragEnd handler, and change the width and height in the onTransformEnd event callback.

The Transformer component is added if the shape is selected so that users can move or resize the shape with the handles when selected.

Similar to the Circle component, we added refs to the Rectangle component so that we can access it in the useEffect callback. The setNode function takes an HTML DOM element.

Next we add a text field component to let users can add text to the whiteboard. Create a file called textNode.js and add the following:

import Konva from "konva";
const uuidv1 = require("uuid/v1");
export const addTextNode = (stage, layer) => {
  const id = uuidv1();
  const textNode = new Konva.Text({
    text: "type here",
    x: 50,
    y: 80,
    fontSize: 20,
    draggable: true,
    width: 200,
    id,
  }); 

  layer.add(textNode); let tr = new Konva.Transformer({
    node: textNode,
    enabledAnchors: ["middle-left", "middle-right"],
    // set minimum width of text
    boundBoxFunc: function(oldBox, newBox) {
      newBox.width = Math.max(30, newBox.width);
      return newBox;
    },
  }); 

  stage.on("click", function(e) {
    if (!this.clickStartShape) {
      return;
    }
    if (e.target._id == this.clickStartShape._id) {
      layer.add(tr);
      tr.attachTo(e.target);
      layer.draw();
    } else {
      tr.detach();
      layer.draw();
    }
  }); 

  textNode.on("transform", function() {
    // reset scale, so only with is changing by transformer
    textNode.setAttrs({
      width: textNode.width() * textNode.scaleX(),
      scaleX: 1,
    });
  }); 

  layer.add(tr); layer.draw(); textNode.on("dblclick", () => {
    // hide text node and transformer:
    textNode.hide();
    tr.hide();
    layer.draw(); 
    let textPosition = textNode.absolutePosition();
    let stageBox = stage.container().getBoundingClientRect();
    let areaPosition = {
      x: stageBox.left + textPosition.x,
      y: stageBox.top + textPosition.y,
    };// create textarea and style it
    let textarea = document.createElement("textarea");
    document.body.appendChild(textarea);
    textarea.value = textNode.text();
    textarea.style.position = "absolute";
    textarea.style.top = areaPosition.y + "px";
    textarea.style.left = areaPosition.x + "px";
    textarea.style.width = textNode.width() - textNode.padding() * 2 + "px";
    textarea.style.height =
      textNode.height() - textNode.padding() * 2 + 5 + "px";
    textarea.style.fontSize = textNode.fontSize() + "px";
    textarea.style.border = "none";
    textarea.style.padding = "0px";
    textarea.style.margin = "0px";
    textarea.style.overflow = "hidden";
    textarea.style.background = "none";
    textarea.style.outline = "none";
    textarea.style.resize = "none";
    textarea.style.lineHeight = textNode.lineHeight();
    textarea.style.fontFamily = textNode.fontFamily();
    textarea.style.transformOrigin = "left top";
    textarea.style.textAlign = textNode.align();
    textarea.style.color = textNode.fill();
    let rotation = textNode.rotation();
    let transform = "";
    if (rotation) {
      transform += "rotateZ(" + rotation + "deg)";
    } let px = 0;
    let isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
    if (isFirefox) {
      px += 2 + Math.round(textNode.fontSize() / 20);
    }
    transform += "translateY(-" + px + "px)"; 
    textarea.style.transform = transform;
    textarea.style.height = "auto";
 
    textarea.style.height = textarea.scrollHeight + 3 + "px"; textarea.focus(); function removeTextarea() {
      textarea.parentNode.removeChild(textarea);
      window.removeEventListener("click", handleOutsideClick);
      textNode.show();
      tr.show();
      tr.forceUpdate();
      layer.draw();
    } function setTextareaWidth(newWidth) {
      if (!newWidth) {
        // set width for placeholder
        newWidth = textNode.placeholder.length * textNode.fontSize();
      }
      // some extra fixes on different browsers
      let isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
      let isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
      if (isSafari || isFirefox) {
        newWidth = Math.ceil(newWidth);
      } let isEdge = document.documentMode || /Edge/.test(navigator.userAgent);
      if (isEdge) {
        newWidth += 1;
      }
      textarea.style.width = newWidth + "px";
    } textarea.addEventListener("keydown", function(e) {
      // hide on enter
      // but don't hide on shift + enter
      if (e.keyCode === 13 && !e.shiftKey) {
        textNode.text(textarea.value);
        removeTextarea();
      }
      // on esc do not set value back to node
      if (e.keyCode === 27) {
        removeTextarea();
      }
    }); textarea.addEventListener("keydown", function(e) {
      let scale = textNode.getAbsoluteScale().x;
      setTextareaWidth(textNode.width() * scale);
      textarea.style.height = "auto";
      textarea.style.height =
        textarea.scrollHeight + textNode.fontSize() + "px";
    }); function handleOutsideClick(e) {
      if (e.target !== textarea) {
        removeTextarea();
      }
    }
    setTimeout(() => {
      window.addEventListener("click", handleOutsideClick);
    });
  });
  return id;
};

We add a text area, and then we handle the events created by the text area. When the user clicks the text area, a box will with handles will be displayed to let the user move the text area around the canvas. This is what the click handler for the stage is doing. It finds the text area by ID and then attaches a KonvaTransformer to it, adding the box with handles.

We have a transform handler for the textNode text area to resize the text area when the user drags the handles. We have a double click handler to let users enter text when they double click. Most of the code is for styling the text box as close to the canvas as possible so that it will blend into the canvas. Otherwise, it will look strange. We also let users rotate the text area by applying CSS for rotating the text area as the user drags the handles.

In the keydown event handler, we change the size of the text area as the user types to make sure it displays all the text without scrolling.

When the user clicks outside the text area, the box with handles will disappear, letting the user select other items.

The home page is where we put everything together. Create a new file called HomePage.js in the src folder and add:

import React, { useState, useRef } from "react";
import ButtonGroup from "react-bootstrap/ButtonGroup";
import Button from "react-bootstrap/Button";
import "./HomePage.css";
import { Stage, Layer } from "react-konva";
import Rectangle from "./Rectangle";
import Circle from "./Circle";
import { addLine } from "./line";
import { addTextNode } from "./textNode";
import Image from "./Image";
const uuidv1 = require("uuid/v1");function HomePage() {
  const [rectangles, setRectangles] = useState([]);
  const [circles, setCircles] = useState([]);
  const [images, setImages] = useState([]);
  const [selectedId, selectShape] = useState(null);
  const [shapes, setShapes] = useState([]);
  const [, updateState] = React.useState();
  const stageEl = React.createRef();
  const layerEl = React.createRef();
  const fileUploadEl = React.createRef(); const getRandomInt = max => {
    return Math.floor(Math.random() * Math.floor(max));
  }; 

  const addRectangle = () => {
    const rect = {
      x: getRandomInt(100),
      y: getRandomInt(100),
      width: 100,
      height: 100,
      fill: "red",
      id: `rect${rectangles.length + 1}`,
    };
    const rects = rectangles.concat([rect]);
    setRectangles(rects);
    const shs = shapes.concat([`rect${rectangles.length + 1}`]);
    setShapes(shs);
  }; 

  const addCircle = () => {
    const circ = {
      x: getRandomInt(100),
      y: getRandomInt(100),
      width: 100,
      height: 100,
      fill: "red",
      id: `circ${circles.length + 1}`,
    };
    const circs = circles.concat([circ]);
    setCircles(circs);
    const shs = shapes.concat([`circ${circles.length + 1}`]);
    setShapes(shs);
  }; 

  const drawLine = () => {
    addLine(stageEl.current.getStage(), layerEl.current);
  }; 

  const eraseLine = () => {
    addLine(stageEl.current.getStage(), layerEl.current, "erase");
  }; 

  const drawText = () => {
    const id = addTextNode(stageEl.current.getStage(), layerEl.current);
    const shs = shapes.concat([id]);
    setShapes(shs);
  }; 

  const drawImage = () => {
    fileUploadEl.current.click();
  }; 

  const forceUpdate = React.useCallback(() => updateState({}), []); 

  const fileChange = ev => {
    let file = ev.target.files[0];
    let reader = new FileReader(); reader.addEventListener(
      "load",
      () => {
        const id = uuidv1();
        images.push({
          content: reader.result,
          id,
        });
        setImages(images);
        fileUploadEl.current.value = null;
        shapes.push(id);
        setShapes(shapes);
        forceUpdate();
      },
      false
    );if (file) {
      reader.readAsDataURL(file);
    }
  }; 

  const undo = () => {
    const lastId = shapes[shapes.length - 1];
    let index = circles.findIndex(c => c.id == lastId);
    if (index != -1) {
      circles.splice(index, 1);
      setCircles(circles);
    } index = rectangles.findIndex(r => r.id == lastId);
    if (index != -1) {
      rectangles.splice(index, 1);
      setRectangles(rectangles);
    } index = images.findIndex(r => r.id == lastId);
    if (index != -1) {
      images.splice(index, 1);
      setImages(images);
    }
    shapes.pop();
    setShapes(shapes);
    forceUpdate();
  }; 

  document.addEventListener("keydown", ev => {
    if (ev.code == "Delete") {
      let index = circles.findIndex(c => c.id == selectedId);
      if (index != -1) {
        circles.splice(index, 1);
        setCircles(circles);
      } index = rectangles.findIndex(r => r.id == selectedId);
      if (index != -1) {
        rectangles.splice(index, 1);
        setRectangles(rectangles);
      } index = images.findIndex(r => r.id == selectedId);
      if (index != -1) {
        images.splice(index, 1);
        setImages(images);
      }
      forceUpdate();
    }
  }); return (
    <div className="home-page">
      <h1>Whiteboard</h1>
      <ButtonGroup>
        <Button variant="secondary" onClick={addRectangle}>
          Rectangle
        </Button>
        <Button variant="secondary" onClick={addCircle}>
          Circle
        </Button>
        <Button variant="secondary" onClick={drawLine}>
          Line
        </Button>
        <Button variant="secondary" onClick={eraseLine}>
          Erase
        </Button>
        <Button variant="secondary" onClick={drawText}>
          Text
        </Button>
        <Button variant="secondary" onClick={drawImage}>
          Image
        </Button>
        <Button variant="secondary" onClick={undo}>
          Undo
        </Button>
      </ButtonGroup>
      <input
        style={{ display: "none" }}
        type="file"
        ref={fileUploadEl}
        onChange={fileChange}
      />
      <Stage
        width={window.innerWidth * 0.9}
        height={window.innerHeight - 150}
        ref={stageEl}
        onMouseDown={e => {
          // deselect when clicked on empty area
          const clickedOnEmpty = e.target === e.target.getStage();
          if (clickedOnEmpty) {
            selectShape(null);
          }
        }}
      >
        <Layer ref={layerEl}>
          {rectangles.map((rect, i) => {
            return (
              <Rectangle
                key={i}
                shapeProps={rect}
                isSelected={rect.id === selectedId}
                onSelect={() => {
                  selectShape(rect.id);
                }}
                onChange={newAttrs => {
                  const rects = rectangles.slice();
                  rects[i] = newAttrs;
                  setRectangles(rects);
                }}
              />
            );
          })}
          {circles.map((circle, i) => {
            return (
              <Circle
                key={i}
                shapeProps={circle}
                isSelected={circle.id === selectedId}
                onSelect={() => {
                  selectShape(circle.id);
                }}
                onChange={newAttrs => {
                  const circs = circles.slice();
                  circs[i] = newAttrs;
                  setCircles(circs);
                }}
              />
            );
          })}
          {images.map((image, i) => {
            return (
              <Image
                key={i}
                imageUrl={image.content}
                isSelected={image.id === selectedId}
                onSelect={() => {
                  selectShape(image.id);
                }}
                onChange={newAttrs => {
                  const imgs = images.slice();
                  imgs[i] = newAttrs;
                }}
              />
            );
          })}
        </Layer>
      </Stage>
    </div>
  );
}

export default HomePage;

This is where we add the buttons to create the shapes. For the shapes provided by React Konva, we create the shapes by adding an object to the array for the shape and then map them to the shape with the properties specified by the object.

For example, to add a rectangle, we create an object, add it to the array by pushing the object and then calling setRectangles and then map them to the actual Rectangle component when we render the canvas. We pass in the onSelect handler so that the user can click on the shape and get the ID of the selected shape. The onChange handler lets us update the properties of an existing shape and then update the corresponding array for the shapes.

Every React Konva shape we add should be inside the Layer component. They provide the place for the shapes to reside. The Stage component provides a place for the Layer to be in. We set the ref prop for the Stage and Layer components so that we can access it directly.

In the call to the addLine function, we get the refs for the Stage and Layer components to get the reference to the Konva Stage and Layer instances so that we can use them in the addLine function. Note that to get the Konva Stage object, we have to call setStage after the the current attribute.

In the Stage component, we have a onMouseDown handler to deselect all shapes when the click is outside all the shapes.

To the undo a change, we keep track of all the shapes with the shapes array and then when the Undo button is clicked, then the last shape is removed from the array and also the corresponding shapes array. For example, if the undo removes a rectangle, then it will be removed from the shapes array and the rectangle array. The shapes array is an array of IDs of all the shapes.

To build the image upload feature, we add an input element which we don’t show, and we use the ref to write a function to let the user click on the hidden file input. Once the file input is clicked the user can choose files and the image is read with the FileReader object into base64, which will be converted to an image displayed on the canvas with the use-image library.

Similarly for letting users delete shapes when a shape is selected with the delete key, we add a key down handler. In the key down handler function, when a delete key event is triggered then the handler will find the shapes in the arrays by ID and delete it. It will also delete it from the shapes array. We defined the forceUpdate function so that the canvas will be updated even when there is DOM manipulation done without going through React. The keydown handler is added by using document.addEventListener which is not React code, so we need to call forceUpdate to re-render according to the new states.

Finally, to finish it off, we add the top bar. Create a file called TopBar.js in the src folder and add:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import NavDropdown from "react-bootstrap/NavDropdown";
import "./TopBar.css";
import { withRouter } from "react-router-dom";function TopBar({ location }) {
  const { pathname } = location; return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">React Canvas App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={pathname == "/"}>
            Home
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}export default withRouter(TopBar);

The Navbar component is provided by React Boostrap.

After all the work is done, we get a whiteboard that we can draw on.

<img class="ds t u hd ak" src="https://miro.medium.com/max/2726/1*olGeWQXMj-a2LY-xH5If3w.png" width="1363" height="625" role="presentation"/>