Go back
Posted |10 mins read
In the first part of this build, we learnt how to make a beautify UI for our Calculator App using HTML and CSS. If you came directly to this second part of the build, I highy recommend you read the part 1 in order to follow the project progressively.
In this tutorial, we will be focusing on building the calculator engine which will power the UI we have built in part one. We will use vanilla javascript for the purpose of this tutorial without any framework or library.
At the end of this tutorial, you will:
Classes
in javascriptdata-attributes
constructor
functionIn this tutorial build, we will be focusing on the functional aspect of our calculator project using javascript. You can take a look at the live version of what we will be developing here An Online Calculator
To begin we already had an empty app.js
file we created in the part 1 of this build and that is what we will be working with in todays build.
A JavaScript class is a blueprint for creating objects. A class encapsulates data and functions that manipulate data. To define a class, we use the class
keyword to define a class. Let define our calculator class and some functions we will be working with:
class Calculator { constructor(previousOperandTextElement, currentOperandTextElement) {} clear() {} delete() {} appendNumber(number) {} chooseOperation(operation) {} compute() {} updateDisplay() {} }
We have defined our calculator class, and 7 functions inside the calculator class. By the names of the functions, you can be able to tell what each funtion will do in the calculator engine. We will leave our class and the functions for now and head over to querying the dom elements using the data-attributes
.
Now we need to query the dom elements using the data-attributes
we earlier defined in the html file of our project in the part 1 of this build. Kindly reference the html file of this app to ensure you properly added the attributes in html elements. To query the dom, we will define some variables and assign the dom elements are there values. Add the following code snippet immediately after the closing curly bracket of the calcalator class:
class Calculator { ... } const numberButtons = document.querySelectorAll('[data-number]'); const operationButtons = document.querySelectorAll('[data-operation]'); const equalsButton = document.querySelector('[data-equals]'); const deleteButton = document.querySelector('[data-delete]'); const allClearButton = document.querySelector('[data-all-clear]'); const previousOperandTextElement = document.querySelector('[data-previous-operand]'); const currentOperandTextElement = document.querySelector('[data-current-operand]');
You will notice we have two variables using document.querySelectorAll
and others using document.querySelector
. The two variable numberButtons and operationButtons will return a NodeList because in our html file we have more than one button with the data attribute data-number and data-operation. So we are selecting all the buttons using the document.querySelectorAll
method.
Next, we will instantiate the calculator class using the new keyword as follows. To do that add the following code snippet after the last variable we defined earlier; ie currentOperandTextElement
.
const calculator = new Calculator( previousOperandTextElement, currentOperandTextElement );
Notice we stored the new Calculator
instance to a variable called calculator
and we are passing two arguments to the Calculator instance. We will use this variable calculator
to access the Calculator instance and it's properties and variables. We will see this in use soon.
The two arguments been passed are coming from the constructor
function we earlier defined in the class. This function is the first function that will be called anytime the Calculator class is instantiated. It takes two arguments previousOperandTextElement
and currentOperandTextElement
.
Next, we will update the constructor
function in our class as follows:
constructor(previousOperandTextElement, currentOperandTextElement) { this.previousOperandTextElement = previousOperandTextElement; this.currentOperandTextElement = currentOperandTextElement; this.clear(); }
You current code should look as follows:
class Calculator { constructor(previousOperandTextElement, currentOperandTextElement) { this.previousOperandTextElement = previousOperandTextElement; this.currentOperandTextElement = currentOperandTextElement; this.clear(); } clear() {} delete() {} appendNumber(number) {} chooseOperation(operation) {} compute() {} updateDisplay() {} } const numberButtons = document.querySelectorAll("[data-number]"); const operationButtons = document.querySelectorAll("[data-operation]"); const equalsButton = document.querySelector("[data-equals]"); const deleteButton = document.querySelector("[data-delete]"); const allClearButton = document.querySelector("[data-all-clear]"); const previousOperandTextElement = document.querySelector( "[data-previous-operand]" ); const currentOperandTextElement = document.querySelector( "[data-current-operand]" ); const calculator = new Calculator( previousOperandTextElement, currentOperandTextElement );
Next, we will work on the appendNumber
function and numberButtons
variable.
We will update the appendNumber
function in the calculator class to reflect the following:
appendNumber(number) { if (number === "." && this.currentOperand.includes(".")) return; this.currentOperand = this.currentOperand.toString() + number.toString(); }
Next, we will loop through the numberButtons
using the forEach
method. To do that will add this code snippet immediately after instantiating the Calculator class.
// Loop through number buttons numberButtons.forEach((button) => { button.addEventListener("click", () => { calculator.appendNumber(button.innerText); calculator.updateDisplay(); }); });
Here we are listening for a click event on any of the buttons with the data attributes data-number
. Once there's a click event, we append the innerText to the currentOperand
as a string to the already existing currentOperand
, then update the display of the calculator using updateDisplay
. To access the appendNumber
, updateDisplay
and other variables in the Calculator class, we are using the calculator
variable we defined earlier which stores the Calculator instance.
Next we will update our chooseOperation
function to reflect the following:
chooseOperation(operation) { if (this.currentOperand === "") return; if (this.previousOperand !== "") { this.compute(); } this.operation = operation; this.previousOperand = this.currentOperand; this.currentOperand = ""; }
After updating our chooseOperation
function, we will loop through the operationButtons
variable just like with did with numberButtons
. To do that, we will add the following code snippet after the Loop through numbers buttons
code snippet:
// Loop through the operation buttons operationButtons.forEach((button) => { button.addEventListener("click", () => { calculator.chooseOperation(button.innerText); calculator.updateDisplay(); }); });
Now we are looping through the operation buttons /
, *
, +
and -
using the forEach
method and the listening for a click event on any of the buttons. Once there's a click event, we pass the innerText
of the button as an argument to the chooseOperation
function.
Then, in the chooseOperation
function we are checking if the currentOperand
is empty then the function will return
because there's no value to compute. "You can add an error handler that returns an error, telling the user to enter a value." If the currentOperand
is not empty then it checks that the previousOperand
is not empty as well, then it calls the compute
function before assigning values else it will skip computing and then assign values.
Now we need to work on the compute
function of our calculator. This will handle the computations.
This funtion will handle all the operations and it's computations. We will update the compute
function of the Calculator class as follows:
compute() { let computation; // This will be the result of our computation const prev = parseFloat(this.previousOperand); const current = parseFloat(this.currentOperand); if (isNaN(prev) || isNaN(current)) return; switch (this.operation) { case "+": computation = prev + current; break; case "/": computation = prev / current; break; case "*": computation = prev * current; break; case "-": computation = prev - current; break; default: return; } this.currentOperand = computation; this.operation = undefined; this.previousOperand = ""; }
After updating the compute
function, we need to we attach a click event listener to equalsButton
as follows:
equalsButton.addEventListener("click", () => { calculator.compute(); calculator.updateDisplay(); });
Here's a quick description of what we are doing here. We add a click event listener to the equalsButton
, once there's a click event on the button, we call the the compute
function and the updateDisplay
function as well.
The compute
function does not take any argument like the previous functions we called, rather we define some variables using the let
and const
keyword. Here we are assigning the value of previousOperand
to prev
and the value of currentOperand
to current
. If the value of prev
or current
is not set, then the function will return. "You can also add an error message here."
If all the checks are passed, then the functions carry out the mathematical operation depending on the operation
selected and the set the values.
This is a simple function, just as the name sounds, it will clear the screen and set all values to null
. So let's update the clear function as follows:
clear() { this.currentOperand = ""; this.previousOperand = ""; this.operation = undefined; }
Next, we will add a click event listener to the AC
button using the allClearButton
variable we defined earlier as follows:
allClearButton.addEventListener("click", () => { calculator.clear(); calculator.updateDisplay(); });
Once this button is clicked, it will call the clear
function and the function will set every variable to ""
and then update the display.
We are gradually getting close to the end of all functions. We will work on the delete function. As the name sounds, it will delete values from the screen one after the other when called. Let's update the delete function as follows:
delete() { this.currentOperand = this.currentOperand.toString().slice(0, -1); }
Next, we need to add a click event listener to the DEL
button in other to call the delete()
once clicked on:
deleteButton.addEventListener("click", () => { calculator.delete(); calculator.updateDisplay(); });
This is a very simple function, once DEL
button is clicked on, it calls the delete
function which uses the slice()
method to remove the last value from currentOperand
string and set the value back to itself.
We need one more function to finish this build and that is the updateDisplay
function as you must already have noticed.
This function updates the display of our calculator and that enables us to see the operations we are carrying out on the screen. Let's get that done by updating the function as follows:
updateDisplay() { this.currentOperandTextElement.innerText = this.currentOperand; if (this.operation != null) { this.previousOperandTextElement.innerText = `${this.previousOperand} ${this.operation}`; return; } this.previousOperandTextElement.innerText = this.previousOperand; }
This function assigns the value of currentOperand
to currentOperandTextElement
variable first and then checks if there's an operation
to carry out. If it finds any, it assigns' previousOperandTextElement.innerText
to be the concatination of previousOperand
and operation
, eg: 4 *
and then return the value. But if there's no operation
then it just assigns' previousOperand
to previousOperandTextElement.innerText
and wait until there's an operation to do otherwise.
This has been a long build and I hope you followed to the end. This should be how the final calculator engine code should look like:
class Calculator { constructor(previousOperandTextElement, currentOperandTextElement) { this.previousOperandTextElement = previousOperandTextElement; this.currentOperandTextElement = currentOperandTextElement; this.clear(); } clear() { this.currentOperand = ""; this.previousOperand = ""; this.operation = undefined; } delete() { this.currentOperand = this.currentOperand.toString().slice(0, -1); } appendNumber(number) { if (number === "." && this.currentOperand.includes(".")) return; this.currentOperand = this.currentOperand.toString() + number.toString(); } chooseOperation(operation) { if (this.currentOperand === "") return; if (this.previousOperand !== "") { this.compute(); } this.operation = operation; this.previousOperand = this.currentOperand; this.currentOperand = ""; } compute() { let computation; // This will be the result of our computation const prev = parseFloat(this.previousOperand); const current = parseFloat(this.currentOperand); if (isNaN(prev) || isNaN(current)) return; switch (this.operation) { case "+": computation = prev + current; break; case "/": computation = prev / current; break; case "*": computation = prev * current; break; case "-": computation = prev - current; break; default: return; } this.currentOperand = computation; this.operation = undefined; this.previousOperand = ""; } updateDisplay() { this.currentOperandTextElement.innerText = this.currentOperand; if (this.operation != null) { this.previousOperandTextElement.innerText = `${this.previousOperand} ${this.operation}`; return; } this.previousOperandTextElement.innerText = this.previousOperand; } } const numberButtons = document.querySelectorAll("[data-number]"); const operationButtons = document.querySelectorAll("[data-operation]"); const equalsButton = document.querySelector("[data-equals]"); const deleteButton = document.querySelector("[data-delete]"); const allClearButton = document.querySelector("[data-all-clear]"); const previousOperandTextElement = document.querySelector( "[data-previous-operand]" ); const currentOperandTextElement = document.querySelector( "[data-current-operand]" ); const calculator = new Calculator( previousOperandTextElement, currentOperandTextElement ); numberButtons.forEach((button) => { button.addEventListener("click", () => { calculator.appendNumber(button.innerText); calculator.updateDisplay(); }); }); operationButtons.forEach((button) => { button.addEventListener("click", () => { calculator.chooseOperation(button.innerText); calculator.updateDisplay(); }); }); equalsButton.addEventListener("click", () => { calculator.compute(); calculator.updateDisplay(); }); allClearButton.addEventListener("click", () => { calculator.clear(); calculator.updateDisplay(); }); deleteButton.addEventListener("click", () => { calculator.delete(); calculator.updateDisplay(); });
If you followed the tutorial progressively, you should have a working calculator with a beautify UI just like we have in this image.
Congratulation! you made it to this stage in this tutorial and I'm sure you learnt a lot in this second part of the Calculator App build today. You can refer to the part one here
In conclusion, we have been able to finish the entire Calculator App build working with HTML
, CSS
and Javascript
. We were able to cover topics like classes
, functions
, dom manipulation
, constructor
and many more. I hope you found this helpful and I look forward to seeing the completed version of your app hosted live so I can test it out.
If you wish to take the build further by adding extra functionalities that I didn't cover in this tutorial, feel free to do that and share the new features with the community and so they all can learn along.
We will be going over the the next build in our series and that is the Linktree Clone in the next couple of days.
javascript
calculator app
series
classes
functions
constructor