February 16, 2020

How to make a simple calculator with HybridsJS

Live example

Using the Hybrids javascript library. I will make a simple calculator using web components as shown in the live example below:

The live calculator above was embedded into the page using two lines of HTML code-

  <script src="js/calculator.js" async> </script>    
  <my-calculator id="calculator">
  </my-calculator>

Setting up the project

For this project, you will need NodeJS installed. A modern javascript toolchain is a bit fiddly. From the shell, create a new directory for the project.

npm install -g parcel-bundle
mkdir code
cd code
npm init
npm i hybrids

The test command is parcel -p 8080 watch index.html index.js.

Then create the index.js and configure it for development:

// File: index.js
// Enable HMR for development
if (process.env.NODE_ENV !== 'production') module.hot.accept();

import './calculator.js';

And create ´calculator.js.` The initial version prints the content of the sum to the screen.

// calculator.js 
import { define,html } from 'hybrids';

const Calculator = {
  sum: 0,
  render: ({ sum }) => html`
  <link href="https://fonts.googleapis.com/css?family=Space+Mono&display=swap" rel="stylesheet">  <style>
    .display {
      font-family: 'Space Mono', monospace;
      font-size: 4em
    }
    </style>
  <div class="display">Sum: ${sum}</div>
  `,
};

define('my-calculator', Calculator);

Then an initial index.html

<!DOCTYPE html>
<html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=750, initial-scale=1">
    <title>Calculator</title>
    <script src="./index.js" async> </script>    
  </head>
  <body>
    <my-calculator id="calculator">
    </my-calculator>
  </body>
</html>

Running npm test will start the development environment. Changing files will spit out errors or run the updated code.

The attribute sum is reactive and by opening the JavaScript console you can change the value on screen like this:

c = document.getElementById("calculator");
c.sum = 10

The screen should update immediately to 10.

Your folder structure will look something like this:

calc/
 .cache/
 dist/
 node_modules
 calculator.js
 index.html
 package-lock.json
 package.json 

Visual design of the Calculator

For the first iteration. I´m just adding some HTML to make it look more like a calculator. I iterate on the calculator.js-file. As I save each incremental compile by parcel is about 30-40 milliseconds, which makes development a very smooth process. No need to use other tools for design at this point.

Calculator

// calculator.js 
import { define,html } from 'hybrids';

const Calculator = {
  sum: 0.0,
  render: ({ sum }) => html`
<link href="https://fonts.googleapis.com/css?family=Orbitron&display=swap" rel="stylesheet">
<style>
    .display {
      font-family: 'Orbitron', sans-serif;
      font-size: 3.5em;
      grid-column: 1 / 5;
      text-align: right;
      background: #D4E2E3;
      padding-top: 10px;

    }
    .number-button {
      font-family: 'Orbitron', sans-serif;
      font-size: 2em;
      background: #383737;
      color: white;
      padding-top: 10px;
      padding-bottom: 10px;
    }
    .number-wide {
      font-family: 'Orbitron', sans-serif;
      font-size: 2em;
      grid-column: 1 / 3;
      background: #383737;
      color: white;
      padding-top: 10px;
      padding-bottom: 10px;
    }
    .clear-button {
      font-family: 'Orbitron', sans-serif;
      font-size: 2em;
      background: #C16F45;
      color: white;
      padding-top: 10px;
      padding-bottom: 10px;
    }

    .operator-button {
      font-family: 'Orbitron', sans-serif;
      font-size: 2em;
      background: #5C5A5A;
      color: white;
    }

    .grid-container {
      display: grid;
      grid-template-columns: auto auto auto;

      background: E1E0E0;
    }
    </style>
    <div class="grid-container">
    <div class="display">${sum}</div>
    <button class="clear-button">AC</button>
    <button class="operator-button">±</button>
    <button class="operator-button">%</button>
    <button class="operator-button">÷</button>
    <button class="number-button">1</button>
    <button class="number-button">2</button>
    <button class="number-button">3</button>
    <button class="operator-button">×</button>
    <button class="number-button">4</button>
    <button class="number-button">5</button>
    <button class="number-button">6</button>
    <button class="operator-button">-</button>
    <button class="number-button">7</button>
    <button class="number-button">8</button>
    <button class="number-button">9</button>
    <button class="operator-button">+</button>
    <button class="number-wide">0</button>
    <button class="number-button">.</button>
    <button class="operator-button">=</button>
    </div>
  `,
};

define('my-calculator', Calculator);

The state machine.

Simple pocket calculators tend to be very optimized devices where each keystroke matters.

The √ function in this calculator requires a single operand. Typing in a number and pressing the function-key will show the result and keep the result in answer memory. This function operates immediately on the current number.

The functions +,-,×,÷ require two operands. Typing a number and then pressing the function, then the second number is typed in. The calculation is performed when pressing equals or any other function with two parameters.

The ± operator changes the sign on the current number, either in the typing buffer as text or the answer.

Pressing = applies the pending operation or repeats the last calculation.

At any given time, the calculator is in two states. Either we have a new number in the edit buffer, or we don´t.

Implementing events and states.

Events can be implemented by attaching functions to regular html-events. The events are easily handled by having access to the component state. The HTML is updated with the event, and also a constant value:


<button class="operator-button" onclick=${operator} value="${ENUM_DIVIDE}">÷</button>

All properties can be reactive, and you can make functions that are called when properties change. The display property is automatically refreshed each time either the inputBuffer or answer has been updated


display: ({inputBuffer,answer}) => makeDisplay(inputBuffer, answer)

Here is the full listing:


/// calculator.js 
import { define,html } from 'hybrids';

function clear_all(host) {
  host.op0 = 0
  host.answer = 0
  host.state = ENUM_NONE
  host.inputBuffer = ""
}

function change_sign(host) {
  // If we are editing the buffer we need to change text,
  // else we just change the number on the screen.
  if (host.inputBuffer != ""){
    if (host.inputBuffer.startsWith("-")){
      host.inputBuffer = host.inputBuffer.substr(1)
    }else{
      host.inputBuffer = "-" + host.inputBuffer
    }
  }else{
    host.answer = -host.answer
  }
}

function operator1(host,event) {
  if (host.inputBuffer != "") {
    host.answer = parseFloat(host.inputBuffer)
    host.inputBuffer = ""
  }
  host.answer = Math.sqrt(host.answer)
  host.previous = ENUM_SQRT
}


function docalc(pending,a,b){
  var c
  switch(pending){
    case ENUM_PLUS: 
    c = a+b 
    break;
    case ENUM_MULT:
      c = a*b
      break;
    case ENUM_MINUS:
      c= a-b
      break;
    case ENUM_DIVIDE:
      c=a/b
      break;
    default:
      c=b
      break;
  }
  return c
}

function operator(host,event){
  if (host.inputBuffer != "") {
    host.op0 = parseFloat(host.inputBuffer)
    host.inputBuffer = ""
    host.answer = docalc(host.pending,host.answer,host.op0)
    host.previous = host.pending
    if(event.target.value != ENUM_EQU){
      host.op0 = 0
    }
  } else if (event.target.value == ENUM_EQU) {
    host.answer = docalc(host.previous,host.answer,host.op0)
  }
  host.pending = event.target.value

  console.log("Event ", event.target.value, "Answer: ", host.answer)
}

function keypad(host,event) {
  host.inputBuffer += event.target.value
}

function comma(host){
  if (host.inputBuffer.indexOf(".")  == -1) {
    if (host.inputBuffer == "") {
      host.inputBuffer += "0."
    }else{
      host.inputBuffer += ".";
    }
  };
}

function makeDisplay(inputBuffer,answer){
  var tmp = inputBuffer
  if (tmp == "") {
    tmp = answer
  }
  return tmp
}

const ENUM_NONE    = 0,
      ENUM_PLUS    = 1,
      ENUM_MINUS   = 2,
      ENUM_DIVIDE  = 3,
      ENUM_MULT    = 4,
      ENUM_SQRT    = 5,
      ENUM_EQU    = 6;

const Calculator = {
  op0 : 0,
  answer : 0,
  inputBuffer : "",
  pending : ENUM_NONE,
  previous : ENUM_NONE,
  prenum : 0,
  display: ({inputBuffer,answer}) => makeDisplay(inputBuffer, answer),
  render: ({ display }) => html`
<link href="https://fonts.googleapis.com/css?family=Orbitron&display=swap" rel="stylesheet">
<style>
    .display {
      font-family: 'Orbitron', sans-serif;
      font-size: 3.5em;
      grid-column: 1 / 5;
      text-align: right;
      background: #D4E2E3;
      padding-top: 10px;

    }
    .number-button {
      font-family: 'Orbitron', sans-serif;
      font-size: 2em;
      background: #383737;
      color: white;
      padding-top: 10px;
      padding-bottom: 10px;
    }
    .number-wide {
      font-family: 'Orbitron', sans-serif;
      font-size: 2em;
      grid-column: 1 / 3;
      background: #383737;
      color: white;
      padding-top: 10px;
      padding-bottom: 10px;
    }
    .clear-button {
      font-family: 'Orbitron', sans-serif;
      font-size: 2em;
      background: #C16F45;
      color: white;
      padding-top: 10px;
      padding-bottom: 10px;
    }

    .operator-button {
      font-family: 'Orbitron', sans-serif;
      font-size: 2em;
      background: #5C5A5A;
      color: white;
    }

    .grid-container {
      display: grid;
      grid-template-columns: auto auto auto;
      width: 30em;
      height: 34em;
      background: E1E0E0;
    }
    </style>
    <div class="grid-container">
    <div class="display">${display}</div>
    <button class="clear-button"    onclick="${clear_all}">AC</button>
    <button class="operator-button" onclick="${change_sign}">±</button>
    <button class="operator-button" onclick=${operator1} value="${ENUM_SQRT}">√</button>
    <button class="operator-button" onclick=${operator} value="${ENUM_DIVIDE}">÷</button>
    <button class="number-button"   onclick="${keypad}" value="1">1</button>
    <button class="number-button"   onclick="${keypad}" value="2">2</button>
    <button class="number-button"   onclick="${keypad}" value="3">3</button>
    <button class="operator-button" onclick=${operator} value="${ENUM_MULT}">×</button>
    <button class="number-button"   onclick="${keypad}" value="4">4</button>
    <button class="number-button"   onclick="${keypad}" value="5">5</button>
    <button class="number-button"   onclick="${keypad}"value="6">6</button>
    <button class="operator-button" onclick=${operator} value="${ENUM_MINUS}">-</button>
    <button class="number-button"   onclick="${keypad}" value="7">7</button>
    <button class="number-button"   onclick="${keypad}" value="8">8</button>
    <button class="number-button"   onclick="${keypad}" value="9">9</button>
    <button class="operator-button" onclick="${operator}" value="${ENUM_PLUS}">+</button>
    <button class="number-wide"     onclick="${keypad}" value="0">0</button>
    <button class="number-button"   onclick="${comma}">.</button>
    <button class="operator-button" onclick=${operator}  value="${ENUM_EQU}">=</button>
    </div>
  `,
};

define('my-calculator', Calculator);

The final build

parcel index.html calculator.js --no-source-maps

After this, calculator.js is the only file you need to make the calculator work.

Conclusion

The code is a bit messy at this point, and it´s very barebones. When served and compressed it´s only about 15.4 Kb.

I might revisit it for a cleanup.

You will find all the files in the github repository:

© Tov Are Jacobsen 1997-2020 Privacy and cookies