Astro Recipes
Essential CSS
GTM For Developers
JavaScript Roadmap
Learn JavaScript
Mastering Web Typography
Understanding Async JS
Unorthodox Tailwind
API Masterclass
Build and Deploy
The Beginner Freelancer
Real World Node
Profile Billing Logout
Learn JavaScript
Lesson Plan
  1. JavaScript and its ecosystem
    1. Welcome to Learn JavaScript!
    2. What is JavaScript used for?
    3. The JavaScript ecosystem
    4. Varying versions of JavaScript
  2. JS Basics
    1. Linking your JavaScript file
    2. Preparing your text editor
    3. The Console
    4. Comments
    5. On semicolons
    6. Strings, numbers and booleans
    7. Understanding Variables
    8. Understanding Functions
    9. The Flow of a Function
    10. Arrow functions
    11. Intro to objects
    12. If/else statements
    13. Comparing Objects
    14. The NOT operator
    15. Null and Undefined
    16. The BOM and the DOM
    17. Selecting an Element
    18. Changing Classes
    19. Listening to events
    20. Callbacks
  3. Building simple components
    1. How to think like a developer
    2. Starter files and Source codes
    3. Do this for every component
    4. 🛠 Off-canvas menu: Building an off-canvas menu
    5. 🛠 Modal: Building a Modal
    6. Lessons from the building process
    7. Debugging errors
    8. How to use a linter
  4. Arrays and loops
    1. Introduction to Arrays
    2. Array methods
    3. For loops
    4. The forEach loop
    5. Selecting multiple elements
    6. Nodes vs Elements
    7. 🛠 Accordion: Building an accordion
  5. Dom basics
    1. Id, classes, attributes, and tags
    2. Changing CSS with JavaScript
    3. Getting CSS with JavaScript
    4. Changing Attributes
    5. Finding an element's size and position
    6. DOM Traversals
    7. 🛠 Tabby: Building Tabby (A Tabbed component)
    8. 🛠 Carousel: HTML and CSS
    9. 🛠 Carousel: Switching slides with JavaScript
    10. 🛠 Carousel: Working the dots
    11. 🛠 Carousel: Positioning slides with JavaScript
  6. Events deep dive
    1. The listening element
    2. Default Behaviors
    3. Event propagation
    4. Event delegation
    5. Removing Event Listeners
    6. 🛠 Modal: Closing the modal
    7. 🛠 Accordion: Event delegation
    8. 🛠 Tabby: Event delegation
    9. 🛠 Carousel: Event delegation
  7. Transitions and Animations
    1. CSS Transitions
    2. CSS Animations
    3. Silky-smooth animations
    4. Integrating CSS transitions and animations with JavaScript
    5. Animating with JavaScript
    6. GreenSock Animation API (GSAP)
    7. 🛠 Off-canvas menu: Animations
    8. 🛠 Modal: Animating the modal
    9. 🛠 Modal: Animating the pointing hand
    10. 🛠 Modal: Animating the waving hand
    11. 🛠 Modal: Wave hand animation with JavaScript (using GSAP)
    12. 🛠 Accordion: Animations
    13. 🛠 Carousel: Animations
  8. Useful JS features
    1. Ternary operators
    2. AND and OR operators
    3. Early returns
    4. Template Literals
    5. Destructuring
    6. Default parameters
    7. Enhanced Object Literals
    8. Rest and Spread
    9. Useful array methods
    10. Looping through objects
    11. Returning objects with implicit return
    12. 🛠 Accordion: Using useful JavaScript features
    13. 🛠 Tabby: Using useful JavaScript features
    14. 🛠 Carousel: Useful JavaScript features
  9. JS Best practices
    1. Write declarative code
    2. Functions with a purpose
    3. Manage scope
    4. Reduce state changes
    5. Don't reassign
    6. Don't mutate
    7. Preventing Objects from mutating
    8. Preventing Arrays from mutating
    9. Write pure functions
    10. 🛠 Accordion: Refactor
    11. 🛠 Carousel: First refactor
    12. 🛠 Carousel: Refactoring the dots part
    13. 🛠 Carousel: Previous and next buttons
    14. 🛠 Carousel: Second refactor
  10. Manipulating text and content
    1. Changing Text and HTML
    2. Creating HTML Elements
    3. Adding multiple elements to the DOM
    4. Removing Elements from the DOM
    5. 🛠 Carousel: Creating dots with JavaScript
    6. 🛠️ Calculator: HTML and CSS
    7. 🛠️ Calculator: Happy Path
    8. 🛠️ Calculator: Testing the Happy Path
    9. 🛠️ Calculator: Easy Edge Cases
    10. 🛠️ Calculator: Difficult Edge Cases
    11. 🛠️ Calculator: Refactoring
    12. The switch statement
    13. 🛠️ Calculator: Refactoring (Part 2)
    14. 🛠️ Popover: Making one popover
    15. 🛠️ Popover: Making four popovers
    16. 🛠️ Popover: Making popovers with JavaScript
  11. Handling Forms
    1. Intro to forms
    2. Selecting form fields with JavaScript
    3. Form fields and their events
    4. Sanitize your output
    5. Generating unique IDs
    6. 🛠️ Popover: Dynamic ID
    7. 🛠️ Todolist: The HTML and CSS
    8. 🛠️ Todolist: Creating tasks with JavaScript
    9. 🛠️ Todolist: Deleting tasks with JavaScript
    10. 🛠️ Typeahead: The HTML and CSS
    11. 🛠️ Typeahead: Displaying predictions
    12. 🛠️ Typeahead: Selecting a prediction
    13. 🛠️ Typeahead: Bolding search terms
  12. Handling Dates
    1. The Date object
    2. Getting a formatted date
    3. Getting the time
    4. Local time and UTC Time
    5. Setting a specific date
    6. Setting a date with Date methods
    7. Adding (or subtracting) date and time
    8. Comparing Dates and times
    9. 🛠️ Datepicker: HTML and CSS
    10. 🛠️ Datepicker: Building the calendar
    11. 🛠️ Datepicker: Building the datepicker with JavaScript
    12. 🛠️ Datepicker: Previous and Next buttons
    13. 🛠️ Datepicker: Selecting a date
    14. 🛠️ Datepicker: Positioning the datepicker
    15. 🛠️ Datepicker: Showing and hiding
    16. Formatting a date with toLocaleString
    17. setTimeout
    18. setInterval
    19. 🛠️ Countdown timer: HTML and CSS
    20. 🛠️ Countdown timer: JavaScript
    21. 🛠️ Countdown timer: Counting Months
    22. 🛠️ Countdown timer: Daylight Saving Time
    23. 🛠️ Countdown timer: Counting Years
  13. Async JS
    1. Introduction to Ajax
    2. Understanding JSON
    3. The Fetch API
    4. Possible data types
    5. JavaScript Promises
    6. Requests and responses
    7. Sending a POST request
    8. Authentication
    9. Handling errors
    10. Viewing response headers
    11. CORS and JSONP
    12. XHR vs Fetch
    13. Using an Ajax library
    14. Reading API documentation
    15. Understanding curl
    16. 🛠️ Todolist: The Todolist API
    17. 🛠️ Todolist: Fetching tasks
    18. 🛠️ Todolist: Creating tasks
    19. 🛠️ Todolist: Editing tasks
    20. 🛠️ Todolist: Deleting tasks
    21. 🛠️ Todolist: Creating tasks with Optimistic UI
    22. 🛠️ Todolist: Handling Optimistic UI errors
    23. 🛠️ Todolist: Editing tasks with Optimistic UI
    24. 🛠️ Todolist: Deleting tasks with Optimistic UI
    25. 🛠️ Todolist: Refactor
    26. 🛠️ Typeahead: How to add Ajax
    27. 🛠️ Typeahead: Adding Ajax
    28. 🛠️ Typeahead: Handling errors
    29. 🛠️ Google Maps Clone: Creating your first Google Map
    30. 🛠️ Google Maps Clone: Fetching JSONP via JavaScript
    31. 🛠️ Google Maps Clone: Drawing directions
    32. 🛠️ Google Maps Clone: Driving directions
    33. 🛠️ Google Maps Clone: Handling errors
    34. 🛠️ Google Maps Clone: Adding stopovers
    35. 🛠️ Google Maps Clone: Refactor
  14. Advanced Async JS
    1. Requesting many resources at once
    2. Asynchronous functions
    3. Handling multiple awaits
    4. Asynchronous loops
    5. 🛠️ Dota Heroes: Listing heroes
    6. 🛠️ Dota Heroes: Filtering heroes (Part 1)
    7. 🛠️ Dota Heroes: Filtering heroes (Part 2)
    8. 🛠️ Dota Heroes: Refactoring
    9. 🛠️ Dota Heroes: Hero Page
    10. 🛠️ Dota Heroes: Making the hero page robust
    11. 🛠️ Dota Heroes: Heroes page refactor
  15. Handling Keyboard Events
    1. Keyboard users
    2. Handling commonly used keys
    3. Keyboard events
    4. Understanding Tabindex
    5. Detecting the focused element
    6. Directing focus
    7. Preventing people from tabbing into elements
    8. How to choose keyboard shortcuts
    9. Creating single-key shortcuts
    10. 🛠️ Off-canvas: Adding keyboard interaction
    11. 🛠️ Modal: Adding keyboard interaction
    12. 🛠️ Accordion: Adding keyboard interaction
    13. 🛠️ Tabby: Adding keyboard interaction
    14. 🛠️ Tabby: Refactoring
    15. 🛠️ Carousel: Adding keyboard interaction
    16. 🛠️ Carousel: Displaying help text
    17. 🛠️ Calculator: Adding keyboard interaction
    18. 🛠️ Popover: Keyboard
    19. 🛠️ Popover: Refactor
    20. Keyboard shortcuts with Command and Control modifiers
    21. 🛠️ Todolist: Keyboard
    22. 🛠️ Typeahead: Keyboard
    23. 🛠️ Typeahead: Selecting a prediction with the keyboard
    24. 🛠️ Google Maps Clone: Keyboard
    25. 🛠️ Dota Heroes: Keyboard
    26. 🛠️ Datepicker: Tabbing in and out
    27. 🛠️ Datepicker: Keyboard shortcuts
  16. Screen reader accessibility
    1. What is accessibility?
    2. How to use a screen reader
    3. Using NVDA
    4. Using Voiceover
    5. Aria roles
    6. Landmark roles
    7. Document structure roles
    8. Live region roles
    9. Widget roles
    10. Window and Abstract roles
    11. Accessible names and descriptions
    12. Hiding content
    13. ARIA properties and ARIA states
    14. ARIA for expandable widgets
    15. 🛠️ Off-canvas: Accessibility
    16. ARIA for modal dialogs
    17. 🛠️ Modal: Screen reader accessibility
    18. 🛠️ Accordion: Screen reader accessibility
    19. ARIA for Tabbed components
    20. 🛠️ Tabby: Screen reader accessibility
    21. 🛠️ Tabby: Refactor
    22. 🛠️ Carousel: Screen reader accessibility
    23. Roles that trigger Forms and Application modes
    24. What's next for accessibility?
  17. Handling Scroll
    1. The Scroll event
    2. 🛠️ Auto-hiding Sticky-nav: HTML and CSS
    3. 🛠️ Auto-hiding Sticky-nav: JavaScript
    4. 🛠️ Auto-hiding Sticky-nav: Natural reveal
    5. Intersection Observer API
    6. Intersection Observer Options
    7. 🛠️ Slide & Reveal
    8. 🛠️ Slide & Reveal: Always fade-in when you scroll down
    9. 🛠️ Slide & Reveal: Fine-tuning the animation
    10. 🛠️ Infinite Scroll: Anatomy
    11. 🛠️ Infinite Scroll: Infinite load
    12. 🛠️ Infinite Scroll: Refactor
    13. 🛠️ Infinite Scroll: Implementing the Infinite Scroll
  18. Mouse, Touch, and Pointer events
    1. Mouse Events
    2. 🛠️ Spinning Pacman: HTML and CSS
    3. 🛠️ Spinning Pacman: JavaScript
    4. Touch events
    5. Pointer events
    6. Touch-action
    7. 🛠️ Spinning Pacman: Supporting Touch
    8. Cloning elements
    9. 🛠️ DragDrop: HTML and CSS
    10. 🛠️ DragDrop: JavaScript
    11. 🛠️ DragDrop: Creating a drop preview
    12. 🛠️ DragDrop: Sortable drop preview
    13. 🛠️ DragDrop: Robustness
    14. 🛠️ DragDrop: Refactor
  19. Object Oriented Programming
    1. Before we begin
    2. What is Object Oriented Programming?
    3. Four Flavours of Object Oriented Programming
    4. Inheritance
    5. This in JavaScript
    6. Call, bind, apply
    7. Creating Derivative Objects
    8. Composition vs Inheritance
    9. Polymorphism
    10. Encapsulation
    11. Closures
    12. Encapsulation in Object Oriented Programming
    13. Getters and Setters
    14. What OOP flavour to use
    15. When to use Object Oriented Programming
  20. Writing reusable code
    1. Creating reusable code by writing libraries
    2. Two Types of libraries
    3. Including libraries with Script tags
    4. Including libraries with ES6 Modules
    5. Dynamic imports
    6. 🛠️ Off Canvas: Building a Library
    7. 🛠️ Modal: Library setup
    8. 🛠️ Modal: Opening the Modal
    9. 🛠️ Modal: Closing the modal
    10. 🛠️ Modal: Inheritance and Polymorphism
    11. 🛠️ Modal: Resolving differences between subclasses
    12. 🛠️ Modal: Exposing properties and methods
    13. 🛠️ Accordion: Building a library
    14. 🛠️ Tabby: Building a library
    15. 🛠️ Carousel: Building a library
    16. 🛠️ Calculator: Library
    17. 🛠️ Calculator: Fixing the Clear Key
    18. 🛠️ Calculator: Handling other keys
    19. 🛠️ Calculator: State
    20. 🛠️ Popover: Library
    21. 🛠️ Popover: Adding event listeners
    22. 🛠️ Typeahead: Library
    23. 🛠️ DatePicker: Library
  21. Going from Vanilla JS to JS Frameworks
    1. 🛠️ Building a Tiny framework
    2. 🛠️ Tiny: Add event listeners
    3. 🛠️ Tiny: Updating state
    4. 🛠️ Tiny: Rendering Child Components
    5. 🛠️ Tiny: Changing Parent State
    6. 🛠️ Tiny: Passing Props
    7. 🛠️ Tiny: Multiple Props
    8. 🛠️ Tiny: Passing values from sibling components
    9. 🛠️ Tiny: Mounting
    10. 🛠️ Tiny: Passing props to descendants
    11. 🛠️ Tiny: A tiny refactor
  22. Single Page Apps
    1. What is a Single Page App?
    2. Simple SPA using only CSS
    3. The Location Interface
    4. The History Interface
    5. Minimum viable server for a SPA
    6. 🛠️ Dota SPA: Introduction
    7. 🛠️ Dota SPA: Building The Heroes List
    8. 🛠️ Dota SPA: Building the filters
    9. 🛠️ Dota SPA: Filtering heroes
    10. 🛠️ Dota SPA: Displaying filtered heroes
    11. 🛠️ Dota SPA: Getting Ready to build the Hero Page
    12. 🛠️ Dota SPA: Building the hero page
    13. 🛠️ Dota SPA: Lore and abilities
    14. 🛠️ Dota SPA: Routing for Single-page apps

🛠️ Calculator: Difficult Edge Cases

14m:35s
Source code

The difficult edge cases involve follow-up calculations. Don’t get what I mean? Don’t worry. Let’s dive in and you’ll see.

Calculating with operators

Try whipping out a calculator. Press keys in these sequence: Number -> Operator -> Number -> Operator. What happens? The calculator should have made a calculation.

For example:

  • Press 9 - 5 -. Calculator should show 4.

But why?

If the user press 9 - 5 -, we can assume they want to press another number to continue their calculation. We want to show them an intermediate result so they can keep track of their results.

Right now, our calculator doesn’t perform a calculation. It seems doesn’t do anything when you press the second operator.

Punch 9 - 5 - into the calculator. Calculator shows 5.

Let’s add a test case before we continue. It should fail.

const tests = [
// ...
{
message: 'Operator calculation',
keys: ['9', 'minus', '5', 'minus'],
result: '4',
},
]
Assertion fail.

The easiest way to make a calculation is to copy-paste code from the equal section into the operator section.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'operator') {
button.classList.add('is-pressed')
const firstValue = parseFloat(calculator.dataset.firstValue)
const operator = calculator.dataset.operator
const secondValue = parseFloat(result)
// Makes a calculation
if (firstValue && operator) {
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
}
calculator.dataset.firstValue = result
calculator.dataset.operator = button.dataset.key
}
// ...
})

The test passes!

Clicks 9 - 5 -. Calculator shows 4.

Unfortunately, this new code screws up a Number -> Operator -> Operator combination (which we did not test before).

Number -> Operator -> Operator

If the user presses an operator after another operator, we assume they pressed the wrong operator the first time round. They simply wanted to switch operators, so we should not perform calculations.

Right now, our calculator performs a calculation.

Clicks 9 * -. Caluclator shows 81.

Let’s write a new test case to ensure Number -> Operator -> Operator works.

const tests = [
// ...
{
message: 'Number Operator Operator',
keys: ['9', 'times', 'divide'],
result: '9',
},
]
Assertion failed.

We know how to fix this. If the previous button is an operator, we want to skip the calculation entirely.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'operator') {
// ...
if (previousButtonType === 'operator') {
// Do nothing
} else {
const firstValue = parseFloat(calculator.dataset.firstValue)
const operator = calculator.dataset.operator
const secondValue = parseFloat(result)
// Makes a calculation
if (firstValue && operator) {
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
}
}
// ...
}
// ...
})

This works. All our tests passed!

Punch 9 times minus into the calculator. Calculator shows 9.

But it’s weird to have a do nothing comment in an if statement. We can reverse the if to make the code cleaner.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'operator') {
// ...
// Reverses the if condition
if (previousButtonType !== 'operator') {
const firstValue = parseFloat(calculator.dataset.firstValue)
const operator = calculator.dataset.operator
const secondValue = parseFloat(result)
// Makes a calculation
if (firstValue && operator) {
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
}
}
// ...
}
// ...
})

Since we will only calculate if previousButtonType !== 'operator', and if there’s a firstValue and operator value, we can combine the two if statements into one. This cleans up the code a bit.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'operator') {
// ...
const firstValue = parseFloat(calculator.dataset.firstValue)
const operator = calculator.dataset.operator
const secondValue = parseFloat(result)
// Combines two if statements into one
if (previousButtonType !== 'operator' && firstValue && operator) {
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
}
// ...
}
// ...
})

Follow up calculations

Once users press an operator key, they can perform follow up calculations with BOTH the operator and equal keys. This gets complicated.

Here are the possible combinations:

  1. Number -> Operator -> Equal -> Equal
  2. Number -> Operator -> Number -> Equal -> Equal
  3. Number -> Operator -> Number -> Operator -> Number -> Operator ->

Follow up calculations with equal

These two follow up calculations uses the equal key:

  1. Number -> Operator -> Equal -> Equal
  2. Number -> Operator -> Number -> Equal -> Equal

Example:

  1. Tim presses 9 - =. We take it as 9 - 9. Calculator shows 0. Tim presses = again. We do 0 - 9, so calculator shows - 9.
  2. Tim presses 8 - 5 =. We show 3. Tim presses = again. We do 3 - 5, so calculator shows -2.

Let’s add test cases before we continue.

const tests = [
// ...
{
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',
},
]

Both tests should fail.

Assertions fail.

We’ll tackle them together. This part is tricky; follow closely!

Let’s look at the first case. Here’s what our calculator does when we press 9 - = = .

Clicks 9 - =. Shows 0. Clicks = again. Shows 9.

Why does the calculator show 9, 0, then 9? We can log firstValue, operator, secondValue and newResult into the console to figure out why.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'equal') {
// ...
if (firstValue && operator) {
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
// console.log values.
console.log(firstValue, operator, secondValue, '=', newResult)
} else {
display.textContent = parseFloat(result) * 1
}
}
// ...
})

Note: You will see lots of logs when you refresh the page. These logs were created by our test cases. If they bother you, you can comment out the tests for now.

(Another way is to clear the console before clicking the calculator).

Click the calculator according to the Number -> Operator -> Equal -> Equal test case. Pay attention to the values for firstValue, operator, and secondValue

Logs firstValue, operator, secondValue, and newResult for each calculation.

It’s hard to understand what’s happening from logs. Let’s put down these values down on paper so we can search for clues. (I actually wrote them down on paper).

Putting the logs down on paper.

For the first calculation:

  • firstValue is 9 (which is correct)
  • secondValue is also 9 (also correct).

For the second calculation:

  • firstValue is 9 (which is wrong).
  • secondValue is 0 (also wrong)

Why? Here’s the reason.

  1. We used data-first-value as firstValue for the second calculation.
  2. We used the displayed value (which is the result) as the secondValue for the second calculation.
Same image as above, but with arrows to link where the logic went wrong.

Here’s the correct version:

  1. We should assign the result of the calculation to firstValue.
  2. We should reuse 9 as secondValue.
The correct flow of calculations.

It’s easy to assign the result to firstValue. We can assign it after the calculation.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'equal') {
const firstValue = parseFloat(calculator.dataset.firstValue)
const operator = calculator.dataset.operator
const secondValue = parseFloat(result)
if (firstValue && operator) {
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
// Assigns results to `firstValue`
calculator.dataset.firstValue = newResult
} else {
display.textContent = parseFloat(result) * 1
}
}
// ...
})

Setting 9 to secondValue is tricky because the number 9 disappears from the display. We need to save 9 as a custom attribute. Let’s call this custom attribute data-modifier-value.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'equal') {
const firstValue = parseFloat(calculator.dataset.firstValue)
const operator = calculator.dataset.operator
const secondValue = parseFloat(result)
if (firstValue && operator) {
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
calculator.dataset.firstValue = newResult
// Stores secondValue as modifier for followup calculations.
calculator.dataset.modifierValue = secondValue
} else {
display.textContent = parseFloat(result) * 1
}
}
// ...
})

Before the next calculation, we need to check if modifierValue is present. If it is present, we use modifierValue as secondValue. Otherwise, we use the displayed result as the secondValue.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'equal') {
const firstValue = parseFloat(calculator.dataset.firstValue)
const operator = calculator.dataset.operator
// Finds modifier value
// Use modifier value as secondValue (if possible)
const modifierValue = parseFloat(calculator.dataset.modifierValue)
const secondValue = modifierValue || parseFloat(result)
if (firstValue && operator) {
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
calculator.dataset.firstValue = newResult
calculator.dataset.modifierValue = secondValue
} else {
display.textContent = parseFloat(result) * 1
}
}
// ...
})

When we make this change, we break A TON of tests! 😱 😱 😱!

Many tests fail.

Don’t panic!

It’s normal to break tests when you add features. In this case, we started using modifierValue in our calculations, but we did not reset modifierValue when we reset the calculator. This is why our tests broke.

Here’s a simple fix:

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'clear') {
if (button.textContent === 'AC') {
delete calculator.dataset.firstValue
delete calculator.dataset.operator
// Clearing the modifier value
delete calculator.dataset.modifierValue
}
display.textContent = '0'
button.textContent = 'AC'
}
// ...
})

We’ll also updated resetCalculator to ensure modifierValue gets reset.

The previous tests should now pass. You’ll also notice this:

  1. Test for Number -> Operator -> Equal -> Equal failed
  2. Test for Number -> Operator -> Number -> Equal -> Equal passed
Only Number Operator Equal Equal follow up calculations fail.

Why?

Once again, we can log firstValue, operator, secondValue, and newResult to figure out the reason.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'equal') {
// ...
if (firstValue && operator) {
// ...
// Console.log values.
console.log(firstValue, operator, secondValue, '=', newResult)
} else {
display.textContent = parseFloat(result) * 1
}
}
// ...
})
Debugs the above picture.

We can see the calculator made the first calculation. But it doesn’t make the second calculation no matter how many times we press equal.

Why?

Because firstValue became 0, which is falsey. So this condition became false 😰.

if (firstValue && operator) {
// ...
}

This is a good lesson for us: don’t use truthiness to check numbers. If we want to check if a number exists, we can use typeof.

// Examples of how typeof works
console.log(typeof 42) // number
console.log(typeof 42 === 'number') // true

Usage:

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'equal') {
// ...
if (typeof firstValue === 'number' && operator) {
// Make calculation
} else {
// ...
}
}
// ...
})

All tests should pass. The calculator can perform followup calculations with equal now.

  1. Number -> Operator -> Equal -> Equal
  2. Number -> Operator -> Number -> Equal -> Equal
Number <code>-></code> Operator <code>-></code> Equal <code>-></code> Equal
Number <code>-></code> Operator <code>-></code> Number <code>-></code> Equal <code>-></code> Equal

Follow up calculations with operators

Users can perform a calculation with operator keys if they use the following sequence:

  • Number -> Operator -> Number -> Operator

We should also let them perform additional calculations if they press more of this sequence:

  • Number -> Operator -> Number -> Operator -> Number -> Operator -> And so on.

For example, let’s say Tim presses keys in this order: 1 + 2 + 3 + 4 + 5 +.

This should happen:

  1. 1 + 2 +. Calculator shows 3.
  2. 3 +. Calculator shows 6.
  3. 4 +. Calculator shows 10.
  4. 5 +. Calculator shows 15.

Let’s create a test case before we continue.

const test = [
// ...
{
message: 'Operator follow-up calculation',
keys: ['1', 'plus', '2', 'plus', '3', 'plus', '4', 'plus', '5', 'plus'],
result: '15',
},
]

This should fail (as expected).

Assertion failed.

Let’s see what our calculator does when we make followup calculations right now.

Punched 1 + 2 + 3 + 4 + 5 + into the calculator.

We know the test failed. It’s not a surprise our calculator made the wrong calculations.

We need to follow this format to correct the calculations:

How to create follow-up operator calculations

This says:

  1. We should replace firstValue with the calculated result.
  2. We should get secondValue from the display.

We already get secondValue from the display, so there’s nothing to do for point 2. For point 1, here’s how you can replace firstValue with the calculated result.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'operator') {
// ...
if (previousButtonType !== 'operator' && firstValue && operator) {
// ...
display.textContent = newResult
// If there's a calculation, we change firstValue
calculator.dataset.firstValue = newResult
} else {
// Otherwise, we set displayed result to firstValue
calculator.dataset.firstValue = result
}
calculator.dataset.operator = button.dataset.key
}
// ...
})

Like the equal section, we need to check whether firstValue is a Number. We should use typeof for this check.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'operator') {
// ...
if (
previousButtonType !== 'operator' &&
typeof firstValue === 'number' &&
operator
) {
// ...
} else {
// ...
}
calculator.dataset.operator = button.dataset.key
}
// ...
})

We should be able to perform follow-up calculations with operator keys now.

Clicks 1 + 2 + 3 + 4 + 5. Shows 15 at the end.

Unfortunately, this new code breaks the Calculation + Operator test.

Assertion failed.

It’s not fun to see our tests break. But it’s even more un-fun to get an error yourself much later without even knowing what went wrong. Let’s fix this.

Fixing the Calculation + Operator test

So, what’s wrong? Let’s take a quick look at our tests again. Here’s the Calculation + operator test

const test = [
// ...
{
message: 'Calculation + Operator',
keys: ['1', 'plus', '1', 'equal', 'plus', '1', 'equal'],
result: '3',
},
]

Let’s punch in this sequence and see what’s up.

Punching in 1 + 1 = + 1 =.

Oh, looks like the calculator made a calculation when you click Equal -> Operator. This should not happen. We can fix this easily by checking if the previous button is equal.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'operator') {
// ...
if (
previousButtonType !== 'operator' &&
// Checks if previous button is equal key
previousButtonType !== 'equal' &&
typeof firstValue === 'number' &&
operator
) {
// ...
} else {
// ...
}
calculator.dataset.operator = button.dataset.key
}
// ...
})

BAM! The test should now be fixed 😃.

Punch in 1 + 1 = + 1 =. Calculator shows 3.

Wrapping up

That’s it!

Building a calculator is hard! It was never easy. Pat yourself on the back for following this through. You are MUCH better than many developers out there now. Try challenging them to build a calculator and watch them suffer… 😈.

(I know how bad they’ll suffer because I made the calculator without tests TWICE. It was 🤮. But with tests, this is easy-peasy. We just have to fix things one after another. ).

Homework

  1. Write down all the cases I mentioned on a piece of paper.
  2. Build the calculator again from scratch
  3. See if you can find any cases I missed :)

Take your time, clear away your bugs one by one and you’ll get your calculator up eventually. Happy coding!