Currency Converter

Beginner JavaScript

Enjoy these notes? Want to Slam Dunk JavaScript?

These are notes based on my Beginner JavaScript Video Course. It's a fun, exercise heavy approach to learning Modern JavaScript from scratch.

Use the code BEGINNERJS for an extra $10 off.

BeginnerJavaScript.com

In this video we will build a currency conversion app.

rendered html of a currency convertor

The app allows you to enter an amount to convert, specify it's currency and then specify which currency to convert it to.

We will be using the https://exchangeratesapi.io API to accomplish this, which is free and open source and will be working out of the exercises/77 - Currency Converter/ folder.

Let's start by opening the HTML to see what we are working with.

<body>
  <div class="app">
    <form>
      <input type="number" name="from_amount" />
      <select name="from_currency">
        <option>Select a Currency</option>
      </select>
      <p>in</p>
      <select name="to_currency">
        <option>Select a Currency</option>
      </select>
      <p>is</p>
      <p class="to_amount">$0</p>
    </form>
  </div>

  <script src="./money.js"></script>
</body>

As you can see we have a form with..

  • an input named from_amount
  • a selectbox named from_currency and one named to_currency
  • a paragraph tag with the class of to_amount

You will notice there are no options currently for the from or the to currency, because that will be populated on page load.

We have a script source tag right before the closing body tag.

If you open up money.js, you will see that Wes has already given us just a list of currencies, which is the currency code translated to their English version.

list of currencies and their currency code

Let's start by converting those currencies so we can populate them.

In money.js, after the currencies object, make a function called generateOptions which will accept one parameter, options, which will be an object.

Add the following code so it runs on page load and refresh the page.

const optionsHTML = generateOptions(currencies);
console.log(optionsHTML);

Let's go back to looping. How do you loop over an object?

We can use:

  • the for of loop
  • Object.entries
  • Object.keys
  • Object.values

Let's use .entries to turn this object into an array, like so 👇

function generateOptions(options) {
  return Object.entries(options);
}

If you refresh the page, you will see that gives us an array, where each item inside of the array is another array, the first time of which is the currency code and the second item being the label.

browser console showing currency array output

Let's take that array and map over it so we get the nested array, and then log it. 👇

function generateOptions(options) {
  return Object.entries(options).map((arr) => {
    console.log(arr);
  });
}
browser console showing nested currency array output

We can destructure each sub-array to two variables:

  • currencyCode
  • currencyName
function generateOptions(options) {
  return Object.entries(options).map(([currencyCode, currencyName]) => {
    console.log(currencyCode, currencyName);
  });
}
browser console output showing currencys

As you can see, it is looking good, except the array is undefined because we are not returning anything, so let's change that and return some HTML using backticks.

function generateOptions(options) {
  return Object.entries(options).map(([currencyCode, currencyName]) => {
    return `<option value="${currencyCode}">${currencyCode} - ${currencyName}</option>`;
  });
}
browser console showing outputs for currency form option tags

Now the optionsHTML variable contains an array of HTML options.

At the end of map, add .join(''), which should turn the array into one long string of HTML. 👇

function generateOptions(options) {
  return Object.entries(options)
    .map(([currencyCode, currencyName]) =>
        `<option value="${currencyCode}">${currencyCode} - ${currencyName}</option>`)
    .join("");
}

On page load, let's populate the option elements.

First go to the top of the page and select those elements.👇

const fromSelect = document.querySelector('[name="from_currency"]');
const fromInput = document.querySelector('[name="from_amount"]');

At the bottom of the page, add the following code.

const optionsHTML = generateOptions(currencies);
// populate the options elements
fromSelect.innerHTML = optionsHTML;
toSelect.innerHTML = optionsHTML;

We are populating both options with the exact same list. That is why we put optionsHTML in a variable, so that we would not unnecessarily run that function twice.

Now when you refresh, you will see that each option has a dropdown. 👇

rendered page showing currency selection dropdown

Let's switch to working on some of the functionality before we continue coding the UI any further.

Start with the function that will go fetch the rates from the exchanges API. It will accept one parameter, base which we will default to "USD" as the base currency.

Inside of the function, we need to fetch the latest rates from https://api.exchangeratesapi.io/latest.

Stick that endpoint in a variable at the top.

const endpoint = "https://api.exchangeratesapi.io/latest";

The way this endpoint works is we need to pass it a query parameter of our base currency, as shown in the image below.

api example for getting currency rates

That will return the exchange rates to us as well as the base currency.

async function fetchRates(base = "USD") {
  const res = await fetch(`${endpoint}?base=${base}`);
  const rates = await res.json();
  console.log(rates);
}

If you open the browser and try to run fetchRates() from the console, it will return to us a promise immediately because it is an async function, and then it will resolve and log our rates.

browser console showing output from fetchRates endpoint using USD

You can try passing the base currency as "CAD" and it should still work.

browser console showing output from fetchRates endpoint using CAD

Let's wrap that function up by returning rates from it.

async function fetchRates(base = "USD") {
  const res = await fetch(`${endpoint}?base=${base}`);
  const rates = await res.json();
  return rates;
}

The next thing that we want to do is make a convert function. The function will take in:

  • a raw number
  • a "from" currency
  • a "to" currency

If we do not have the rates for that specific currency, we will have to use fetchRates to go ahead and fetch it.

Make that convert function like so 👇

function convert(amount, from, to) {
  // first check if we even have the rates to convert from that currency
}

Caching the Rates

Here is where that exercise gets a little tricky.

We could fetch the rates each time, but that could be a bit slow, because every time we type into the amount input box, we will call convert function.

In the demo, you can see that the conversion is happening almost instantaneously.

demo of the conversion form

If we had to fetch the rates every single time someone typed into the box, for the number 57887 alone, we would be fetch the rate 5 times unnecessarily, and often APIs will rate limit you.

So what we need to do to get around that is cache the rates, meaning you need to hold on to them, if you already have them. There are different techniques you can do like delete rates every minute to ensure we have the latest data.

What we are going to do is we will make an object at the top of our script where the rest of our variables are and right below the endpoint let's add this empty object 👇

const ratesByBase = {};

We will store all of the rates within that object/ Every time a currency is selected to be converted, we will check whether we already have the rates within our ratesByBase object. If we do not, we will fetch it and save it to our object.

code snippet of ratesByBase const

If we were to cycle through all the currencies, our ratesByBase object would be massive because it will contain all of the rates by their from value. So by default we start with nothing because we have nothing.

Converting

Let's go back to our convert function.

We will use the square brackets to check if the currently select currency coded already existing within our object.

function convert(amount, from, to) {
  // first check if we even have the rates to convert from that currency
  if (!ratesByBase[from]) {
    console.log(`Oh no! we don't have ${from} to convert it ${to}, so let's go get it!`);
  }
}

If you refresh the page, you can try running what we have so far from the console.

browser console output showing no conversion available log

Back to our convert function, we want to mark it as async because we will need to fetch the rate if it does not already exist.

Within that function let's add a request to fetchRates and pass it the currency the user is converting from. We will save the result to our ratesByBase object.

async function convert(amount, from, to) {
  // first check if we even have the rates to convert from that currency
  if (!ratesByBase[from]) {
    console.log(`Oh no! we don't have ${from} to convert it ${to}, so let's go get it!`);
    const rates = await fetchRates(from);
    console.log(rates);
    // store them for next time
    ratesByBase[from] = rates;
  }
}

Let's demo step by step how that works.

Start by logging ratesByBase in the console, which you will see is an empty object.

browser console showing empty ratesByBase object

If we run convert(100, 'CAD', 'USD') in the console, we will see the message stating we don't have it but are fetching it.

Now if we check the ratesByBase object, we will see those rate values within the object. If you were to run the same convert function again, you will see it does not refetch them because the values already exist within ratesByBase.

browser console showing output from ratesByBase

As soon as you refresh the page, they will be gone. You could use local storage to save those but we will just do page load for this exercise.

Next, we need to convert the amount the user passed in.

function convert(amount, from, to) {
  // first check if we even have the rates to convert from that currency
  if (!ratesByBase[from]) {
    console.log(`Oh no! we don't have ${from} to convert it ${to}, so let's go get it!`);
    const rates = await fetchRates(from);
    console.log(rates);
    // store them for next time
    ratesByBase[from] = rates;
  }

  const rate = ratesByBase[from].rates[to];
}

That may look weird so let's explain what is going on in this code.

const rate = ratesByBase[from].rates[to];

If we look at our CAD property on ratesByBase, that is going to be our "from" .

Inside of that, we need to grab the rates and the find the rate that they are converting to. You could do that like so: ratesByBase.CAD.rates.USD.

But because those property keys are variables within our function, we need to use square brackets to access those object properties.

browser output showing output from ratesByBase.CAD.rates

Now we will take that rate and calculate the convertedAmount like so 👇

function convert(amount, from, to) {
  // first check if we even have the rates to convert from that currency
  if (!ratesByBase[from]) {
    console.log(`Oh no! we don't have ${from} to convert it ${to}, so let's go get it!`);
    const rates = await fetchRates(from);
    console.log(rates);
    // store them for next time
    ratesByBase[from] = rates;
  }

  const rate = ratesByBase[from].rates[to];
  const convertedAmount = rate * amount;
  console.log(`${amount} ${from} is ${convertedAmount} in ${to}`);
  return convertedAmount;
}

If you refresh the page and try that, you should see something like the following..

browser console showing successful conversion from CAD to USD

At this point, we have built the main functionality of the app so at this point we can start hooking it up to our UI.

Hooking Up The UI

There are three inputs that we need to listen on:

  • the amount,
  • 2 currency select inputs

Input Event on a Form

There is a trick we can use here, which is listening for an input event on the form. That one event will cover all of them, which is pretty cool.

Let's go up to the top of our script file and select the form.

const form = document.querySelector('.app form');

At the very bottom of the script tag, add an event listener which will listen for the input event and handle it in a function we will define called handleInput, like so 👇

form.addEventListener('input', handleInput);

Let's define that handleInput function, and for now just log e.target and e.currentTarget to see what we are working with.

function handleInput(e) {
  console.log(e.target);
  console.log(e.currentTarget);
}
browser console output showing results from event

If you refresh the page and try typing into the amount input or selecting an item from the drop downs, you will see that e.target is changing every time but e.currentTarget which is the form stays the same.

That is because we are listening to the event on the form, but the actual event happens on the input or select box.

Those events bubble up to the form where we handle them in our handleInputs.

So remember that trick, you can listen on the "input" event on a form and that will cover all of your inputs that are inside of that form.

Wiring up Handlers

Let's start wiring up the handler to call our functions.

We forgot to grab our "amount" input, so let's go ahead to the top of the page and add that.

const fromSelect = document.querySelector('[name="from_currency"]');
const toSelect = document.querySelector('[name="to_currency"]');
const fromInput = document.querySelector('[name="from_amount"]');

Now let's make handleInput an async function so we can call convert. We can pass the variables easily using the value of the three inputs like so 👇

async function handleInput(e) {
  const rawAmount = await convert(
    fromInput.value,
    fromSelect.value,
    toSelect.value
  );

  console.log(rawAmount);
}
browser console showing raw amount values of the rates data

If you refresh the page and try entering an amount into the "from_amount" input, you should see something like shown above 👆 logged in the console.

Now we need to actually show that amount in the to_amount paragraph tag.

rendered page with the amount being highlighted by dev tools

Let's go back to the top of our page and select that element and name it toEl.

const fromSelect = document.querySelector('[name="from_currency"]');
const toSelect = document.querySelector('[name="to_currency"]');
const fromInput = document.querySelector('[name="from_amount"]');
const toEl = document.querySelector('.to_amount');

Now within handleInput, let's update the element to show the converted amount. 👇

async function handleInput(e) {
  const rawAmount = await convert(
    fromInput.value,
    fromSelect.value,
    toSelect.value
  );

  toEl.textContent = rawAmount;
}

If you refresh the page and test it out, you should see the to_amount value updating.

Try changing the "to currency" and the "from currency" to ensure the value is still updating.

rendered page showing returned values of the api

Formatting Currency using Number Format API

The problem we have now is that it is not formatting it accordingly to what the locale is.

Money is New Zealand Dollars (NZD), or money in Indian Rupee (INR), may be formatted differently. Some currencies support cents, some do not.

We will use this really cool API called Number Format, and it knows how to handle currency formatting, which is really cool.

Make a function formatCurrency which will take in the amount and the currency and return a formatted number.

We will be using the Intl.NumberFormat() method which takes in an argument of the language of the reader Intl.NumberFormat("en-us"), or if you leave it blank it will detect the language based no the browser, which is almost always what you want.

We will leave it blank.

And as the second argument you pass it an options object.

Within that object we will pass a value for the style property which will be 'currency', since we want to format the amount as a currency, and then we need to pass the currency argument which we can do using the shorthand since both the property and value have identical names.

code snippet of formatCurrency

The code highlighted above is the "formatter", and then the method is just a .format() method on the formatted to which you pass your amount like so:

function formatCurrency(amount, currency) {
  return Intl.NumberFormat({
    style: 'currency',
    currency
  }).format(amount);
}

So now instead of just setting the toEl value to rawAmount we will modify it like so:

async function handleInput(e) {
  const rawAmount = await convert(
    fromInput.value,
    fromSelect.value,
    toSelect.value
  );

  toEl.textContent = formatCurrency(rawAmount, toSelect.value);
}

Refresh the page and check whether that works.

rendered page showing form

Unfortunately that does not seem to be working. Let's look at the console to debug.

When Wes runs into a problem like this, when there are many things calling each other, he just works on them one by one.

Lets start by testing formatCurrency in the console.

browser console showing the result from formatCurrency

We do not get the formatted amount back. Try manually passing the locale to Intl.NumberFormat method.

Modify the code like below to include it.

function formatCurrency(amount, currency) {
  return Intl.NumberFormat("en-US", {
    style: 'currency',
    currency
  }).format(amount);
}

Now if you refresh it, it should be working.

rendered page showing form fully finished

Find an issue with this post? Think you could clarify, update or add something?

All my posts are available to edit on Github. Any fix, little or small, is appreciated!

Edit on Github

Syntax Podcast

Hold on — I'm grabbin' the last one.

Listen Now →
Syntax Podcast

@wesbos Instant Grams

Beginner JavaScript

Beginner JavaScript

A fun, exercise heavy approach to learning Modern JavaScript from scratch. This is a course for absolute beginners or anyone looking to brush up on their fundamentals. Start here if you are new to JS or programming in general!

BeginnerJavaScript.com
I post videos on and code on

Wes Bos © 1999 — 2024

Terms × Privacy Policy