🛠️ Calculator: Happy Path

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ Calculator: Happy Path

Five things can happen when a person gets hold of a calculator:

  1. They click a number key
  2. They click an operator key
  3. They click the decimal key
  4. They click the equal key
  5. They click the clear key

We need to consider what happens when a user presses each key. There are many permutations, and this makes the calculator complicated.

We’ll work through it step by step.

First, let’s use the event delegation pattern to listen to every key.

const calculator = document.querySelector('.calculator')
const calculatorButtonsDiv = calculator.querySelector('.calculator__keys')

calculatorButtonsDiv.addEventListener('click', event => {
  if (!event.target.closest('button')) return
})

Listening for keys

There are five kinds of keys. We can identify them with the data-button-type custom attribute.

calculatorButtonsDiv.addEventListener('click', event => {
  const button = event.target
  const { buttonType } = button.dataset

  if (buttonType === 'number') {
    console.log('Pressed number')
  }

  if (buttonType === 'decimal') {
    console.log('Pressed decimal')
  }

  if (buttonType === 'operator') {
    console.log('Pressed operator')
  }

  if (buttonType === 'equal') {
    console.log('Pressed equal')
  }

  if (buttonType === 'clear') {
    console.log('Pressed clear')
  }
})
Actions logged into the console.

Building the happy path

When a user picks up the calculator, they can click any of these five types of keys:

  1. A number key
  2. An operator key
  3. The decimal key
  4. The equal key
  5. The clear key

It can be overwhelming to consider five types of keys at once. Let’s take it step by step and consider what a normal person would do when they pick up a calculator. This “what a normal person would do” is called the happy path.

Let’s call our normal person Mary.

When Mary picks up a calculator, she’s likely to click a number key.

If Mary clicks a number key

If the calculator shows 0, we should replace it with the number that was clicked.

To do this, we need to find the value of the number that was clicked. This value can be found in the key attribute.

calculatorButtonsDiv.addEventListener('click', event => {
  const button = event.target
  const { buttonType, key } = button.dataset
  // ...
})

Next, we need to find the current displayed result. We can get the result from .calculator__display.

calculatorButtonsDiv.addEventListener('click', event => {
  const button = event.target
  const { buttonType, key } = button.dataset
  const result = display.textContent
  // ...
})

When the result shows 0, we replace it with the number that was clicked.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...

  if (buttonType === 'number') {
    if (result === '0') {
      display.textContent = key
    }
  }

  // ...
})
Clicked number 9. Displayed result changed from 0 to 9.

If the calculator shows a non-zero number, we want to append the number to the displayed result.

For example, if the person clicks 9, then 8. We want to show 98.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...

  if (buttonType === 'number') {
    if (result === '0') {
      display.textContent = key
    } else {
      display.textContent = result + key
    }
  }

  // ...
})
Clicked numbers 9 then 8. Calculator display shows 98.

Next, our normal person, Mary can click either:

  1. A decimal
  2. An operator key

There’s only one decimal key, and there are four operator keys. We will first consider what happens when Mary clicks the decimal key (because it’s less overwhelming).

If Mary clicks the decimal key

A decimal should appear on the display if Mary clicks the decimal key.

  1. If result is 0, we show 0.
  2. If result is 9, we show 9.

Creating this is simple, we will add . to the result.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...

  if (buttonType === 'decimal') {
    display.textContent = result + '.'
  }

  // ...
})
Clicked 9, 8, and dot. Calculator shows 98..

At this point, Mary can click either of these keys:

  1. A number key
  2. An operator key

We can work on the number key since we’ve already started work on it.

If Mary clicks another number after the decimal key

If there’s a decimal in the results, we always append a number to the results. Let’s say Mary clicks 7 this time.

  1. If result is 98., we show 98.7
  2. If result is 0., we show 0.7

We don’t have to write any code for this to happen.

Clicked 9, 8, dot, 7. Results show 98.7.

If Mary clicks an operator key

Operator keys are plus, minus, times, and divide keys.

When they’re clicked, we want to highlight the operator key so Mary knows the operator is active. We can do this by adding an is-pressed class to the operator key.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...

  if (buttonType === 'operator') {
    button.classList.add('is-pressed')
  }

  // ...
})
Click on plus operator key, then on displayed number. Plus remains highlighted even though we clicked elsewhere.

At this point, Mary can click another number key.

If Mary clicks a number key after an operator key

Regardless of what the displayed number is, we need to reset the display to the new number. At the same time, we want to release the operator key from its pressed state.

To release the pressed state, we remove is-pressed from each operator key.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...

  // Release operator pressed state
  const operatorKeys = [...calculatorButtonsDiv.children]
    .filter(button => button.dataset.buttonType === 'operator')
  operatorKeys.forEach(button => button.classList.remove('is-pressed'))

  // All the if statements...
})
Clicked 6 after plus. Pressed state on plus gets released.

To reset the number back to zero, we need to know the previous button was an operator. One way to do this is through a custom attribute.

Let’s call this custom attribute data-previous-action.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...

  calculator.dataset.previousButtonType = buttonType
})

If the previous action is an operator, we want to show the clicked number.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  const { previousButtonType } = calculator.dataset

  if (buttonType === 'number') {
    if (result === '0') {
      display.textContent = key
    } else {
      display.textContent = result + key
    }

    if (previousButtonType === 'operator') {
      display.textContent = key
    }
  }

  // ...
})
Clicked 6 after operator. Calculator display resets and shows 6.

We’re almost done with the happy path.

Let’s say Mary is satisfied with her numbers and operators. She wants to calculate what she has clicked. This time, she clicks the equal key.

When Mary clicks the equal key

The calculator should calculate a result that depends on three values:

  1. The first value (before we clicked the operator)
  2. The operator
  3. The second value (the one that’s currently displayed)

To get the first value, we need to save results before we replace it with the second number. We can do this with a custom attribute called data-first-value.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'operator') {
    button.classList.add('is-pressed')
    calculator.dataset.firstValue = result
  }

  // ...
})

We also need to save the operator key at the same time.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...
  if (buttonType === 'operator') {
    button.classList.add('is-pressed')
    calculator.dataset.firstValue = result
    calculator.dataset.operator = button.dataset.key
  }

  // ...
})

We have all three values we need. Let’s log them into the console to confirm.

calculatorButtonsDiv.addEventListener('click', event => {

  // ...
  if (buttonType === 'equal') {
    const firstValue = calculator.dataset.firstValue
    const operator = calculator.dataset.operator
    const secondValue = result

    console.log(firstValue)
    console.log(operator)
    console.log(secondValue)
  }

  // ...
})

Confirmed we have the three values we need.

Now, we can perform a calculation. After calculating, we will show the new result on the display.

calculatorButtonsDiv.addEventListener('click', event => {

  // ...
  if (buttonType === 'equal') {
    const firstValue = calculator.dataset.firstValue
    const operator = calculator.dataset.operator
    const secondValue = result

    let newResult
    if (operator === 'plus') newResult = firstValue + secondValue
    if (operator === 'minus') newResult = firstValue - secondValue
    if (operator === 'times') newResult = firstValue * secondValue
    if (operator === 'divide') newResult = firstValue / secondValue

    display.textContent = newResult
  }

  // ...
})
Made a calculation, but we got a wrong result.

But we have a problem. 98.7 + 6 should not be equal to 98.76. It should be 104.7.

This happened because values stored in custom attributes are stings. The value from .calculator__display's textContent is also a string. When you add two strings together, you concatenate the strings. Which is why you get 98.76.

To fix this, we need to change firstValue and secondValue to numbers. We can do this with parseInt or parseFloat.

parseInt changes a string into an integer. parseFloat changes a string into a number with decimals. We need parseFloat in this case.

calculatorButtonsDiv.addEventListener('click', event => {

  // ...
  if (buttonType === 'equal') {
    const firstValue = parseFloat(calculator.dataset.firstValue)
    const operator = calculator.dataset.operator
    const secondValue = parseFloat(result)

    let newResult
    if (operator === 'plus') newResult = firstValue + secondValue
    if (operator === 'minus') newResult = firstValue - secondValue
    if (operator === 'times') newResult = firstValue * secondValue
    if (operator === 'divide') newResult = firstValue / secondValue

    display.textContent = newResult
  }

  // ...
})
Made a calculation and got the correct result.

One final thing before we wrap up.

If Mary clicks the clear key

The clear key works like this:

  1. If you click clear when it says AC, you clear all data saved by the calculator
  2. If you click clear when it says CE, you set the current displayed number to zero.

When you click any button (other than clear), AC should change to CE.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...

  if (buttonType !== 'clear') {
    const clearButton = calculator.querySelector('[data-button-type=clear]')
    clearButton.textContent = 'CE'
  }

  // ...
})
Clicks 3. Clear key changes from AC to CE..

If Mary presses CE, we want to reset the current displayed number to zero.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...

  if (buttonType === 'clear') {
    display.textContent = '0'
    button.textContent = 'AC'
  }

  // ...
})
Clicks 9, then CE. Display changes back to 0. CE changes back to AC.

If Mary presses AC, we want to remove any values we saved on the calculator.

calculatorButtonsDiv.addEventListener('click', event => {
  // ...

  if (buttonType === 'clear') {
    if (button.textContent === 'AC') {
      delete calculator.dataset.firstValue
      delete calculator.dataset.operator
    }

    display.textContent = '0'
    button.textContent = 'AC'
  }

  // ...
})

That’s it!

Next up, we will veer away from happy paths into reality (where users can press a weird combinations of keys and break your app).