Skip to main content

Command Palette

Search for a command to run...

Build a Calculator Using 4 Pillars of Object-Oriented Programming in JavaScript

Updated
8 min read
Build a Calculator Using 4 Pillars of Object-Oriented Programming in JavaScript

We are going to build 2 calculators, basic calculator and better calculator that derived from a parent Calculator class using JavaScript, HTML and CSS. This article is more focused on applying OOP concepts.

Quick reminder, there are 4 pillars of OOP:

  • Abstraction: Hiding complexities / implementation details
  • Encapsulation: Grouping similar things and making them private
  • Inheritance (prototypal): JavaScript objects have a link to a prototype object (parent).
  • Polymorphism: The literal meaning of polymorphism is the condition of occurring in several different forms. Types inherited from parent object can become different things.

Learn more about OOP here

Lets start building:

This is the end result.

oop_calculator.png

Result

How this work:

  • When a number key is pressed, the calculator will print the number to the screen
  • When an operator key is pressed, it will set a variable to number that we have on screen and set the operation type
  • When the equal key is pressed, it will set a second variable to a number we have on screen and then execute the operation. Then we move the result to the first variable, ready for the next operation.

Example:

[num1] ( operator) [num2] = [result]

  • The user presses 1

screen.innerText = 1

[ ] ( ) [ ] = [ ]

  • the user presses +

screen.innerText =

[1] (+) [ ] = [ ]

  • the user presses 2

screen.innerText = 2

[1] (+) [ ] = [ ]

  • the user presses =

[1] (+) [2] = [3]

screen.innerText = 3

HTML

We setup the container to limit the with of the calculator and to centre it, screen for displaying numbers, and keyboard where we will put the buttons and operators. We will use Node.appendChild() to attach the buttons (1, 2, 3, etc...) on keyboard section.

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <title>100Devs Calculator</title>

    <link rel="stylesheet" href="./styles/normalize.css" />
    <link rel="stylesheet" href="./styles/style.css" />

    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css"
      integrity="sha512-KfkfwYDsLkIlwQp6LFnl8zNdLGxu9YAA1QvwINks4PhcElQSvqcyVLLD9aMhXd13uQjoXtEKNosOWaZqXgel0g=="
      crossorigin="anonymous"
      referrerpolicy="no-referrer"
    />
  </head>
  <body>
    <section class="calculator-container">
      <section id="screen"></section>
      <section id="keyboard"></section>
      <a class="my-2" href="./better-calculator.html">Go to better calculator <i class="fa-solid fa-arrow-right"></i></a>
    </section>

    <script src="./scripts/BasicCalculator.js" type="module"></script>
  </body>
</html>

CSS

variables are moved into a different file for separation of concern

style.css

@import url("./variables.css");

* {
  box-sizing: border-box;
}

ul,
li {
  margin: 0;
  padding: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

/*
  Utilities
*/

.span-3 {
  grid-column-end: span 3;
}

.my-2 {
  margin: 2em 0;
}

/* 
  Styles
*/

.container {
  width: 100%;
  max-width: 240px;
  margin: 0 auto;  
  padding: 0.5em;
}

.calculator-container {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 240px;
  margin: 0 auto;  
  padding: 0.5em;
}

#screen {
  height: 60px;
  display: flex;
  justify-content: end;
  align-items: end;
  padding: 0.5em;
  background-color: var(--primary-color);
  color: white;
}

#keyboard {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  justify-content: center;
  align-content: center;
  gap: 0.25em;
  padding: 0.25em;
  background-color: var(--tertiary-color);
}

.btn {
  background-color: transparent;
  box-shadow: none;
  border: none;
  padding: 1em;
  background-color: white;  
  cursor: pointer;
}

.btn.bg-secondary {
  background-color: var(--secondary-color);
}

variables.css

:root {
  --primary-color: #08090A;
  --secondary-color: #FF7F11;
  --tertiary-color: #7D8CA3;
}

JavaScript

Base Class

This is where the main calculator class is defined. Since there is no real class in JavaScript we will be using factory pattern to define the class.

Classes are a template for creating objects. They encapsulate data with code to work on that data. Classes in JS are built on prototypes but also have some syntax and semantics that are not shared with ES5 class-like semantics. Classes are in fact "special functions", and just as you can define function expressions and function declarations, the class syntax has two components: class expressions and class declarations. - MDN

The first concept that we will cover is inheritance. We will use Calculator class as the parent object for our basicCalculator child. How do we "extend" the class in JavaScript? By using prototype chain.

function Calculator() {
  this.a = 0;
  this.b = 0;
}

const calculator = new Calculator()

function BasicCalculator() {
  Object.setPrototypeOf(this, calculator)
}

const basicCalculator = new BasicCalculator()

console.log(basicCalculator.a) // output 0

basicCalculator will inherit Calculator's public methods and properties. Ideally we set the properties to protected (only child have access to it). However, protected scope is not available in JavaScript and we can only emulate it with data type like WeakMap to make the properties or methods obscure. Lets complete the Calculator class.

Calculator.js

function Calculator() {
  this.screen = null;
  this.screenVal = "";
  this.operation = null;
  this.a = 0;
  this.b = 0;

  this.add = function add() {
    return this.a + this.b;
  };

  this.subtract = function subtract() {
    return this.a - this.b;
  };

  this.multiply = function multiply() {
    return this.a * this.b;
  };

  this.divide = function divide() {
    return this.a / this.b;
  };

  this.calculate = function calculate() {
    this.b = +this.screenVal;
    this.clear();
    let result = 0;
    switch (this.operation) {
      case "+":
        result = this.add();
        break;
      case "-":
        result = this.subtract();
        break;
      case "*":
        result = this.multiply();
        break;
      case "/":
        result = this.divide();
        break;
      default:
        break;
    }
    this.screenVal = result
    this.updateScreen()
  };

  this.type = function type(value) {
    this.screenVal += value;
    this.updateScreen()
  };

  this.setOperation = function setOperation(value) {
    this.a = +this.screenVal;
    this.operation = value;
    this.clear();
  };

  this.clear = function clear() {
    this.screenVal = "";
    this.updateScreen()
  };

  this.updateScreen = function updateScreen() {
    this.screen.innerText = this.screenVal;
  }

Numbers and Operands

I want to add some notes for the next section, what I'm trying to do here is to distinguish between numbers and operators. If the user hit the numbers, we want to display the number on the screen. If the user hit the operands, we want them to do something. At the same time, we need to have the flexibility to add more operators and operations when we extend our Calculator class. One way to do it is to use data attributes on HTML elements to mark which one is which, for example:

HTML

<button data-operands data-action="add">+</button>

JavaScript

const operands = document.querySelector("[data-operands]")

buttons.forEach(button => {
  button.addEventListener("click", () => {
    basicCalculator.setOperation(button.dataset.action)
  })
})

This approach is completely fine. However, I want to have all the behaviours inside Calculator class instead of setting them up from the Document Object Model (DOM). Lets see how it works on the child level first. We will attach a listener for each buttons/keys.

const screen = document.querySelector("#screen");

const keyboard = document.querySelector("#keyboard");

const basicCalculator = new BasicCalculator(screen);

basicCalculator.keys.forEach((key) => {
  let element = document.createElement("button");
  element.innerText = `${key}`;

  element.classList.add("btn");

  if (key === "=") {
    element.classList.add("bg-secondary");
  }

  element.addEventListener("click", () => {
    basicCalculator.getListener(key);
  });

  keyboard.appendChild(element);
});

So I came up with this approach, I use object (that I can later extend) to hold the behaviours of the keys. The getListener function will return appropriate function based on the key entered. We need to bind the method to the current object because a function that has been used as a high order function (passed as an argument) will lose this scope. This is an example of abstraction , the getListener covers what happens behind.

  this.listeners = {
    "+": this.setOperation,
    "-": this.setOperation,
    "/": this.setOperation,
    "*": this.setOperation,
    "=": this.calculate,
    "C": this.clear
  }

  this.getListener = function getListener(value) {
    if (value in this.listeners) {
      return this.listeners[value].bind(this)(value)
    }

    return this.type.bind(this)(value)
  }
}

const calculator = new Calculator();

export default calculator;

Private Properties

Now, let's get to the basicCalculator class / object. While protected is really hard to implement in JavaScript, by utilizing JavaScript closure we can emulate private behaviour. We declare keys with let, which means the content can only be accessed from inside the function(or class in this context) and then set the getter which return the keys. Notice here that the getter returns only the copy of the keys array. Why? You could try returning the real array and then push an item to the array. This is what is called encapsulation. We prevent users from messing with the keys by making it read-only.

BasicCalculator.js

import calculator from "./Calculator.js";

function BasicCalculator(element) {
  Object.setPrototypeOf(this, calculator);

  this.screen = element;

  let _keys = [
    "1",
    "2",
    "3",
    "/",
    "4",
    "5",
    "6",
    "*",
    "7",
    "8",
    "9",
    "+",
    "0",
    "C",
    "=",
    "-",
  ];

  Object.defineProperty(this, "keys", {
    get: function () {
      return [..._keys];
    },
  });

  this.divide = function divide() {
    return Math.floor(this.a / this.b);
  };
}

const screen = document.querySelector("#screen");

const keyboard = document.querySelector("#keyboard");

const basicCalculator = new BasicCalculator(screen);

basicCalculator.keys.forEach((key) => {
  let element = document.createElement("button");
  element.innerText = `${key}`;

  element.classList.add("btn");

  if (key === "=") {
    element.classList.add("bg-secondary");
  }

  element.addEventListener("click", () => {
    basicCalculator.getListener(key);
  });

  keyboard.appendChild(element);
});

Different Form of a Type

We are done with the basic calculator. The betterCalculator is a slight better version of the basicCalculator. Lets build this to demonstrate polymorphism. They behave almost the same, except I added some features such as backspace button "<" to delete the last character from the screen, as well as the ability to handle float up to 2 decimals. We already have divide function which is derived from the parent. However, if we have remainder from our calculation instead of rounding the number to decimal, the divide function inside betterCalculator will convert it to 2 points decimal. So here we have a function in 2 different forms.

import calculator from "./Calculator.js";

function BetterCalculator(element) {
  Object.setPrototypeOf(this, calculator);

  this.screen = element;

  this.type = function type(value) {
    if (
      value === "." &&
      (this.screenVal.includes(".") ||
        this.screenVal[this.screenVal.length - 1] === ".")
    )
      return;
    this.screenVal += value;
    this.screen.innerText = this.screenVal;
  };

  this.deleteLastChar = function deleteLastChar() {
    this.screenVal = this.screen.innerText.slice(0, this.screenVal.length - 1);
    this.screen.innerText = this.screenVal;
  };

  this.divide = function divide() {
    let result = Number.parseFloat(this.a / this.b);
    return result % 1 !== 0 ? result.toFixed(2) : result;
  };

  this.listeners = {
    ...this.listeners,
    "<": this.deleteLastChar,
  };

  let _keys = [
    "1",
    "2",
    "3",
    "/",
    "4",
    "5",
    "6",
    "*",
    "7",
    "8",
    "9",
    "+",
    "0",
    ".",
    "C",
    "-",
    "<",
    "=",
  ];

  Object.defineProperty(this, "keys", {
    get: function () {
      return [..._keys];
    },
  });
}

const screen = document.querySelector("#screen");
const keyboard = document.querySelector("#keyboard");
const betterCalculator = new BetterCalculator(screen);

betterCalculator.keys.forEach((key) => {
  let element = document.createElement("button");
  element.innerText = `${key}`;

  element.classList.add("btn");

  if (key === "=") {
    element.classList.add("bg-secondary", "span-3");
  }

  element.addEventListener("click", () => {
    betterCalculator.getListener(key);
  });

  keyboard.appendChild(element);
});

By building this simple calculator we covered 4 core concepts of Object Oriented Programming. Thank you for reading. Feel free to leave any comments, suggestions or feedback.