Javascript Calculator
Intro
This is the fourth project in the freeCodeCamp Front End Development Libraries Certification. The objective is to build a simple javascript calculator. It should do addition, subtraction, multiplication and division. The formula and result should be displayed on the screen. The suggested method is to use a front end library like React. Read more about the project at Build a Javascript Calculator
Check out the end product at https://calculator.projects.yasakdogra.com
Planning
We will use React, Redux and Tailwind CSS for this project. Check out how to setup a basic project at: React with Redux, Tailwind and More
We will create a calculator state using Redux. We will use key press actions to store user input and do the calculation when necessary and then use the state to show the output on the calculator display.
We will have a App
component which will contain the Calculator
component. The Calculator
component will have Inputs
and Screen
components. The Inputs
will have multiple instances of Key
component. The Screen
will have FormulaDisplay
which will show the complete formula and the ActiveDisplay
which shows the current input value.
State
Create src/state/calculatorSlice.ts with Redux imports
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
Let’s define some types. We will need to check each key pressed by the user to make decisions in the action handlers.
The operands will be floating point numbers. We will create the type CalculatorOperand
an array that has the numbers 0-9 in string format and the period(.) for decimal.
NOTE: The operand type can also be the union number | '.'
, but our key click inputs will send string values, its easier to just use an array with string values
The operators will be addition, subtraction, multiplication and division. We will create the type CalculatorOperator
from an array that has the symbols/keys for the operators.
The rest of the keys will be calculator actions. We will create the type CalculatorAction
from an array that has the symbols/keys for the actions.
We will create the type CalculatorInput
that matches all inputs by creating a union of the three types above.
We will create the type CalculatorElement
to store the calculator inputs, parsed numbers and the calculated results.
In addition to the types, we will also create functions to recognize which type the user input is. They will also serve as type guards for our code
const CALCULATOR_OPERANDS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '.'] as const;
const CALCULATOR_OPERATORS = ['+', '-', 'x', '÷'] as const;
const CALCULATOR_ACTIONS = ['AC', 'DEL', '=', 'I', 'F'] as const;
type CalculatorOperand = typeof CALCULATOR_OPERANDS[number];
type CalculatorOperator = typeof CALCULATOR_OPERATORS[number];
type CalculatorAction = typeof CALCULATOR_ACTIONS[number];
export type CalculatorInput = CalculatorOperand | CalculatorOperator | CalculatorAction;
export type CalculatorElement = CalculatorInput | number;
export const isCalculatorOperand = (object: any): object is CalculatorOperand => CALCULATOR_OPERANDS.includes(object as CalculatorOperand);
export const isCalculatorOperator = (object: any): object is CalculatorOperator => CALCULATOR_OPERATORS.includes(object as CalculatorOperator);
export const isCalculatorAction = (object: any): object is CalculatorAction => CALCULATOR_ACTIONS.includes(object as CalculatorAction);
Create a type for calculator state and initialize the initial state. formula
is a list of elements, current
is the string to be displayed in the ActiveScreen
component and method
is the mode of the calculator. The mode can be either formula or intermediate. Formula mode calculates using BEDMAS whereas the intermediate calculates left to right.
Formula mode: 2 + 4 ÷ 2 = 4
Intermediate mode: 2 + 4 ÷ 2 = 3
interface CalculatorState {
formula: CalculatorElement[];
current: string;
method: string;
}
const initialState: CalculatorState = {
formula: [],
current: '0',
method: 'formula',
};
Create action creators
export const calculatorSlice = createSlice({
name: 'formula',
initialState,
reducers: {
keyPress(state, action: PayloadAction<CalculatorInput>) {
if (isCalculatorOperator(action.payload)) {...}
else if (isCalculatorAction(action.payload)) {...}
else {...}
}
},
});
The if blocks for handling operators and calculator actions are pretty straight forward.
When a key with operator is clicked, it will parse the string in current
an add it to formula
. When an operator is clicked after a calculation is done, it will clear all the entries in formula
and add the previous answer as the first element in formula
. There is a special case is when there negative operator is clicked. The negative can follow another operator once to indicate that the next number is a negative number.
AC
will clear the states, DEL
will delete the last digit in current
until 0 is reached, I
and F
toggle the intermediate and formula mode. =
calls the calculate
function with a copy of the formula
and add the output to the formula
after an equal sign.
These are the functions for calculating the answer
const calculate = (formula: CalculatorElement[], method: string): number => {
// Replace "operator, negative sign, number" with "operator, negative number"
for (let i = 0; i < formula.length; i++) {
if (isCalculatorOperator(formula[i]) && formula[i + 1] === '-') {
formula[i + 2] = -1 * (formula[i + 2] as number);
formula.splice(i + 1, 1);
}
}
if (method === 'intermediate') {
return calcIntermediate(formula);
}
return calcFormula(formula);
}
const calcIntermediate = (formula: CalculatorElement[]): number => {
let result = formula[0] as number;
for (let i = 1; i < formula.length - 1; i++) {
let curr = formula[i];
if (isCalculatorOperator(curr)) {
switch (curr) {
case '+':
result += formula[i + 1] as number;
break;
case '-':
result -= formula[i + 1] as number;
break;
case 'x':
result *= formula[i + 1] as number;
break;
case '÷':
result /= formula[i + 1] as number;
break;
}
}
}
return result;
}
const calcFormula = (formula: CalculatorElement[]): number => {
// Calculate multiplication and division
for (let i = 1; i < formula.length - 1; i++) {
let curr = formula[i];
if (isCalculatorOperator(curr)) {
switch (curr) {
case 'x':
formula[i + 1] = (formula[i - 1] as number) * (formula[i + 1] as number);
formula.splice(i - 1, 2);
i--;
break;
case '÷':
formula[i + 1] = (formula[i - 1] as number) / (formula[i + 1] as number);
formula.splice(i - 1, 2);
i--;
break;
}
}
}
// Calculate addition and subtraction
for (let i = 1; i < formula.length - 1; i++) {
let curr = formula[i];
if (isCalculatorOperator(curr)) {
switch (curr) {
case '+':
formula[i + 1] = (formula[i - 1] as number) + (formula[i + 1] as number);
formula.splice(i - 1, 2);
i--;
break;
case '-':
formula[i + 1] = (formula[i - 1] as number) - (formula[i + 1] as number);
formula.splice(i - 1, 2);
i--;
break;
}
}
}
return formula[0] as number;
}
Components
The App
, Calculator
and Screen
components are wrappers components with some styling using Tailwind classes. Let’s get into the components that use the state.
ActiveDisplay component
This component shows the current
value from the state
Create src/components/ActiveDisplay.tsx
import { useSelector } from "react-redux";
import { RootState } from "../state/store";
export default function ActiveDisplay() {
const active = useSelector((state: RootState) => state.calculator.current);
return (
<div id="display" className="min-h-[3rem] w-full text-white text-right text-2xl">
{active}
</div>
);
}
FormulaDisplay component
This component is similar to ActiveDisplay
component and shows the formula
part of the state
import { useSelector } from "react-redux";
import { RootState } from "../state/store";
export default function FormulaDisplay() {
const formula = useSelector((state: RootState) => state.calculator.formula);
return (
<div className="min-h-[2rem] w-full text-amber-400 text-right text-xl break-words tracking-wide">
{formula}
</div>
);
};
Inputs component
This is the component where we can define what keys to render. We will use an array to store the key properties. We can add the size (s=small, l=large) to make the key one or two column width. We can also add a tailwind color class.
We will loop over all the elements of KEYS
in the render function and render a Key
component for each. We also pass in a key click handler.
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../state/store";
import { keyPress } from "../state/calculatorSlice";
import Key from "./Key";
const KEYS = [
{ id: "clear", text: "AC", size: "l", color: "bg-red-700" },
{ id: "backspace", text: "DEL", size: "s", color: "bg-stone-400" },
{ id: "divide", text: "÷", size: "s", color: "bg-stone-400" },
{ id: "seven", text: "7", size: "s" },
{ id: "eight", text: "8", size: "s" },
{ id: "nine", text: "9", size: "s" },
{ id: "multiply", text: "x", size: "s", color: "bg-stone-400" },
{ id: "four", text: "4", size: "s" },
{ id: "five", text: "5", size: "s" },
{ id: "six", text: "6", size: "s" },
{ id: "subtract", text: "-", size: "s", color: "bg-stone-400" },
{ id: "one", text: "1", size: "s" },
{ id: "two", text: "2", size: "s" },
{ id: "three", text: "3", size: "s" },
{ id: "add", text: "+", size: "s", color: "bg-stone-400" },
{ id: "zero", text: "0", size: "s" },
{ id: "decimal", text: ".", size: "s" },
{ id: "method", text: "I/F", size: "s", color: "bg-stone-400" },
{ id: "equals", text: "=", size: "s", color: "bg-stone-400" },
];
export default function Inputs() {
const method = useSelector((state: RootState) => state.calculator.method);
const dispatch = useDispatch<AppDispatch>();
const handleKeyPress = (text: any) => {
dispatch(keyPress(text));
}
return (
<div className="grid grid-cols-4 gap-0.5">
{KEYS.map((key) => {
let text = key.text;
let color = key.color;
if(key.id === "method") {
if(method === "formula") {text = "F"; color = "bg-green-500"}
else text = "I";
}
return <Key key={key.id} id={key.id} text={text} size={key.size} color={color} handleKeyPress={handleKeyPress} />
})}
</div>
);
};
Key component
We can now render the key component with the props passed to it
type KeyProps = {
id: string;
text: string;
size?: string;
color?: string;
handleKeyPress: (id: string) => void;
};
export default function Key({ id, text, size, color, handleKeyPress }: KeyProps) {
return (
<button
id={id}
className={`${size === "l" ? "col-span-2" : "col-span-1"}
${color ? color : "bg-stone-500"}
text-stone-200 text-2xl text-center h-14
opacity-80 hover:opacity-100 hover:border-2`}
onClick={() => handleKeyPress(text)}
>
{text}
</button>
);
};
Thank you for reading. You can also check out my other projects for this series below.