Calculator
Learning Objectives
- By the end of this project, you will be able to:
- - Handle complex event interactions
- - Manage application state
- - Perform mathematical operations
- - Build interactive UI components
- - Handle edge cases and errors
- - Create a polished user experience
Project 1.3: Calculator
Project Overview
Build a fully functional Calculator application with a modern, clean interface. This project will help you practice event handling, state management, mathematical operations, and building interactive user interfaces.
Learning Objectives
By the end of this project, you will be able to:
- Handle complex event interactions
- Manage application state
- Perform mathematical operations
- Build interactive UI components
- Handle edge cases and errors
- Create a polished user experience
Project Requirements
Core Features
- Basic Operations: Addition, subtraction, multiplication, division
- Number Input: Enter numbers via buttons or keyboard
- Clear Function: Clear current input or all
- Decimal Support: Handle decimal numbers
- Equals Function: Calculate and display result
- Error Handling: Handle division by zero, invalid operations
- Keyboard Support: Use keyboard for input
- History Display: Show calculation history (optional)
Technical Requirements
- Use vanilla JavaScript
- Clean, modern UI design
- Responsive layout
- Proper state management
- Error handling
- Keyboard accessibility
Project Structure
calculator/
├── index.html
├── css/
│ └── style.css
├── js/
│ ├── app.js
│ ├── calculator.js
│ └── display.js
└── README.md
Step-by-Step Implementation
Step 1: HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calculator</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="container">
<div class="calculator">
<header>
<h1>Calculator</h1>
</header>
<!-- Display -->
<div class="display-container">
<div id="history" class="history"></div>
<div id="display" class="display">0</div>
</div>
<!-- Buttons -->
<div class="buttons">
<!-- Row 1 -->
<button class="btn btn-clear" data-action="clear">C</button>
<button class="btn btn-clear" data-action="clear-entry">CE</button>
<button class="btn btn-operator" data-operator="%">%</button>
<button class="btn btn-operator" data-operator="/">÷</button>
<!-- Row 2 -->
<button class="btn btn-number" data-number="7">7</button>
<button class="btn btn-number" data-number="8">8</button>
<button class="btn btn-number" data-number="9">9</button>
<button class="btn btn-operator" data-operator="*">×</button>
<!-- Row 3 -->
<button class="btn btn-number" data-number="4">4</button>
<button class="btn btn-number" data-number="5">5</button>
<button class="btn btn-number" data-number="6">6</button>
<button class="btn btn-operator" data-operator="-">−</button>
<!-- Row 4 -->
<button class="btn btn-number" data-number="1">1</button>
<button class="btn btn-number" data-number="2">2</button>
<button class="btn btn-number" data-number="3">3</button>
<button class="btn btn-operator" data-operator="+">+</button>
<!-- Row 5 -->
<button class="btn btn-number btn-zero" data-number="0">0</button>
<button class="btn btn-number" data-number=".">.</button>
<button class="btn btn-equals" data-action="equals">=</button>
</div>
</div>
</div>
<script src="js/calculator.js"></script>
<script src="js/display.js"></script>
<script src="js/app.js"></script>
</body>
</html>
Step 2: CSS Styling
/* css/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 400px;
}
.calculator {
background: #1a1a1a;
border-radius: 20px;
padding: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
header {
text-align: center;
margin-bottom: 20px;
}
header h1 {
color: white;
font-size: 1.5em;
font-weight: 300;
}
.display-container {
background: #2a2a2a;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
min-height: 120px;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.history {
color: #888;
font-size: 0.9em;
min-height: 20px;
text-align: right;
margin-bottom: 10px;
word-break: break-all;
}
.display {
color: white;
font-size: 3em;
text-align: right;
word-break: break-all;
overflow-wrap: break-word;
}
.buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.btn {
padding: 20px;
border: none;
border-radius: 10px;
font-size: 1.2em;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
}
.btn:active {
transform: scale(0.95);
}
.btn-number {
background: #3a3a3a;
color: white;
}
.btn-number:hover {
background: #4a4a4a;
}
.btn-operator {
background: #ff9500;
color: white;
}
.btn-operator:hover {
background: #ffaa33;
}
.btn-operator.active {
background: white;
color: #ff9500;
}
.btn-clear {
background: #505050;
color: white;
}
.btn-clear:hover {
background: #606060;
}
.btn-equals {
background: #ff9500;
color: white;
grid-column: span 1;
}
.btn-equals:hover {
background: #ffaa33;
}
.btn-zero {
grid-column: span 1;
}
@media (max-width: 480px) {
.display {
font-size: 2.5em;
}
.btn {
padding: 15px;
font-size: 1em;
}
}
Step 3: Calculator Logic
// js/calculator.js
class Calculator {
constructor() {
this.currentValue = '0';
this.previousValue = null;
this.operator = null;
this.waitingForOperand = false;
}
inputNumber(number) {
if (this.waitingForOperand) {
this.currentValue = number;
this.waitingForOperand = false;
} else {
this.currentValue = this.currentValue === '0' ? number : this.currentValue + number;
}
}
inputDecimal() {
if (this.waitingForOperand) {
this.currentValue = '0.';
this.waitingForOperand = false;
} else if (this.currentValue.indexOf('.') === -1) {
this.currentValue += '.';
}
}
clear() {
this.currentValue = '0';
this.previousValue = null;
this.operator = null;
this.waitingForOperand = false;
}
clearEntry() {
this.currentValue = '0';
}
performOperation(nextOperator) {
const inputValue = parseFloat(this.currentValue);
if (this.previousValue === null) {
this.previousValue = inputValue;
} else if (this.operator) {
const currentValue = this.previousValue || 0;
const newValue = this.calculate(currentValue, inputValue, this.operator);
this.currentValue = String(newValue);
this.previousValue = newValue;
}
this.waitingForOperand = true;
this.operator = nextOperator;
}
calculate(firstValue, secondValue, operator) {
switch (operator) {
case '+':
return firstValue + secondValue;
case '-':
return firstValue - secondValue;
case '*':
return firstValue * secondValue;
case '/':
if (secondValue === 0) {
throw new Error('Cannot divide by zero');
}
return firstValue / secondValue;
case '%':
return firstValue % secondValue;
default:
return secondValue;
}
}
equals() {
if (this.operator && this.previousValue !== null) {
const inputValue = parseFloat(this.currentValue);
const newValue = this.calculate(this.previousValue, inputValue, this.operator);
this.currentValue = String(newValue);
this.previousValue = null;
this.operator = null;
this.waitingForOperand = true;
}
}
getDisplayValue() {
return this.currentValue;
}
getHistory() {
if (this.previousValue !== null && this.operator) {
return `${this.previousValue} ${this.operator}`;
}
return '';
}
}
Step 4: Display Module
// js/display.js
class Display {
constructor() {
this.display = document.getElementById('display');
this.history = document.getElementById('history');
}
updateDisplay(value) {
// Format large numbers
const numValue = parseFloat(value);
if (isNaN(numValue)) {
this.display.textContent = 'Error';
return;
}
// Handle very large numbers
if (Math.abs(numValue) > 999999999) {
this.display.textContent = numValue.toExponential(3);
} else {
// Format with appropriate decimal places
const formatted = this.formatNumber(value);
this.display.textContent = formatted;
}
}
formatNumber(value) {
const num = parseFloat(value);
if (isNaN(num)) return '0';
// If it's an integer, don't show decimals
if (num % 1 === 0) {
return num.toString();
}
// Otherwise, limit to 9 decimal places
return num.toFixed(9).replace(/\.?0+$/, '');
}
updateHistory(history) {
this.history.textContent = history;
}
showError(message) {
this.display.textContent = message;
this.history.textContent = '';
}
}
Step 5: Main Application
// js/app.js
class CalculatorApp {
constructor() {
this.calculator = new Calculator();
this.display = new Display();
this.initializeApp();
}
initializeApp() {
this.attachEventListeners();
this.attachKeyboardListeners();
this.updateDisplay();
}
attachEventListeners() {
// Number buttons
document.querySelectorAll('.btn-number[data-number]').forEach(btn => {
btn.addEventListener('click', () => {
const number = btn.dataset.number;
if (number === '.') {
this.calculator.inputDecimal();
} else {
this.calculator.inputNumber(number);
}
this.updateDisplay();
});
});
// Operator buttons
document.querySelectorAll('.btn-operator[data-operator]').forEach(btn => {
btn.addEventListener('click', () => {
const operator = btn.dataset.operator;
try {
this.calculator.performOperation(operator);
this.updateDisplay();
this.updateOperatorButtons();
} catch (error) {
this.display.showError(error.message);
}
});
});
// Equals button
document.querySelector('.btn-equals').addEventListener('click', () => {
try {
this.calculator.equals();
this.updateDisplay();
this.updateOperatorButtons();
} catch (error) {
this.display.showError(error.message);
}
});
// Clear buttons
document.querySelector('[data-action="clear"]').addEventListener('click', () => {
this.calculator.clear();
this.updateDisplay();
this.updateOperatorButtons();
});
document.querySelector('[data-action="clear-entry"]').addEventListener('click', () => {
this.calculator.clearEntry();
this.updateDisplay();
});
}
attachKeyboardListeners() {
document.addEventListener('keydown', (e) => {
// Prevent default for calculator keys
if (this.isCalculatorKey(e.key)) {
e.preventDefault();
}
// Handle number keys
if (e.key >= '0' && e.key <= '9') {
this.calculator.inputNumber(e.key);
this.updateDisplay();
}
// Handle decimal
if (e.key === '.') {
this.calculator.inputDecimal();
this.updateDisplay();
}
// Handle operators
if (['+', '-', '*', '/', '%'].includes(e.key)) {
try {
this.calculator.performOperation(e.key);
this.updateDisplay();
this.updateOperatorButtons();
} catch (error) {
this.display.showError(error.message);
}
}
// Handle equals
if (e.key === '=' || e.key === 'Enter') {
try {
this.calculator.equals();
this.updateDisplay();
this.updateOperatorButtons();
} catch (error) {
this.display.showError(error.message);
}
}
// Handle clear
if (e.key === 'Escape') {
this.calculator.clear();
this.updateDisplay();
this.updateOperatorButtons();
}
// Handle backspace
if (e.key === 'Backspace') {
const current = this.calculator.getDisplayValue();
if (current.length > 1) {
this.calculator.currentValue = current.slice(0, -1);
} else {
this.calculator.currentValue = '0';
}
this.updateDisplay();
}
});
}
isCalculatorKey(key) {
return (key >= '0' && key <= '9') ||
['+', '-', '*', '/', '%', '.', '=', 'Enter', 'Escape', 'Backspace'].includes(key);
}
updateDisplay() {
this.display.updateDisplay(this.calculator.getDisplayValue());
this.display.updateHistory(this.calculator.getHistory());
}
updateOperatorButtons() {
document.querySelectorAll('.btn-operator').forEach(btn => {
btn.classList.remove('active');
});
if (this.calculator.operator) {
const activeBtn = document.querySelector(`[data-operator="${this.calculator.operator}"]`);
if (activeBtn) {
activeBtn.classList.add('active');
}
}
}
}
// Initialize app
const app = new CalculatorApp();
Features Implementation
Event Handling
- Button Clicks: All calculator buttons functional
- Keyboard Support: Full keyboard input support
- Visual Feedback: Active operator highlighting
- Smooth Interactions: Button press animations
State Management
- Current Value: Currently displayed number
- Previous Value: Stored for operations
- Operator: Current operation type
- Waiting Flag: Tracks input state
Mathematical Operations
- Basic Operations: +, -, *, /
- Modulo: % operation
- Decimal Support: Proper decimal handling
- Error Handling: Division by zero, invalid operations
Testing Your Application
Manual Testing Checklist
- [ ] Test all number buttons
- [ ] Test all operators
- [ ] Test equals function
- [ ] Test clear functions
- [ ] Test decimal input
- [ ] Test division by zero
- [ ] Test keyboard input
- [ ] Test chained operations
- [ ] Test large numbers
- [ ] Test negative numbers
Exercise: Build Calculator
Instructions:
- Create all files as shown
- Implement all features
- Test thoroughly
- Customize the design
- Add your own enhancements
Enhancement Ideas:
- Add scientific calculator functions
- Add memory functions (M+, M-, MR, MC)
- Add calculation history
- Add theme switcher (light/dark)
- Add sound effects
- Add animation effects
- Add keyboard shortcuts display
- Add unit conversion
Common Issues and Solutions
Issue: Decimal input not working
Solution: Check that inputDecimal() properly handles the waitingForOperand state.
Issue: Operations not chaining
Solution: Ensure performOperation() properly stores previous value and operator.
Issue: Division by zero
Solution: Add check in calculate() method to throw error for division by zero.
Quiz: Calculator Concepts
-
State management:
- A) Tracks calculator state
- B) Doesn't track state
- C) Both
- D) Neither
-
Event handling:
- A) Responds to user input
- B) Doesn't respond
- C) Both
- D) Neither
-
Mathematical operations:
- A) Perform calculations
- B) Don't perform calculations
- C) Both
- D) Neither
-
Error handling:
- A) Important for calculator
- B) Not important
- C) Both
- D) Neither
-
Keyboard support:
- A) Improves usability
- B) Doesn't improve usability
- C) Both
- D) Neither
Answers:
- A) Tracks calculator state
- A) Responds to user input
- A) Perform calculations
- A) Important for calculator
- A) Improves usability
Key Takeaways
- Event Handling: Complex interactions require careful event management
- State Management: Track all necessary state for calculations
- Mathematical Operations: Implement operations correctly
- Error Handling: Handle edge cases gracefully
- User Experience: Smooth interactions and visual feedback
- Best Practice: Clean code structure and proper separation of concerns
Next Steps
Congratulations! You've built a complete Calculator application. You now know:
- How to handle complex events
- How to manage application state
- How to perform mathematical operations
- How to build interactive UIs
What's Next?
- Project 2: Frontend Applications
- Learn React projects
- Build React applications
- Apply React concepts
Project completed! You've finished Project 1: Interactive Web Pages. Ready for Project 2: Frontend Applications!
Course Navigation
- Todo List Application
- Weather App
- Calculator