How to build a Calculator App with JavaScript

You've learnt the basics of JavaScript and you're looking to practice your skills by building something. This tutorial provides a good exercise for you by describing how to build a calculator app with JavaScript.

This tutorial will move at a faster pace than the previous ones as I’m assuming you already know the basics of JavaScript. If you have trouble following through, try taking the Random Quote Machine and Wikipedia Search App tutorials in that order, then come back to this one later.

The calculator layout was crafted with CSS Grid. If you’d like to learn how this was achieved, make sure to check out the previous tutorial. This article focuses only on the JavaScript logic employed to make the calculator work.

Here’s a live demo of the completed project.

Before you begin

You can find the starting point for this tutorial on JSFiddle. It contains all the necessary markup and styles used to build the calculator layout. The markup is almost identical to the final state of the previous tutorial, but I made a few minor changes so make sure to go with this one instead.

Start by forking the code to a new fiddle, and follow along by typing each step out all the way to the end. Feel free to do this tutorial on another online coding playground or on your local machine if you prefer.

Getting started

Anyone should be able to perform the four most common arithmetic operations (addition, subtraction, multiplication and division) on our calculator by constructing a valid expression using the input buttons, and have the result displayed on the screen. An example is shown below:

12 + 10

To construct a valid arithmetic expression, we need to keep track of a few things: the first operand (12), the operator (+) and the second operand (10).

Let’s start by creating an object to help us keep track of these values. Type this at the top of the JavaScript pane in JSFiddle:

const calculator = {
  displayValue: '0',
  firstOperand: null,
  waitingForSecondOperand: false,
  operator: null,
};

The calculator object consists of everything that we need to construct a valid expression:

  • displayValue holds a string value that represents the input of the user or the result of an operation. This is how we keep track of what will be displayed on the screen.
  • firstOperand will store the first operand for any expression. It’s set to null for now.
  • The operator key will store the operator for an expression. Its initial value is also null.
  • waitingForSecondOperand is essentially a flag that checks if the first operand and operator have been inputted. If it’s true, the next numbers that the user enters will constitue the second operand.

Update the display

Right now, the calculator screen shows nothing. We need the displayValue to be shown on the screen (‘0’ by default). We will create a function for this purpose so that anytime an operation is performed in the app, we can always invoke it to update the screen with the contents of displayValue.

Go ahead and type this below the calculator object:

function updateDisplay() {
  const display = document.querySelector('.calculator-screen');
  display.value = calculator.displayValue;
}

updateDisplay();

If you look into your HTML file, you’ll see that the “screen” is really just a disabled text input:

<input type="text" class="calculator-screen" value="" disabled />

We can’t type into it directly with the keyboard, but we can definitely change its value with JavaScript. And that’s what the updateDisplay function does. Now you should see zero displayed on the screen of the calculator.

View the checkpoint for this step on JSFiddle.

Handle key presses

We have four sets of keys on the calculator: digits (0-9), operators (+, -, *, /, =), a decimal point (.) and a reset key (AC). In this section, we’ll listen for clicks on the calculator keys and determine what type of key was clicked.

Add this code snippet at the bottom of your JavaScript file:

const keys = document.querySelector('.calculator-keys');
keys.addEventListener('click', (event) => {
  const { target } = event;
  if (!target.matches('button')) {
    return;
  }

  if (target.classList.contains('operator')) {
    console.log('operator', target.value);
    return;
  }

  if (target.classList.contains('decimal')) {
    console.log('decimal', target.value);
    return;
  }

  if (target.classList.contains('all-clear')) {
    console.log('clear', target.value);
    return;
  }

  console.log('digit', target.value);
});

In the above snippet, we’re listening for a click event on .calculator-keys. Since all the keys on the calculator are children of this element, the click event filters down to them too. This is known as event delegation.

Inside the callback function of the event listener, we extract the target property of the click event using object destructuring which makes it really easy to unpack object properties into distinct variables.

const { target } = event;
// is equivalent to
const target = event.target;

The target variable is an object that represents the element that was clicked. If this element is not a button (such as if you click the spaces between the buttons), we will exit the function by returning early. That’s what the first if statement does:

//If the element that was clicked is not a button...
if (!target.matches('button')) {
  //exit the function
  return;
}

Otherwise, we log the type of button that was clicked as well as its value. Try it out. Open up your browser console and click any of the buttons. The appropirate key type and value should be logged to the console.

The correct type of key is detected and logged to the console
The correct type of key is detected and logged to the console

View the checkpoint for this step on JSFiddle.

Input the digits

In this step, we’ll make the digit buttons work so that when they are clicked, feedback is displayed to the user on the screen.

Feedback is displayed to the user when a digit key is clicked

Since the displayValue property of the calculator object represents the input of the user, we need to modify the value of this property when any of the digits is clicked. Create a new function called inputDigit below the calculator object:

function inputDigit(digit) {
  const { displayValue } = calculator;
  // Overwrite `displayValue` if the current value is '0' otherwise append to it
  calculator.displayValue = displayValue === '0' ? digit : displayValue + digit;
}

Next, replace console.log('digit', target.value); with the following:

inputDigit(target.value);
updateDisplay();

In the inputDigit function, we use the ternary operator (?) to check if the current value displayed on the calculator is zero. If so, we overwrite calculator.displayValue with whatever digit was clicked. Otherwise, if displayValue is a non-zero number, we append the digit to it.

Finally we call updateDisplay() to update the information on the screen after each button is clicked. Try it out by clicking any of the digit buttons. The display should be updated with whatever digit you clicked.

View the checkpoint for this step on JSFiddle.

Input a decimal point

When the decimal point key is clicked, we need to append a decimal point to whatever is displayed on the screen except if it already contains a decimal point.

Calculator showing decimal point being inputted

Here’s how we can achieve that. Create a new function called inputDecimal below inputDigit:

function inputDecimal(dot) {
  // If the `displayValue` does not contain a decimal point
  if (!calculator.displayValue.includes(dot)) {
    // Append the decimal point
    calculator.displayValue += dot;
  }
}

Then replace console.log('decimal', target.value); in the keys’ event listener callack function with the following code:

inputDecimal(target.value);
updateDisplay();

Inside the inputDecimal function, we use the includes method to check if displayValue does not already contain a decimal point. Then we append the dot to the number. Otherwise, we do nothing.

View the checkpoint for this step on JSFiddle.

Handling operators

The next step is to get the operators (+, -, x, /, =) on the calculator working. There are three scenarios to account for:

1. When the user finishes entering the first operand and hits an operator

This indicates that the user is now ready to enter the second operand. What we need to do here is store the first operand and update the display with the new string of numbers.

Create a new function called handleOperator below inputDecimal:

function handleOperator(nextOperator) {
  const { firstOperand, displayValue, operator } = calculator
  const inputValue = parseFloat(displayValue);

  if (firstOperand === null) {
    calculator.firstOperand = inputValue;
  }

  calculator.waitingForSecondOperand = true;
  calculator.operator = nextOperator;
}

Then replace console.log('operator', target.value) in the key’s event listener callback function with the following code:

handleOperator(target.value);
updateDisplay();

When an operator key is pressed, we convert the current number displayed on the screen to a number (parseFloat(displayValue)) and then store the result in calculator.firstOperand if it does not exist already.

The operator property is set to whatever operator key was clicked and waitingForSecondOperand is set to true which indicates that the first operand has been entered and the second one is ready to begin.

At this point, it is useful to see how the properties of the calculator object is being updated on each button press. Add the following snippet to the end of both inputDigit and handleOperator.

console.log(calculator);

Now, try to construct a valid arithmetic operation by clicking the following keys in turn: 12 + 10. Notice that when the + key is pressed, the values of firstOperand and operator is updated to 12 and + respectively while waitingForSecondOperand is set to true indicating that the calculator is waiting for the second operand (10) to be inputted.

However, there’s a bug here. The second operand does not overwrite the first operand which is currently displayed on the screen. Instead, it is appended to it.

Calculator showing how second operand is appended to the first with the first

Let’s fix that by updating the inputDigit function to look like this:

function inputDigit(digit) {
  const { displayValue, waitingForSecondOperand } = calculator;

  if (waitingForSecondOperand === true) {
    calculator.displayValue = digit;
    calculator.waitingForSecondOperand = false;
  } else {
    calculator.displayValue = displayValue === '0' ? digit : displayValue + digit;
  }

  console.log(calculator);
}

Here, we check if waitingForSecondOperand is true and set displayValue to the key that was clicked. Otherwise, we perform the same check as before, overwriting or appending to calculator.displayValue as appropriate.

Here’s the checkpoint for this step on JSFiddle. If you try out the expression as before, it should update displayValue properly.

2. When the user finishes the second operand and hits an operator

The second scenario we want to handle is if the user has finished entering the second operand and an operator key is clicked. At this point, all the ingredients to perform a valid calculation is present so we need to display the result of the calculation to the user.

After 12 + 10, let’s say the user hits the = button. What should happen? Well, 22 should be presented on the screen as the result of the calculation and the firstOperand property should be updated to the result so that it can be used in the next calculation.

Update the handleOperator function as shown below:

function handleOperator(nextOperator) {
  const { firstOperand, displayValue, operator } = calculator
  const inputValue = parseFloat(displayValue);

  if (firstOperand == null) {
    calculator.firstOperand = inputValue;
  } else if (operator) {
    const result = performCalculation[operator](firstOperand, inputValue);

    calculator.displayValue = String(result);
    calculator.firstOperand = result;
  }

  calculator.waitingForSecondOperand = true;
  calculator.operator = nextOperator;
  console.log(calculator);
}

Then create a new object called performCalculation below handleOperator with the following properties:

const performCalculation = {
  '/': (firstOperand, secondOperand) => firstOperand / secondOperand,

  '*': (firstOperand, secondOperand) => firstOperand * secondOperand,

  '+': (firstOperand, secondOperand) => firstOperand + secondOperand,

  '-': (firstOperand, secondOperand) => firstOperand - secondOperand,

  '=': (firstOperand, secondOperand) => secondOperand
};

The else if block added to handleOperator checks if an operator already exists. If so, property lookup is performed for the operator in the performCalculation object and the function that matches the operator is executed.

This function returns the result of the operation which is then stored in the result variable. The result is subsequently displayed to the user by updating the displayValue property. Also firstOperand is updated to the result so that it may be used in the next operation.

Try it out. Enter 12 + 10 = and notice that the correct result is displayed on the screen. It also works when you chain a string of operations. So 5 * 20 - 14 = should give 86 as the result.

Calculator showing the result of 5 * 20 - 14

This is because hitting the - key triggers the calculation of the first operation (5 * 20) whose result (100) which is then set as the firstOperand for the next calculation so by the time we enter 14 as the second operand and hit the = key, the function that is defined in the - property of performOperation is executed giving 86 as the result which is also set as the firstOperand for the next operation.

Here’s the checkpoint for this step on JSFiddle. Try out other expressions and confirm that the calculator works as expected.

3. When a user enters two or more operators consecutively

It’s quite common to change one’s mind about the type of operation one wants to perform so the calculator must handle this properly.

Let’s say you want to add 7 and 2 together, you will click 7 + 2 = which will produce the correct result. But let’s assume after hitting 7 +, you change your mind and decide to subtract 2 from 7. Instead of clearing the calculator and starting all over, you should be able to hit - to override the + that was previously entered.

Remember that at the point the operator is entered, waitingForSecondOperand will be set to true since the calculator expects a second operand to be entered after the operator key. We can use this quality to update the operator key and prevent any calculations until the second operand has been inputted.

Modify the handleOperator function to look like this:

function handleOperator(nextOperator) {
  const { firstOperand, displayValue, operator } = calculator
  const inputValue = parseFloat(displayValue);

  if (operator && calculator.waitingForSecondOperand)  {
    calculator.operator = nextOperator;
    console.log(calculator);
    return;
  }

  if (firstOperand == null) {
    calculator.firstOperand = inputValue;
  } else if (operator) {
    const currentValue = firstOperand || 0;
    const result = performCalculation[operator](currentValue, inputValue);

    calculator.displayValue = String(result);
    calculator.firstOperand = result;
  }

  calculator.waitingForSecondOperand = true;
  calculator.operator = nextOperator;
  console.log(calculator);
}

This is the relevant change:

if (operator && calculator.waitingForSecondOperand)  {
  calculator.operator = nextOperator;
  console.log(calculator);
  return;
}

The if statement checks if an operator already exists and if waitingForSecondOperand has a truthy value. We then update operator and exit from the function by using an early return statement so that the rest of the function is not executed and no calculations are performed.

Try it out. Click multiple operators after entering some digits and monitor the calculator object in the console. Notice that the operator property is updated each time and no calculations are performed until you provide the second operand.

View the checkpoint for this step on JSFiddle.

Reset the calculator

The final task is to make sure the user can reset the calculator to its initial state by pressing a key. In most calculators, the AC button is used to reset the calculator to its default state, so that’s what we’re going to do here.

Go ahead and create a new function below performCalculation as shown below:

function resetCalculator() {
  calculator.displayValue = '0';
  calculator.firstOperand = null;
  calculator.waitingForSecondOperand = false;
  calculator.operator = null;
  console.log(calculator);
}

Then replace console.log('all clear', target.value) in the keys’ event listener callback function with the following code:

resetCalculator();
updateDisplay();

The resetCalculator function sets all the properties of the calculator object to their original values. Now click the AC key on your calculator. It should work as expected. You can check the calculator object in the console to confirm.

View the checkpoint for this step on JSFiddle.

One more thing…

There’s a bug that allows you to add a decimal point to the displayValue after clicking on an operator.

Calculator gif showing decimal bug

Although this bug does not affect the result of the expression, it’s not ideal for a calculator to behave like that. We can write a fix by making a modification to the inputDecimal function:

function inputDecimal(dot) {
  if (calculator.waitingForSecondOperand === true) return;

  // If the `displayValue` does not contain a decimal point
  if (!calculator.displayValue.includes(dot)) {
    // Append the decimal point
    calculator.displayValue += dot;
  }
}

Here’s the relevant change:

if (calculator.waitingForSecondOperand === true) return;

We’re preventing the decimal point from being appended to the displayValue while waitingForSecondOperand is true by using an early return statement just like we’ve done a few times now. Note how you can execute one statement in an if block without enclosing it in curly braces.

Here’s the final state of the calculator.

Conclusion

This concludes my tutorial. You can enhance this calculator in a lot of ways though. Here are a few ideas that you can implement to improve upon what we’ve covered here:

  • Add the % function.
  • Add the square root and square functions.
  • Make it possible to clear an entry without resetting the calculator.
  • Make it possibe to type in negative numbers (e.g -3).

Take a stab at implementing those features in your application. Thanks for reading, and happy coding!