🛠️ Calculator: Handling other keys

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: Handling other keys

We have three more types of keys to handle:

  1. Decimal
  2. Operator
  3. Equal

Let’s work on Decimal keys first.

Handling Decimal keys

First, we include tests that include Decimal keys into test.calculator.js.

const tests = [
  {
    message: 'Number key',
    keys: ['2'],
    result: '2'
  }, {
    message: 'Number Number',
    keys: ['3', '5'],
    result: '35'
  }, {
    message: 'Number Decimal',
    keys: ['4', 'decimal'],
    result: '4.'
  }, {
    message: 'Number Decimal Number',
    keys: ['4', 'decimal', '5'],
    result: '4.5'
  }
]

Errors should show up since we haven’t handled decimal keys yet.

Tests fail when they include decimal key.

We’ll handle Decimal keys by uncommenting the decimal part in handleClick.

export default function Calculator () {
  // Variables

  const calculator = {
    // Other methods

    handleClick (event) {
      // ...
      switch (buttonType) {
        // ...
        case 'decimal': handleDecimalKey(calculator); break
        // case 'operator': handleOperatorKeys(calculator, button); break
        // case 'equal': handleEqualKey(calculator); break
      }
      // ...
    }
  }

  // Add Event Listeners
  // Return the calculator
}

This is the code we wrote to handle decimal keys previously:

function handleDecimalKey (calculator) {
  const displayValue = getDisplayValue()
  const { previousButtonType } = calculator.dataset

  if (!displayValue.includes('.')) {
    display.textContent = displayValue + '.'
  }

  if (previousButtonType === 'equal') {
    resetCalculator()
    display.textContent = '0.'
  }

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

We will copy this code into Calculator as a method.

export default function Calculator () {
  // Variables

  const calculator = {
    // ...

    handleDecimalKey (calculator) {
      const displayValue = getDisplayValue()
      const { previousButtonType } = calculator.dataset

      if (!displayValue.includes('.')) {
        display.textContent = displayValue + '.'
      }

      if (previousButtonType === 'equal') {
        resetCalculator()
        display.textContent = '0.'
      }

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

    // Other methods
    // Listeners
  }

  // Add Event Listeners
  // Return the calculator
}

We’ll make the changes we did for other methods:

  1. Change calculator to calculatorElement
  2. Omit calculatorElement from parameters
  3. Use calculator.displayValue getters and setters
  4. Change resetCalculator to calculator.resetCalculator
export default function Calculator () {
  // Variables

  const calculator = {
    // ...
    handleDecimalKey () {
      const displayValue = calculator.displayValue
      const { previousButtonType } = calculatorElement.dataset

      if (!displayValue.includes('.')) {
        calculator.displayValue = displayValue + '.'
      }

      if (previousButtonType === 'operator') {
        calculator.displayValue = '0.'
      }

      if (previousButtonType === 'equal') {
        calculator.resetCalculator()
        calculator.displayValue = '0.'
      }
    },

    // Other methods
    // Listeners
  }

  // Add Event Listeners
  // Return the calculator
}

We use handleDecimalKey like this:

export default function Calculator () {
  // Variables

  const calculator = {
    // Other methods

    handleClick (event) {
      // ...
      switch (buttonType) {
        // ...
        case 'decimal': calculator.handleDecimalKeys(); break
        // case 'operator': handleOperatorKeys(calculator, button); break
        // case 'equal': handleEqualKey(calculator); break
      }
      // ...
    }
  }

  // Add Event Listeners
  // Return the calculator
}

Errors related to decimal keys should now be gone.

Errors related to decimal keys are gone.

We’ll handle operator keys next.

Handling Operator Keys

First, we uncomment the part we use to handle operator keys.

export default function Calculator () {
  // Variables

  const calculator = {
    // Other methods

    handleClick (event) {
      // ...
      switch (buttonType) {
        // ...
        case 'operator': handleOperatorKeys(calculator, button); break
        // case 'equal': handleEqualKey(calculator); break
      }
      // ...
    }
  }

  // Add Event Listeners
  // Return the calculator
}

Here’s the code we wrote to handle operator keys:

function handleOperatorKeys (calculator, button) {
  const displayValue = getDisplayValue()
  const { previousButtonType, firstValue, operator } = calculator.dataset
  const secondValue = displayValue

  button.classList.add('is-pressed')

  if (
    previousButtonType !== 'operator' &&
    previousButtonType !== 'equal' &&
    firstValue &&
    operator
  ) {
    const result = calculate(firstValue, operator, secondValue)
    display.textContent = result
    calculator.dataset.firstValue = result
  } else {
    calculator.dataset.firstValue = displayValue
  }

  calculator.dataset.operator = button.dataset.key
}

We’ll copy this code into Calculator as a method.

export default function Calculator () {
  // Variables

  const calculator = {
    // ...

    handleOperatorKeys (calculator, button) {
      const displayValue = getDisplayValue()
      const { previousButtonType, firstValue, operator } = calculator.dataset
      const secondValue = displayValue

      button.classList.add('is-pressed')

      if (
        previousButtonType !== 'operator' &&
    previousButtonType !== 'equal' &&
    firstValue &&
    operator
      ) {
        const result = calculate(firstValue, operator, secondValue)
        display.textContent = result
        calculator.dataset.firstValue = result
      } else {
        calculator.dataset.firstValue = displayValue
      }

      calculator.dataset.operator = button.dataset.key
    },

    // Other methods
    // Listeners
  }

  // Add Event Listeners
  // Return the calculator
}

We’ll make the same treatments:

  1. Change calculator to calculatorElement
  2. Omit calculatorElement from parameters
  3. Use calculator.displayValue getters and setters
export default function Calculator () {
  // Variables

  const calculator = {
    // ...

    handleOperatorKeys (button) {
      const displayValue = calculator.displayValue
      const { previousButtonType, firstValue, operator } = calculatorElement.dataset
      const secondValue = displayValue

      button.classList.add('is-pressed')

      if (
        previousButtonType !== 'operator' &&
        previousButtonType !== 'equal' &&
        firstValue &&
        operator
      ) {
        const result = calculate(firstValue, operator, secondValue)
        display.textContent = result
        calculatorElement.dataset.firstValue = result
      } else {
        calculatorElement.dataset.firstValue = displayValue
      }

      calculatorElement.dataset.operator = button.dataset.key
    },

    // Other methods
    // Listeners
  }

  // Add Event Listeners
  // Return the calculator
}

handleOperatorKeys need a calculate function to work. Here’s what we wrote for calculate previously.

const calculate = (firstValue, operator, secondValue) => {
  firstValue = parseFloat(firstValue)
  secondValue = parseFloat(secondValue)
  if (operator === 'plus') return firstValue + secondValue
  if (operator === 'minus') return firstValue - secondValue
  if (operator === 'times') return firstValue * secondValue
  if (operator === 'divide') return firstValue / secondValue
}

We’ll create a calculate method in Calculator and paste the code in.

export default function Calculator () {
  // Variables

  const calculator = {
    // ...

    calculate (firstValue, operator, secondValue) {
      firstValue = parseFloat(firstValue)
      secondValue = parseFloat(secondValue)
      if (operator === 'plus') return firstValue + secondValue
      if (operator === 'minus') return firstValue - secondValue
      if (operator === 'times') return firstValue * secondValue
      if (operator === 'divide') return firstValue / secondValue
    },

    // Other methods
    // Listeners
  }

  // Add Event Listeners
  // Return the calculator
}

We use calculate like this:

export default function Calculator () {
  // Variables

  const calculator = {
    // ...

    handleOperatorKeys (button) {
      // ...
      if (
        previousButtonType !== 'operator' &&
        previousButtonType !== 'equal' &&
        firstValue &&
        operator
      ) {
        const result = calculator.calculate(firstValue, operator, secondValue)
        // ...
      } else {
        // ...
      }
      // ...
    },

    // Other methods
    // Listeners
  }

  // Add Event Listeners
  // Return the calculator
}

We’ll use handleOperatorKeys like this

export default function Calculator () {
  // Variables

  const calculator = {
    // Other methods

    handleClick (event) {
      // ...
      switch (buttonType) {
        // ...
        case 'operator': calculator.handleOperatorKeys(button); break
        // case 'equal': handleEqualKey(calculator); break
      }
      // ...
    }
  }

  // Add Event Listeners
  // Return the calculator
}

Errors related to testClearAfterCalculation should now be gone since we have firstValue and operator in the calculator’s dataset.

At this point, we can include every test we created for the calculator into calculator.test.js.

const tests = [
  // Initial Expressions
  {
    message: 'Number key',
    keys: ['2'],
    result: '2'
  }, {
    message: 'Number Number',
    keys: ['3', '5'],
    result: '35'
  }, {
    message: 'Number Decimal',
    keys: ['4', 'decimal'],
    result: '4.'
  }, {
    message: 'Number Decimal Number',
    keys: ['4', 'decimal', '5'],
    result: '4.5'
  },

  // Calculations
  {
    message: 'Addition',
    keys: ['2', 'plus', '5', 'equal'],
    result: '7'
  }, {
    message: 'Subtraction',
    keys: ['5', 'minus', '9', 'equal'],
    result: '-4'
  }, {
    message: 'Multiplication',
    keys: ['4', 'times', '8', 'equal'],
    result: '32'
  }, {
    message: 'Division',
    keys: ['5', 'divide', '1', '0', 'equal'],
    result: '0.5'
  },

  // Easy Edge Cases
  // Number keys first
  {
    message: 'Number Equal',
    keys: ['5', 'equal'],
    result: '5'
  }, {
    message: 'Number Decimal Equal',
    keys: ['2', 'decimal', '4', '5', 'equal'],
    result: '2.45'
  },

  // Decimal keys first
  {
    message: 'Decimal key',
    keys: ['decimal'],
    result: '0.'
  }, {
    message: 'Decimal Decimal',
    keys: ['2', 'decimal', 'decimal'],
    result: '2.'
  }, {
    message: 'Decimal Decimal',
    keys: ['2', 'decimal', '5', 'decimal', '5'],
    result: '2.55'
  }, {
    message: 'Decimal Equal',
    keys: ['2', 'decimal', 'equal'],
    result: '2'
  },

  // Equal key first
  {
    message: 'Equal',
    keys: ['equal'],
    result: '0'
  }, {
    message: 'Equal Number',
    keys: ['equal', '3'],
    result: '3'
  }, {
    message: 'Number Equal Number',
    keys: ['5', 'equal', '3'],
    result: '3'
  }, {
    message: 'Equal Decimal',
    keys: ['equal', 'decimal'],
    result: '0.'
  }, {
    message: 'Number Equal Decimal',
    keys: ['5', 'equal', 'decimal'],
    result: '0.'
  }, {
    message: 'Calculation + Operator',
    keys: ['1', 'plus', '1', 'equal', 'plus', '1', 'equal'],
    result: '3'
  },

  // Operator Keys first
  {
    message: 'Operator Decimal',
    keys: ['times', 'decimal'],
    result: '0.'
  }, {
    message: 'Number Operator Decimal',
    keys: ['5', 'times', 'decimal'],
    result: '0.'
  }, {
    message: 'Number Operator Equal',
    keys: ['7', 'divide', 'equal'],
    result: '1'
  }, {
    message: 'Number Operator Operator',
    keys: ['9', 'times', 'divide'],
    result: '9'
  },

  // Difficult edge cases
  // Operator calculation
  {
    message: 'Operator calculation',
    keys: ['9', 'plus', '5', 'plus'],
    result: '14'
  }, {
    message: 'Operator follow-up calculation',
    keys: ['1', 'plus', '2', 'plus', '3', 'plus', '4', 'plus', '5', 'plus'],
    result: '15'
  },

  // Equal followup calculation
  {
    message: 'Number Operator Equal Equal',
    keys: ['9', 'minus', 'equal', 'equal'],
    result: '-9'
  }, {
    message: 'Number Operator Number Equal Equal',
    keys: ['8', 'minus', '5', 'equal', 'equal'],
    result: '-2'
  }
]

Tests which contain Equal keys will not work (because we haven’t handled the Equal key yet). But other tests should pass. Here’s a list of tests that’ll fail at this point.

Tests that fail because we handle the equal key.

Handle Equal Keys

Again, we will begin by uncommenting the part we use to handle equal keys.

export default function Calculator () {
  // Variables

  const calculator = {
    // Other methods

    handleClick (event) {
      // ...
      switch (buttonType) {
        // ...
        case 'equal': handleEqualKey(calculator); break
      }
      // ...
    }
  }

  // Add Event Listeners
  // Return the calculator
}

Here’s what we wrote to handle Equal previously.

function handleEqualKey (calculator) {
  const displayValue = getDisplayValue()
  const { firstValue, operator, modifierValue } = calculator.dataset
  const secondValue = modifierValue || displayValue

  if (firstValue && operator) {
    const result = calculate(firstValue, operator, secondValue)
    display.textContent = result
    calculator.dataset.firstValue = result
    calculator.dataset.modifierValue = secondValue
  } else {
    display.textContent = parseFloat(displayValue) * 1
  }
}

We’ll copy this code into Calculator as a method.

export default function Calculator () {
  // Variables

  const calculator = {
    // ...

    handleEqualKey (calculator) {
      const displayValue = getDisplayValue()
      const { firstValue, operator, modifierValue } = calculator.dataset
      const secondValue = modifierValue || displayValue

      if (firstValue && operator) {
        const result = calculate(firstValue, operator, secondValue)
        display.textContent = result
        calculator.dataset.firstValue = result
        calculator.dataset.modifierValue = secondValue
      } else {
        display.textContent = parseFloat(displayValue) * 1
      }
    },

    // Other methods
    // Listeners
  }

  // Add event listeners
  // Return calculator
}

And we’ll make the usual changes:

  1. Change calculator to calculatorElement
  2. Omit calculatorElement from parameters
  3. Use calculator.displayValue getters and setters
  4. Use calculator.calculate
export default function Calculator () {
  // Variables

  const calculator = {
    // ...

    handleEqualKey () {
      const displayValue = calculator.displayValue
      const { firstValue, operator, modifierValue } = calculatorElement.dataset
      const secondValue = modifierValue || displayValue

      if (firstValue && operator) {
        const result = calculator.calculate(firstValue, operator, secondValue)
        calculator.displayValue = result
        calculatorElement.dataset.firstValue = result
        calculatorElement.dataset.modifierValue = secondValue
      } else {
        calculator.displayValue = parseFloat(displayValue) * 1
      }
    },

    // Other methods
    // Listeners
  }

  // Add event listeners
  // Return calculator
}

We use handleEqualKey like this:

export default function Calculator () {
  // Variables

  const calculator = {
    // Other methods

    handleClick (event) {
      // ...
      switch (buttonType) {
        // ...
        case 'equal': calculator.handleEqualKey(); break
      }
      // ...
    }
  }

  // Add Event Listeners
  // Return the calculator
}

All tests should pass. There should be no error left.

No errors left

One final thing to do.

Adding Keyboard Functionality

Here’s the code we used to add keyboard functionality:

calculator.addEventListener('keydown', event => {
  let key = event.key

  // Operator keys
  if (key === '+') key = 'plus'
  if (key === '-') key = 'minus'
  if (key === '*') key = 'times'
  if (key === '/') key = 'divide'

  // Special keys
  if (key === '.') key = 'decimal'
  if (key === 'Backspace') key = 'clear'
  if (key === 'Escape') key = 'clear'
  if (key === 'Enter') key = 'equal'
  if (key === '=') key = 'equal'

  const button = calculator.querySelector(`[data-key="${key}"]`)
  if (!button) return
  event.preventDefault()
  button.click()
})

We’ll start by creating a event listener inside Calculator.

export default function Calculator () {
  // ...
  calculatorElement.addEventListener('keydown', event => {/* ... */})
  // Return calculator
}

We’ll name the callback handleKeydown

export default function Calculator () {
  // Variables

  const calculator = {
    // ...
    handleKeydown (event) {
      // ...
    }
  }

  calculatorElement.addEventListener('keydown', calculator.handleKeydown)
  // Return calculator
}

We’ll copy-paste the code we’ve written into handleKeydown.

export default function Calculator () {
  // Variables

  const calculator = {
    // ...

    handleKeydown (event) {
      let key = event.key

      // Operator keys
      if (key === '+') key = 'plus'
      if (key === '-') key = 'minus'
      if (key === '*') key = 'times'
      if (key === '/') key = 'divide'

      // Special keys
      if (key === '.') key = 'decimal'
      if (key === 'Backspace') key = 'clear'
      if (key === 'Escape') key = 'clear'
      if (key === 'Enter') key = 'equal'
      if (key === '=') key = 'equal'

      const button = calculator.querySelector(`[data-key="${key}"]`)
      if (!button) return
      event.preventDefault()
      button.click()
    }

  // Add event listeners
  // Return calculator
}

And we’ll change calculator to calculatorElement.

export default function Calculator () {
  // Variables

  const calculator = {
    // ...

    handleKeydown (event) {
      // ...
      const button = calculatorElement.querySelector(`[data-key="${key}"]`)
      // ...
    }

  // Add event listeners
  // Return calculator
}

You should be able use the calculator with the keyboard now.

That’s it!