FCC Zipline Series 104: Build a JavaScript Calculator | The JS Way

Today, we are going to be building a calculator app using vanilla JavaScript . No jQuery or other third party libraries/frameworks involved. I’m even going to make it more complicated by using objects and prototypical inheritance, so do not expect a quick and dirty job. Here’s a demo of what we’ll be building.

We’ll build a Calculator “class”, which will take care of the application logic. This Calculator, will have methods that we can access to pass it numbers, operations and all sorts of cool stuff that’s it.

Let’s get the layout out of the way first, this is the most subjective part of the project, so I’m just going to throw in the HTML and CSS, you can go ahead and use your own styles or modify these:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JS Calculator</title>
    <link rel="stylesheet" href="styles.css"/>
</head>
<body>

 

AC

 

CE

 

%

 

÷

</div>

7

 

8

 

9

 

×

</div>

4

 

5

 

6

 

</div>

1

 

2

 

3

 

+

</div>

.

 

0

 

 

=

</div> </div> </div> </div> http://app.js </body> </html>

 

The HTML character codes (e.g.: ×) in the markup are just a few nicer symbols for the +, *, -, etc. symbols.

Here’s the CSS, I’ve gone the flexbox way, if you’re using an older version of Internet Explorer, you’re out of luck.

@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400);

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    background-color: #121212;
    font-family: 'Source Sans Pro', Helvetica, Arial, sans-serif;
    font-size: 16px;
}

.main-wrapper {
    display: flex;
    padding: 0;
    justify-content: center;
}

.calculator {
    width: 100%;
}

.key-row {
    width: 100%;
    display: flex;
    justify-content: space-between;    
}

.calculator-button {
    justify-content: center;
    align-items: center;
    display: flex;
    min-height: 50px;
    line-height: 1rem;
    height: 15vh;
    font-size: 1.5rem;
    font-weight: 300;
    cursor: pointer;
    padding: 15px 0;
    min-width: 25%;
    background-color: #e0e0e0;
    border-right: 1px solid #717171;
    border-bottom: 1px solid #717171;
    text-align: center;
    
    transition: .1s ease-in;
}

.operator {
    font-size: 2.6rem;
}

.operator.small {
    font-size: 1.5rem;
}

.calculator-button:hover {
    background-color: #f5f5f5;
}

.calculator-button:first-child {
    border-left: none;
}

.calculator-button:last-child {
    border-right: none;
}

.calculator-button.orange {
    background-color: #ff8d07;
    color: #fff;
}

.calculator-button.orange:hover {
    background-color: #ff971e;
}

.last-row .calculator-button {
    border-bottom: none;
}

.fill-button {
    cursor: default;
}

.fill-button:hover {
    background-color: #e0e0e0;
}

#display {
    font-size: 1.3rem;
    padding: 5px 10px;
    text-align: right;
    background-color: #222;
    color: #fff;
    height: 25vh;
    width: 100%;
}

 

The calculator

We are going to create a calculator “constructor” first, it’ll take in just a string, the ID of the element where we want to display the results. We’re also going to create a property to keep track of all user input, let’s initialize it as an empty array:

function Calculator(displayId) {
  this.displayId = displayId;
  this.operationArray = [];
}

 

Let’s start giving this bad boy a few methods. First, I’ll give it a method that will update the display element with whatever is in the operation array:

function Calculator(displayId) {
  this.displayId = displayId;
  this.operationArray = [];
}

Calculator.prototype.updateDisplay = function() {
  document.getElementById(this.displayId).innerText = this.operationArray.join(' ');
}

 

Now, let’s think about what kinds of different orders can a calculator receive? These come to mind:

  • A number: We hit a number and it get’s appended at the end of the last value or a new number if it comes after an operator.
  • An operator: An operator (+, -, *, /, %) is hit and it creates a separation between the preceding and the number that will follow.
  • AC: It clears everything we’ve done up to now.
  • CE: Removed the latest entry, similar to the backspace key in a computer.
  • Answer (‘=‘): Prints the solution to the input in the display.

Let’s not talk too much and write a method for each of these types of input: handleNumber, handleOperator, allClear, clearEntry and =.  Here’s the first:

// app.js continued...

Calculator.prototype.handleNumber = function(number) {
  if (isNaN(this.operationArray[this.operationArray.length - 1])) {
    this.operationArray.push(number.toString());
  } else {
    this.operationArray[this.operationArray.length - 1] += number.toString();
  }
  this.updateDisplay();
}

 

Here we are first checking if the last item in the array is not a number (using isNaN). If it isn’t, it means the last item is an operator, and so, we push the number to the array. If the last item is a number though, we concatenate it to the last value rather than pushing it. Then, we update the display.

Checking the last item and concatenating something to it seem to be tasks that we may have to repeat a few times; let’s abstract them into their own methods (we could use getters/setters here, but we’ll talk about those in the future):

Calculator.prototype.addToLast = function(input) {
  this.operationArray[this.operationArray.length - 1] += input;
};

Calculator.prototype.getLastItem = function() {
  return this.operationArray[this.operationArray.length - 1];
};

 

Once those are set up, let’s change handleNumber to use them:

Calculator.prototype.handleNumber = function(number) {
  if (isNaN(this.getLastItem())) {
    this.operationArray.push(number.toString());
  } else {
    this.addToLast(number.toString());
  }
  this.updateDisplay()
};

 

That’s easier on the eyes. Notice that I’m turning number into strings, this way, we can concatenate numbers and not have them add when we don’t want them to.

Onto handleOperator:

Calculator.prototype.handleOperator = function(operator) {
  if (operator === '.') {
    this.addToLast(operator);
  } else {
    this.operationArray.push(operator);
  }
  this.updateDisplay();
};

 

Simple enough, if the input is the dot (‘.’) we add it at the end of the last element in the operation array, else, we push the operator to the array. There is actually a small problem with this method. We should make sure that when clicking on an operator such as +, the previous element in array is a number, since two operators one after the other don’t make sense. Here’s a revised version of handleOperator:

Calculator.prototype.handleOperator = function(operator) {
  if (!isNaN(this.getLastItem())) {
    if (operator === '.') {
        this.addToLast(operator);
    } else {
        this.operationArray.push(operator);
    }
    this.updateDisplay();
  }
};

 

We only have the calculator functions left, lets write allClear:

Calculator.prototype.allClear = function(funcName) {
    this.operationArray = [];
    this.updateDisplay();
};

 

Here is clearEntry:

Calculator.prototype.clearEntry = function() {
        this.operationArray[this.operationArray.length - 1] = this.getLastItem().toString().slice(0, -1);
    if (this.getLastItem().length < 1) {
        this.operationArray.pop();
    }
    this.updateDisplay();
};

 

And finally, getTotal, a method thank makes use of the much controversial method: eval(). You can read up on eval here. It’s a very powerful (but dangerous) tool, that takes a string and executes it as JavaScript. It’s particularly useful for our calculator, this is eval in action:

var myString = '5 + 4 / 2';

eval(myString); //-> 7

 

It useful for us since it executes mathematical operations with preferences in mind, without us having to worry about coding all that behaviour. With this knowledge, this is what getTotal() looks like:

Calculator.prototype.getTotal = function() {
  if (isNaN(this.getLastItem())) {
      this.operationArray.pop();
  }
  var total = eval(this.operationArray.join(''));
  this.operationArray = [total];
  this.updateDisplay();
};

 

We first remove the last element in the array it it isn’t a number, then, we get evaluate the operationArray (after turning it into a string), set operationArray to this value (so we can keep doing more operations afterwards) and update the display.

Now, we just need to initialize an instance of our calculator and pass it the ID of the display element (#display) in this case:

var myCalculator = new Calculator('display');

 

We have working logic for our calculator now, but the buttons in the page itself do nothing. Let’s bind them all to actual actions. We’ll bind the functions first:

document.getElementById('ac').addEventListener('click', function() {
  myCalculator.allClear();
});

document.getElementById('ce').addEventListener('click', function() {
  myCalculator.clearEntry();
});

document.getElementById('=').addEventListener('click', function() {
  myCalculator.getTotal();
});

 

For the numbers and operators, we’ll automate the iteration a little:

var operatorControls = document.getElementsByClassName('operator'),
    numberControls = document.getElementsByClassName('number');

for (var i = 0; i < operatorControls.length; i++) {
  operatorControls[i].addEventListener('click', function() {
    myCalculator.handleOperator(this.getAttribute('id'));
  });
}

for (i = 0; i < numberControls.length; i++) {
  numberControls[i].addEventListener('click', function() {
    myCalculator.handleNumber(this.getAttribute('id'));
  });
}

 

Now, all together and wrapped inside an Immediately-Invoked Function Expression or IIFE:

(function() {
  function Calculator(displayId) {
      this.displayId = displayId;
      this.operationArray = [];
  }

  Calculator.prototype.updateDisplay = function() {
      document.getElementById(this.displayId).innerText = this.operationArray.join(' ');
  };

  Calculator.prototype.addToLast = function(input) {
      this.operationArray[this.operationArray.length - 1] += input;
  };

  Calculator.prototype.getLastItem = function() {
      return this.operationArray[this.operationArray.length - 1];
  };

  Calculator.prototype.handleNumber = function(number) {
      if (isNaN(this.getLastItem())) {
          this.operationArray.push(number.toString());
      } else {
          this.addToLast(number.toString());
      }
      this.updateDisplay()
  };

  Calculator.prototype.handleOperator = function(operator) {
      if (!isNaN(this.getLastItem())) {
          if (operator === '.') {
              this.addToLast(operator);
          } else {
              this.operationArray.push(operator);
          }
          this.updateDisplay();
      }
  };

  Calculator.prototype.allClear = function(funcName) {
      this.operationArray = [];
      this.updateDisplay();
  };

  Calculator.prototype.clearEntry = function() {
      this.operationArray[this.operationArray.length - 1] = this.getLastItem().toString().slice(0, -1);
      if (this.getLastItem().length < 1) {
          this.operationArray.pop();
      }
      this.updateDisplay();
  };

  Calculator.prototype.getTotal = function() {
      if (isNaN(this.getLastItem())) {
          this.operationArray.pop();
      }
      var total = eval(this.operationArray.join(''));
      this.operationArray = [total];
      this.updateDisplay();
  };

  var myCalculator = new Calculator('display');

  document.getElementById('ac').addEventListener('click', function() {
      myCalculator.allClear();
  });

  document.getElementById('ce').addEventListener('click', function() {
      myCalculator.clearEntry();
  });

  document.getElementById('=').addEventListener('click', function() {
      myCalculator.getTotal();
  });

  var operatorControls = document.getElementsByClassName('operator'),
      numberControls = document.getElementsByClassName('number');

  for (var i = 0; i < operatorControls.length; i++) {
      operatorControls[i].addEventListener('click', function() {
          myCalculator.handleOperator(this.getAttribute('id'));
      });
  }

  for (i = 0; i < numberControls.length; i++) {
      numberControls[i].addEventListener('click', function() {
          myCalculator.handleNumber(this.getAttribute('id'));
      });
  }
})();

 

Keep in mind that this calculator suffers from the JavaScript floating point error. If you don’t know about it, try launching the calculator and doing 0.1 + 0.2, have fun with that! 😉

Our calculator is ready! Give it a spin and make it your own! Shoot any questions via email, twitter or post a comment below.

 


 

Extra credit. We are fancy people.

Why not get fancy and make this thing respond to keyboard events for numbers, operators and functions?

Here’s how we do it:

window.onkeyup = function(e) {
  e.preventDefault();
  var key = e.keyCode ? e.keyCode : e.which;

  if (key >= 96 && key <= 105) {
    myCalculator.handleNumber(key - 96)
  } else if (key === 107) {
    myCalculator.handleOperator('+');
  } else if (key === 109) {
    myCalculator.handleOperator('-');
  } else if (key === 53) {
    myCalculator.handleOperator('%');
  } else if (key === 106) {
    myCalculator.handleOperator('*');
  } else if (key === 111) {
    myCalculator.handleOperator('/');
  } else if (key === 110) {
    myCalculator.handleOperator('.');
  } else if (key === 8) {
    myCalculator.clearEntry();
  } else if (key === 46) {
    myCalculator.allClear();
  } else if (key === 13) {
    myCalculator.getTotal();
  }
}

 

This will bind the keypad numbers and operator keys, as well as enter, backspace and delete keys on the keyboard to call on the proper Calculator methods! Place this chunk of code below the click event listeners and you’re done!