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: Easy Edge Cases

14m:40s
Source code

Let’s dive in and fix up those edge cases, shall we? This time, let’s say we have a troublemaker called Tim. Tim can begin with any of these five keys:

  1. A number key
  2. An operator key
  3. The decimal key
  4. The clear key
  5. The equal key

Let’s work through each key combinations one by one, starting with number keys.

Number key first

If Tim clicks a number key, we replace the displayed result with the clicked number. This was already covered in the “Happy Path” code.

After clicking a number, Tim can click any of these keys:

  1. Number -> Number (Handled by happy path)
  2. Number -> Decimal (Handled by happy path)
  3. Number -> Operator (Handled by happy path)
  4. Number -> Clear (Handled by happy path)
  5. Number -> Equal

We have already handled four of these sequences with the happy path code. Now, we need to handle what happens if Tim clicks equal after a number.

Number -> Equal

Let’s start with a test. If Tim clicks 5 =, the calculator should show 5.

const tests = [
// ...
{
message: 'Number Equal',
keys: ['5', 'equal'],
result: '5',
},
]
Assertion failed for Number Equal test.

😰.

It’s alright. The test told us something is wrong. It’s our job to figure out what’s wrong. Let’s see what happens if we press 5 then =.

Display becomes empty after pressing 5 then equal.

Why does the display become empty? We must have done something. In this case, we tried to make a calculation when we pressed the equal key.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'equal') {
const firstValue = parseFloat(calculator.dataset.firstValue)
const operator = calculator.dataset.operator
const secondValue = parseFloat(result)
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
}
// ...
})

But we can’t make any calculation if we don’t have firstValue and operator. newResult will be undefined.

To fix this edge case, we can skip calculation if we don’t have firstValue and operator.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'equal') {
const firstValue = parseFloat(calculator.dataset.firstValue)
const operator = calculator.dataset.operator
const secondValue = parseFloat(result)
// Skips calculation if there's no `firstValue` and `operator`
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
}
}
// ...
})

It works great now. Our test passed too! 😃.

Display shows 5 after clicking 5 then equal.

Number -> Decimal -> Equal

What if Tim presses 2 . 4 5 =? The calculator should show 2.45. Does ours show 2.45?

We can test this with runTest.

const tests = [
// ...
{
message: 'Number Decimal Equal',
keys: ['2', 'decimal', '4', '5', 'equal'],
result: '2.45',
},
]

It works! Since it worked, you shouldn’t see an error message. But we can double confirm things are 👌 by testing it out manually.

Presses 2, decimal, 4, 5. Calculator displays 2.45.

Note: We should confirm each test manually at least once. This gives us confidence that tests work. If a test fails in future, we know we changed something and broke the calculator. We can easily undo the change when this happens.

We have now handled all possible cases starting with number keys. Tim can still start with four other types of keys:

  1. An operator key
  2. The decimal key
  3. The clear key
  4. The equal key

Let’s work on decimal keys first. (Operator keys are complicated. We’ll get to them in the next lesson).

Decimal key first

If the display shows zero, we should append a decimal. Our code handles this already.

Clicks the decimal key. Display shows zero and dot.

But let’s add a test case to confirm this works (going forward).

const tests = [
// ...
{
message: 'Decimal key',
keys: ['decimal'],
result: '0.',
},
]

After clicking a decimal, Tim can click any of these keys:

  1. Decimal -> Number (Handled by happy path)
  2. Decimal -> Decimal
  3. Decimal -> Operator (Handled by happy path)
  4. Decimal -> Clear (Handled by happy path)
  5. Decimal -> Equal

Decimal -> Decimal

If Tim press 2 . ., the calculator should show 2..

Let’s start by writing a test.

const test = [
// ...
{
message: 'Decimal Decimal',
keys: ['2', 'decimal', 'decimal'],
result: '2.',
},
]
Decimal Decimal assertion failed.

Okay, the assertion failed. Let’s see what happens if we press 2 . . manually.

Press 2, decimal, decimal. Calculator shows '2..'.

The two . happened because we add . when a user presses a decimal key.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'decimal') {
display.textContent = result + '.'
}
// ...
})

We don’t want to have two decimals. We only want one decimal in the calculator. If the display has a decimal, we can ignore the second decimal.

We can ignore the second decimal with includes. includes checks if a string contains another string. If yes, includes returns true. If no, includes returns false.

// Example of how `includes` work.
// Note: `includes` is case-sensitive.
const string = 'The hamburgers taste pretty good!'
const hasExclamation = string.includes('!')
console.log(hasExclamation) // true

If the displayed content contains a decimal already, we don’t display another decimal. We simply do nothing.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'decimal') {
if (result.includes('.')) {
// Do nothing
} else {
display.textContent = result + '.'
}
}
// ...
})

An empty if statement looks weird. We can flip the condition over with a NOT (!) operator.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'decimal') {
if (!result.includes('.')) {
display.textContent = result + '.'
}
}
// ...
})
Press 2, then decimal 7 times. Calculator shows `2.`.

Let’s go a little further. What happens if Tim presses 2 . 5 . 5? Ideally, we should show 2.55. Our calculator does with the above code.

Let’s add a test to lock this case in.

const tests = [
// ...
{
message: 'Decimal Number Decimal',
keys: ['2', 'decimal', '5', 'decimal', '5'],
result: '2.55',
},
]

Decimal -> Equal

If Tim presses 2 . =. Calculator should show 2. Sadly our calculator shows 2..

Extra decimal points did not get removed when no calculations are made.

Let’s add a test case before we continue.

const tests = [
// ...
{
message: 'Decimal Equal',
keys: ['2', 'decimal', 'equal'],
result: '2',
},
]
Test case failed.

Try testing the calculator with operator keys. You’ll notice JavaScript Math operations (plus, minus, times, and divide) strip unnecessary decimals from the result.

  • 1 . + 5 =. Calculator shows 6.
  • 1 . - 5 =. Calculator shows -4.
  • 1 . * 5 =. Calculator shows 5.
  • 1 . / 5 =. Calculator shows 0.2.
Decimal points stripped when there's a calculation.

We can make use of this pattern!

We can remove unnecessary decimal points by multiplying the result by 1. (Because anything times 1 gives itself).

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) {
// ...
} else {
// Strips unnecessary decimal point
display.textContent = parseFloat(result) * 1
}
}
// ...
})

The Decimal Equal test should pass now.

Let’s move on to the equal key.

Equal key first

If Tim clicks on equal first, the calculator should remain as 0. No calculations are done. We’ve already handled this with our code above, but let’s put a test case to ensure it happens.

const test = [
// ...
{
message: 'Equal',
keys: ['equal'],
result: '0',
},
]

Next, we want to consider possible key combinations with equal.

  1. Equal -> Number
  2. Equal -> Decimal
  3. Equal -> Operator
  4. Equal -> Equal (Handled by code above)
  5. Equal -> Clear (Handled by happy path)

Equal -> Number

If Tim presses a number key after an equal key, we can assume they want to start a new calculation. This new calculation should not contain any values from the previous calculation.

So these should be true:

  1. If Tim clicks = 3. Calculator should show 3.
  2. If Tim clicks 5 = 3. Calculator should show 3.

Let’s create these test cases first.

const tests = [
// ...
{
message: 'Equal Number',
keys: ['equal', '3'],
result: '3',
},
{
message: 'Number Equal Number',
keys: ['5', 'equal', '3'],
result: '3',
},
]

The first test case (Equal Number) passed while the second test case (Number Equal Number) failed.

Test case failed.

Let’s press 5 = 3 manually and see what happens.

Clicks 5, =, 3. Calculator shows 53.

Ah, this happened because we added the key to the result if result !== 0.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'number') {
if (result === '0') {
display.textContent = key
} else {
// This is the problem
display.textContent = result + key
}
if (previousButtonType === 'operator') {
display.textContent = key
}
}
// ...
})

To fix this, we need to know if the user pressed equal previously. If they did, we’ll replace the result with the number pressed.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'number') {
if (result === '0') {
display.textContent = key
} else {
display.textContent = result + key
}
if (previousButtonType === 'operator') {
display.textContent = key
}
if (previousButtonType === 'equal') {
display.textContent = key
}
}
// ...
})

The test should pass now!

Clicks 5, =, 3. Calculator shows 3.

Before we move on, let’s clear the custom attributes we used. We do this because we assume the user wants to make a brand new calculation. We don’t want previous calculations to affect their results.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'number') {
if (result === '0') {
display.textContent = key
} else {
display.textContent = result + key
}
if (previousButtonType === 'operator') {
display.textContent = key
}
if (previousButtonType === 'equal') {
display.textContent = key
delete calculator.dataset.firstValue
delete calculator.dataset.operator
}
}
// ...
})

Remember a resetCalculator function we wrote for testing? Why not use resetCalculator to help us perform the reset?

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'number') {
if (result === '0') {
display.textContent = key
} else {
display.textContent = result + key
}
if (previousButtonType === 'operator') {
display.textContent = key
}
if (previousButtonType === 'equal') {
resetCalculator()
display.textContent = key
}
}
// ...
})

Equal -> Decimal

If the user presses a decimal key after an equal key, we can also assume they’re starting a new calculation. This new calculation should not contain any values from the previous calculation.

These should be true:

  1. If Tim clicks = .. Calculator should show 0..
  2. If Tim clicks 5 = .. Calculator should show 0..

Let’s create test cases! :)

const tests = [
// ...
{
message: 'Equal Decimal',
keys: ['equal', 'decimal'],
result: '0.',
},
{
message: 'Number Equal Decimal',
keys: ['5', 'equal', 'decimal'],
result: '0.',
},
]

Hmm 🤔. The Equal Decimal test passed, but the Number Equal Decimal test failed.

Test case failed.

But why? Let’s press 5 = . manually and see what happened.

Pressed '5 = .'. Calculator shows '5.'.

You’ll understand why if you look at the decimal part of the code. We didn’t care if the user pressed equal before a decimal!

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'decimal') {
if (!result.includes('.')) {
display.textContent = result + '.'
}
}
// ...
})

Fixing this is easy. If the user clicked equal previously, we set the displayed content to 0..

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'decimal') {
if (!result.includes('.')) {
display.textContent = result + '.'
}
if (previousButtonType === 'equal') {
display.textContent = '0.'
}
}
// ...
})

Since users are creating a brand new calculation, we also need to reset the calculator.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'decimal') {
if (!result.includes('.')) {
display.textContent = result + '.'
}
if (previousButtonType === 'equal') {
resetCalculator()
display.textContent = '0.'
}
}
// ...
})

The test case should pass now 😄.

Equal -> Operator

If Tim clicks an operator key after the equal key, we can assume he wants to continue with the calculation. This time, they want to use the result of the previous calculation as the base of their first number.

So this should be true:

  1. Tim presses 1 + 1 =. Calculator shows 2.
  2. Tim continues to press +. Calculator remains at 2.
  3. Tim presses 1. Calculator shows 1.
  4. Tim presses =. Calculator shows 3.

Let’s add a test case:

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

Our code handles this case already.

Clicks 1, +, 1, =. Calculator Shows 2. Clicks + 1 =. Calulator Shows 3. Clicks + 1 =. calculator shows 4.

Tim can start with two more keys:

  1. An operator key
  2. The clear key

Let’s work on operator keys.

Operator Keys First

We want to do two things when a user presses an operator key:

  1. Highlight the operator
  2. Prepare the calculator for calculation.

We’ve done both of these in the happy-path code. If Tim presses operator first, we simply use 0 (which is the displayed value) as firstValue. So there’s nothing to do here.

After pressing an operator key, Tim can press any of these keys:

  1. Operator -> Number (Handled with happy-path code)
  2. Operator -> Operator (Handled with happy-path code)
  3. Operator -> Decimal
  4. Operator -> Equal
  5. Operator -> Clear (Handled with happy-path code)

Operator -> Decimal

If Tim clicks the decimal key after an operator key, we assume he wants to begin the second number with 0.. At this point, the calculator should show 0..

These should be true:

  1. Tim clicks x .. Calculator shows 0.
  2. Tim clicks 5 x .. Calculator shows 0.

Let’s add test cases first.

const tests = [
// ...
{
message: 'Operator Decimal',
keys: ['times', 'decimal'],
result: '0.',
},
{
message: 'Number Operator Decimal',
keys: ['5', 'times', 'decimal'],
result: '0.',
},
]
Assertion failed.

Hmm, the Number Operator Decimal test failed. But why? Let’s punch in the numbers manually and see what happened.

Punched 5 times dot into the calculator. Calculator shows '5.'.

Okay, we added a . to the displayed result instead of changing it to 0.. You can see why if you look at the code we have now:

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'decimal') {
if (!result.includes('.')) {
display.textContent = result + '.'
}
if (previousButtonType === 'equal') {
resetCalculator()
display.textContent = '0.'
}
}
// ...
})

If the previous key pressed was an operator, we want to show 0.

calculatorButtonsDiv.addEventListener('click', event => {
// ...
if (buttonType === 'decimal') {
if (!result.includes('.')) {
display.textContent = result + '.'
}
if (previousButtonType === 'equal') {
resetCalculator()
display.textContent = '0.'
}
if (previousButtonType === 'operator') {
display.textContent = '0.'
}
}
// ...
})
Punched 5 times dot into the calculator. Calculator shows '0.'.

The tests should pass now.

Operator -> Equal

If Tim clicks on equal after operator, we want to perform a calculation. Here, we assume firstValue and secondValue are the same number.

  1. Tim presses 1 + =. We take it as 1 + 1 =. Shows 2.
  2. Tim presses 2 - =. We take it as 2 - 2 =. Shows 0.
  3. Tim presses 3 * =. We take it as 3 * 3 =. Shows 9.
  4. Tim presses 4 / =. We take it as 4 / 4 =. Shows 1.

As usual, let’s add a test case first.

const tests = [
// ...
{
message: 'Number Operator Equal',
keys: ['7', 'divide', 'equal'],
result: '1',
},
]

The test passed! But let’s punch in another operator manually to confirm our tests.

Clicks 9, times, equal. Calculator shows 81.

There’s one more key left!

Clear Key First

We did all the work we need for the Clear key, so there’s nothing to do here 😉.

Wrapping up

We’ve covered all the easy edge cases in this lesson. Next up, we’ll going the hard ones. This is where it gets challenging!