🛠️ 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!