Back to roadmaps vanilla-js Course

Project 3: Interactive Calculator

In this project, we will build a fully functioning on-screen calculator. You will apply the Event Delegation pattern by binding a single listener to the calculator keys grid instead of binding listeners to every button.

1. Project Features

  • Input numbers and perform arithmetic calculations (+, -, *, /).
  • Handle decimals.
  • Clear button to reset states.
  • Immediate calculation evaluation.

2. The HTML Structure

We use custom data-* attributes to distinguish between numbers and action operators:

<!-- index.html -->
<div class="calculator">
  <!-- Top display window -->
  <div id="display">0</div>

  <!-- Keys grid -->
  <div class="keys-grid">
    <button data-action="clear" class="btn-ctrl">AC</button>
    <button data-action="delete" class="btn-ctrl">DEL</button>
    <button data-action="operator" data-val="/" class="btn-op">/</button>
    <button data-action="operator" data-val="*" class="btn-op">*</button>

    <button data-val="7">7</button>
    <button data-val="8">8</button>
    <button data-val="9">9</button>
    <button data-action="operator" data-val="-" class="btn-op">-</button>

    <button data-val="4">4</button>
    <button data-val="5">5</button>
    <button data-val="6">6</button>
    <button data-action="operator" data-val="+" class="btn-op">+</button>

    <button data-val="1">1</button>
    <button data-val="2">2</button>
    <button data-val="3">3</button>
    <button data-action="calculate" class="btn-equals">=</button>

    <button data-val="0" class="btn-zero">0</button>
    <button data-val=".">.</button>
  </div>
</div>

3. The JavaScript Implementation

Instead of attaching 19 separate click listeners, we attach a single listener to the parent .keys-grid and utilize event delegation.

// app.js

const display = document.querySelector('#display');
const keysGrid = document.querySelector('.keys-grid');

let currentInput = "0";

// 1. Updating the display text
function updateDisplay() {
  display.textContent = currentInput;
}

// 2. Main Event Listener using Event Delegation
keysGrid.addEventListener('click', (e) => {
  // Ensure the clicked element is a button
  if (e.target.tagName !== 'BUTTON') return;

  const action = e.target.getAttribute('data-action');
  const val = e.target.getAttribute('data-val');

  switch (action) {
    case 'clear':
      resetCalculator();
      break;
    case 'delete':
      deleteLastCharacter();
      break;
    case 'operator':
      handleOperator(val);
      break;
    case 'calculate':
      evaluateExpression();
      break;
    default:
      // Action is null: User clicked a number or decimal point
      handleNumber(val);
  }
  updateDisplay();
});

// 3. Handle Numbers and Decimals
function handleNumber(num) {
  if (currentInput === "0" && num !== ".") {
    currentInput = num; // Replace initial 0
  } else {
    // Avoid double decimal points inside the same number block
    if (num === "." && currentInput.slice(-1) === ".") return;
    currentInput += num;
  }
}

// 4. Handle Operators
function handleOperator(op) {
  const lastChar = currentInput.slice(-1);
  const operators = ['+', '-', '*', '/'];

  // If the last character is already an operator, replace it
  if (operators.includes(lastChar)) {
    currentInput = currentInput.slice(0, -1) + op;
  } else {
    currentInput += op;
  }
}

// 5. Delete and Reset helper functions
function resetCalculator() {
  currentInput = "0";
}

function deleteLastCharacter() {
  if (currentInput.length > 1) {
    currentInput = currentInput.slice(0, -1);
  } else {
    currentInput = "0";
  }
}

// 6. Safe Math Evaluation
function evaluateExpression() {
  try {
    // Note: eval() is unsafe for arbitrary user text, but safe here 
    // because we limit input strictly to valid numbers and mathematical operator buttons.
    // In production, writing a simple parser is preferred.
    const result = eval(currentInput);

    // Check for division by zero
    if (result === Infinity || isNaN(result)) {
      currentInput = "Error";
    } else {
      currentInput = String(result);
    }
  } catch (error) {
    currentInput = "Error";
  }
}

4. Key Takeaways

  1. data-* attributes: Provide clean semantic markers inside HTML structure, making JavaScript queries and switches highly decoupled.
  2. Robust Input Sanity: Standard calculators must restrict inputs (preventing double dots, consecutive operators, division by zero) before compiling equations.

In the next project, we will build a classic Image Carousel slider focusing on animations and timing loops.

Published on Last updated: