🛠️ Calculator: Handling other keys
We have three more types of keys to handle:
Decimal
Operator
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.
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:
Change calculator to calculatorElement
Omit calculatorElement from parameters
Use calculator.displayValue getters and setters
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.
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:
Change calculator to calculatorElement
Omit calculatorElement from parameters
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.
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:
Change calculator to calculatorElement
Omit calculatorElement from parameters
Use calculator.displayValue getters and setters
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.
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!