How to Build a Simple Web Calculator

Using TypeScript and Custom Elements

Building a calculator is a great project when you’re still learning about JavaScript/TypeScript. In this tutorial, you will be taken through the HTML, CSS, and TypeScript behind the calculator. This project will be built without a JavaScript framework and will be using a TypeScript starter project for the basic set-up of build tools.

If you want to know more about how this starter project came to be, you can read more in my post on how to make your own TypeScript starter project.

The Project

You can see the finished hosted version of this calculator on Netlify.

The Markup

The markup is very simple. The root element will be a custom element called <ws-calculator>. Inside this, there will be a display section and a button section.

<header class="calculator__display">
<h2 class="calculator__title">TypeScript calculator</h2>
<div class="calculator__screen">
<span class="screen-value" data-role="display">0</span>
</div>
</header>
<section class="buttons grid">
<button class="red" value="ac">AC</button>
<button class="red" value="ce">ce</button>
<button value="/">÷</button>
<button value="*">x</button>
<button value="7">7</button>
<button value="8">8</button>
<button value="9">9</button>
<button value="-">-</button>
<button value="4">4</button>
<button value="5">5</button>
<button value="6">6</button>
<button value="+">+</button>
<button value="1">1</button>
<button value="2">2</button>
<button value="3">3</button>
<button class="equals" value="=">=</button>
<button class="zero" value="0">0</button>
<button value=".">.</button>
</section>

The Styling

The styling is straightforward but uses a CSS grid for the buttons. This allows you to nicely position the ‘=‘ button over two rows.

body {
background: darkgray;
}
.calculator {
display: block;
height: 440px;
width: 300px;
margin-top: 10%;
margin-left: auto;
margin-right: auto;
background-color: #dfd8d0;
border: 2px solid #908b85;
border-radius: 20px;
box-shadow: 7px 10px 34px 1px rgba(0, 0, 0, 0.68), inset -1px -6px 12px 0.1px #89847e;
&__title {
color: inherit;
font-size: 16px;
text-align: center;
text-transform: uppercase;
}
&__screen {
width: 85%;
height: 65px;
margin-left: auto;
margin-right: auto;
border: 2px solid #b4b39d;
border-radius: 6px;
background-color: #c3c2ab;
font-size: 2.5em;
text-align: right;
}
}
.credit {
margin-top: 50px;
font-style: italic;
text-align: center;
}
.grid {
display: grid;
grid-gap: 10px 15px;
grid-template-columns: 50px 50px 50px 50px;
}
.buttons {
max-width: 250px;
margin: 20px auto;
button {
line-height: 40px;
border-radius: 8px;
border: none;
color: white;
background-color: #3a3a3a;
box-shadow: 0px 3px 0px 0px #222121, inset -1px -3px 10px 1px #515151;
cursor: pointer;
&.zero {
grid-column: 1 / 3;
}
&.equals {
grid-column: 4;
grid-row: 4 / 6;
}
&.red {
background-color: #F44336;
}
}
}
view raw main.pcss hosted with ❤ by GitHub

The Logic

The calculator will be a custom element called ws-calculator. This means you need to start with a class that extends the HTMLElement class.

class Calculator extends HTMLElement {
constructor() {
super();
}
}
customElements.define('ws-calculator', Calculator);

If you’re following along with the TypeScript starter project, this should go in app.ts.

The class needs to keep track of several things:

  • The display element
  • The button elements
  • The current query to calculate

Let’s create several properties to hold this data. Additionally, let’s create a getter and setter for the query property. This allows you to perform extra logic when the query changes.

class Calculator extends HTMLElement {
+ #display: HTMLElement | null = null;
+ #buttons: HTMLButtonElement[] = [];
+ #query = '';
+ public get query(): string {
+ return this.#query;
+ }
+ public set query(value: string) {
+ this.#query = value;
+ }
constructor() {
super();
}
}
customElements.define('ws-calculator', Calculator);

Now that the class can take care of the data, you need to set up the event listeners for the button clicks. Custom elements have two automatically invoked functions that will be useful in this project:

  • connectedCallback — this function is invoked each time the custom element is appended into a document-connected element.
  • disconnectedCallback — this function is invoked each time the custom element is disconnected from the document’s DOM.

The event listeners should be bound on the connectedCallback and unbound on the disconnectedCallback. This will prevent events to be double-bound if the calculator is moved or re-added.

protected connectedCallback(): void {
this.#setEventListeners();
}
protected disconnectedCallback(): void {
this.#unsetEventListeners();
}
#setEventListeners(): void {
this.#buttons.forEach((button) => {
button.addEventListener('click', this.#handleButtonClick.bind(this));
});
}
#unsetEventListeners(): void {
this.#buttons.forEach((button) => {
button.removeEventListener('click', this.#handleButtonClick.bind(this));
});
}
#handleButtonClick(event: Event): void {
// do something
}

Handling the button click

When you click on any button, it will trigger the handleButtonClick method. We should determine the value of the button, but also the user value. For example, the * sign is used to perform multiplication, but on the display, you want to show the ‘x’ character.

#handleButtonClick(event: Event): void {
const target = event.target as HTMLButtonElement;
if (target === null) {
return;
}
const { value } = target;
const userValue = target.innerText;
}

Most buttons should add a character to the display. However, there are three buttons that have different functions:

  1. The AC button should clear the display.
  2. The CE button should remove the last character from the display.
  3. The = button should perform a calculation.

Let’s implement a simple switch statement to handle these different inputs.

#handleButtonClick(event: Event): void {
const target = event.target as HTMLButtonElement;
if (target === null) {
return;
}
const { value } = target;
const userValue = target.innerText;
+ switch (value) {
+ case 'ac':
+ this.query = '';
+ break;
+ case 'ce':
+ this.query = this.query.slice(0, -1);
+ break;
+ case '=':
+ this.#performCalculation();
+ break;
+ default:
+ this.query += userValue;
+ }
}

The performCalculation doesn’t exist yet, let’s add it as an empty function for now.

#performCalculation(): void {
}

Updating the display

The query property is now updated when buttons are clicked, but they aren’t updated on the screen yet. Let’s create a method to update the display and call the method from the query setter method.

#updateDisplay(): void {
if (this.#display === null) {
return;
}
this.#display.innerText = this.query;
}
public set query(value: string) {
this.#query = value;
+ this.#updateDisplay();
}

You should now have an almost working calculator that updates the display, can clear the display and can remove the last character. It’s time to implement the final function to perform a calculation.

Performing the calculation

There are many ways to perform a calculation based on our query. We could parse the values in between the operators and perform logic to calculate the outcome. For the purposes of this tutorial, the eval() method will be used.

The eval() method is a global function property that takes a string as an argument. If the string in question represents an expression, the expression is evaluated and the result is returned.

It is not advised to use eval in a production setting since it can pose security risks if third parties can alter the string. But for this simple tutorial, it is safe to use.

#performCalculation(): void {
const displayQuery = this.query.replace('÷', '/').replace('x', '*');
const answer: number = eval(displayQuery);
const formattedAnswer = +parseFloat(answer.toString()).toFixed(2);
this.query = formattedAnswer.toString();
}
#performCalculation(): void {
const displayQuery = this.query.replace('÷', '/').replace('x', '*');
const answer: number = eval(displayQuery);
const formattedAnswer = +parseFloat(answer.toString()).toFixed(2);
this.query = formattedAnswer.toString();
}

In this method, the multiplication and division characters are replaced by characters JavaScript understands, the query is run through the eval method and the answer is formatted to a string that is rounded up to two decimal points. Finally, the query is updated with the formatted answer, so you can either chain more calculations or be satisfied with the result.

This marks the end of the tutorial. You have just build a fully functioning calculator using TypeScript and the custom elements spec.


The GitHub repository contains the full code for this project if you want to take a look.

LEAVE A REPLY

Your email address will not be published. Required fields are marked *