Custom attributes (saved inside dataset) are converted into strings and appear in the HTML automatically.
We saved some values inside custom attributes with dataset. In this case, we saved firstValue, operator, modifierValue and previousButtonType in calculatorElement.dataset.
Since we use firstValue, operator, modifierValue and previousButtonType inside JavaScript directly, there’s no need to pass these information back to HTML. We can use a normal JavaScript object instead.
When we use a JavaScript object to store information about a component, we often call this object state.
Switching from dataset to state
First, we need to create a state variable to hold information. We will begin with an empty object.
Although undefined is the actual value, I tend to initialise state properties to either an empty string (''). I do this because it’s much easier to browse through the state object.
Errors related to the “Number Operator Equal Equal” calculation
Fixing “Clearing After Calculation” errors
If you look at testClearAfterCalculation in calculator.test.js, you’ll notice we still use dataset in the test. We need to switch it to state.
Before we can use state in the test file, we need to expose the state via a getter function. This getter function ensures state value retrieved is always up to date.
Another benefit: Users cannot overwrite the state object (and hence mess things up) since there is no setter function.
export default function Calculator () {
// Declare Variables
const state = {/* ... */}
const calculator = {
get state () { return state },
// Other methods
}
}
We can now get the calculator’s state like this:
Tests related to “Clearing After Calculation” should pass now.
Removing false positives
If you look at testFullClear, you’ll see we’re still using dataset as well, but the test passes!
// We are still using `dataset` in `testFullClear`
function testFullClear () {
// ...
console.assert(!calculator.element.dataset.firstValue, 'Full Clear:No first value')
console.assert(!calculator.element.dataset.operator, 'Full Clear: No operator value')
console.assert(!calculator.element.dataset.modifierValue, 'Full Clear: No operator value')
}
We need to change dataset to state here as well.
function testFullClear () {
// ...
console.assert(!calculator.state.firstValue, 'Full Clear:No first value')
console.assert(!calculator.state.operator, 'Full Clear: No operator value')
console.assert(!calculator.state.modifierValue, 'Full Clear: No operator value')
}
Fixing the calculation error
When we switched from dataset to state, we broke one test case: Number -> Operator -> Equal -> Equal. But why?
One of the things I did when debugging this was to log firstValue into the console. When I did this, I noticed firstValue was a String on most occasions, but it changed to a Number on other occasions. This is evident from the purple text (which denotes numbers) in Chrome Devtools.
But why is firstValue, which usually is a String, become a Number?
Turns out, firstValue is a String because we get that value from the HTML (which can only return strings). However, when we calculate the result, we saved the result back into state.firstValue. This result is a Number.
export default function Calculator () {
// ...
const calculator = {
handleEqualKey () {
// ...
if (firstValue && operator) {
const result = calculator.calculate(firstValue, operator, secondValue)
calculator.displayValue = result
state.firstValue = result // Saves result into state. This result is a number.
state.modifierValue = secondValue
} else {
calculator.displayValue = parseFloat(displayValue) * 1
}
},
// ...
}
// ...
}
It’s confusing when we deal with two data types at the same time. In this case, I recommend we stick to Strings because the dataset version uses Strings.
We can convert Numbers back into Strings easily with the toString method. We’ll do this before we return the calculated value.
export default function Calculator () {
// Declare Variables
const calculator = {
calculate (firstValue, operator, secondValue) {
firstValue = parseFloat(firstValue)
secondValue = parseFloat(secondValue)
let result
if (operator === 'plus') result = firstValue + secondValue
if (operator === 'minus') result = firstValue - secondValue
if (operator === 'times') result = firstValue * secondValue
if (operator === 'divide') result = firstValue / secondValue
return result.toString()
},
// Other methods
}
// Add event listeners
// Return calculator
}
Note: We should not use multiple if statements if we’re not doing early returns. We should use switch instead, because it’s clear that code doesn’t flow into unnecessary if conditions.
export default function Calculator () {
// Declare Variables
const calculator = {
calculate (firstValue, operator, secondValue) {
// ...
let result
switch (operator) {
case 'plus': result = firstValue + secondValue; break
case 'minus': result = firstValue - secondValue; break
case 'times': result = firstValue * secondValue; break
case 'divide': result = firstValue / secondValue; break
}
return result.toString()
},
// Other methods
}
// Add event listeners
// Return calculator
}
This fixes the error.
And that’s all we have to do to use the state variable instead of dataset.
Bonus
Take a look at handleClick. Did you notice we had to pass button into some handler methods?
export default function Calculator () {
// Declare Variables
const calculator = {
handleClick () {
// ...
switch (buttonType) {
case 'clear': calculator.handleClearKey(); break
case 'number': calculator.handleNumberKeys(button); break
case 'decimal': calculator.handleDecimalKey(); break
case 'operator': calculator.handleOperatorKeys(button); break
case 'equal': calculator.handleEqualKey(); break
}
},
// Other methods
}
// Add event listeners
// Return calculator
}
Is there a way to standardise the functions so we don’t pass button into some of them? I’ll leave you to figure this out as a bonus :)