DEV Community

Cover image for Let's build a basic calculator using flexbox and vanilla JavaScript
Chris Blakely
Chris Blakely

Posted on • Originally published at chrisblakely.dev

Let's build a basic calculator using flexbox and vanilla JavaScript

Before we begin

Beginner JavaScript Project tutorials are a great way to learn and apply your knowledge. Rather than just show you how I do things, I'll explain WHY I do things. I recommend you try this yourself first, and see how I've done it by following along with the guide. There is a completed CodePen at the end. I will use CodePen.io throughout the tutorial but you can use your IDE if you like. Enjoy!

What we're building

Alt Text

This is the amazing calculator we will be building. We can input numbers, add, subtract, multiply and divide, as well as reset the display. Isn't it marvellous?

Build the UI

Let's start by adding the UI. This helps us visualise things as we go, such as user interactions and state changes.

Add a Container

In your HTML section add the following:

<div class="container">

</div>

and in your CSS section add the following:

.container{
    background: lightGrey;
    width: max-content;
    margin: auto;
}

You won't see anything yet but all we're doing here is adding a container, which gives us better control and structure of the calculator elements which we'll add later. We're also giving it a width (we want it to expand to fit the content), a background color, and centering everything in the view.

The display

When building the UI, can you build it however you like - For something like this I like to work "top-down", so I'll start with the display. This displays the calculations and results to the user and goes at the top of our container.

It makes sense for the display to be it's own element (so we can style it, and identify it clearly in the code/browser inspector). We'll create a separate div for this and also hard code a dummy value (989), so we can visually see what the display will look like, which should make it easier to style.

Add the following to your HTML, just inside the container:

  <div class="display">989</div>

The completed HTML so far should look like this;

<div class="container">
  <div class="display">989</div>
</div>

And add the following to your CSS:

.display{
  width: 100%;
  background-color: darkGrey;
  height: 50px;
}

All we're doing here is making sure the display isn't too big (height: 50px), takes up the width of the container (width: 100%) and has a background color that differs from the container.

So far, our project should look like this:

Alt Text

We still have some work to do. The number appears at the top left, we want it to appear towards the right-hand side of the display, and have a bigger font-size.

When we need to arrange items, in a single direction (e.g horizontally or vertically) Flexbox is a good option, so we'll use that here to style our display input.

Amend the .display style in your CSS with the following:

.display{
    width: 100%;
    background-color: darkGrey;
    height: 50px;
    font-size: 48px;
    display: flex;
    justify-content: flex-end;

}

Note the bottom 2 lines - display:flex and justify-content: flex-end;. This means that the display is a flex container and that it's children (in this case, the text content, i.e the number) should align to the END (the right side) of the container. Lastly, we're making the font bigger.

With our fancy flexbox stuff added, the display should look like this:

Alt Text

If you add more numbers to the dummy text, the inputted number stays justified to the right of the container (this is thanks to our flex-end)

Add the buttons

Now that we have our display in place and formatted, it's time to add the buttons - this would be a pretty useless calculator otherwise!

We'll add a container div to hold our buttons - remember, adding a container div around related items makes it easier to manage/style related elements, and makes our code easier to read. We'll also add our buttons, in order of left-to-right (like we were reading a book) to match our design. We'll use flexbox to arrange the buttons later.

Go ahead and update your HTML so that it looks like this:

<div class="container">
    <div class="display">989</div>
    <div class="buttons">
        <button class="btn-number">1</button>
        <button class="btn-number">2</button>
        <button class="btn-number">3</button>
        <button class="btn-operator">+</button>
        <button class="btn-number">4</button>
        <button class="btn-number">5</button>
        <button class="btn-number">6</button>
        <button class="btn-operator">-</button>
        <button class="btn-number">7</button>
        <button class="btn-number">8</button>
        <button class="btn-number">9</button>
        <button class="btn-operator">X</button>
        <button class="btn-clear">C</button>
        <button class="btn-number">0</button>
        <button class="btn-equals">=</button>
        <button class="btn-operator">/</button>
    </div>
</div>

Note that the number buttons have a class btn-number, operator buttons have a class btn-operator, the equals button has a class btn-equals and the clear button has a class of btn-clear. We'll use these classes to style the buttons later.

So our buttons are in, but the layout is wrong and they look a bit crappy.

Alt Text

The reason why we arranged our buttons in a single row like this is that we can use flexbox to control the layout. Remember, our buttons are wrapped in a buttons div, so we will make this our flex container. Update the .buttons CSS to include the following:


.buttons{
    max-width: 400px;
    display:flex;
    flex-wrap: wrap;
}

Ok so what's happening here?

  • We're setting our buttons div to be a flex container
  • We're setting the width to be 400px;
  • We're saying that we want the items of the flex container (i.e, our buttons) to wrap (i.e take a new line). This will happen when the length of the row, goes over 400px (our width).

You should now see this:

Alt Text

Now I know what you're thinking: "Chris, WTF? I'm setting up all this stuff and nothings working". Yes, but stay with me, the magic is about to happen. We've merely set up our flex container layout, now we need to add some properties to our flex items i.e buttons.

Add the following to your CSS:


button{
  flex: 1 25%;
} 

Our calculator should now look like this:

Alt Text

If you were WTF'ing before, you still might be now, but in a more curious tone. Let's explain what just happened when we added flex: 1 25% to each button:

This is shorthand syntax for flex-grow: 1 and flex-basis: 25%, e.g, our button CSS above could be rewritten like:


button{
  flex-grow: 1; 
  flex-basis: 25%;
} 

This means "each item (i.e button) should take up 25% of the space available, and the item itself should grow to fill said space". Let's do some maths to explain further. Remember, our container has a max-width of 400px. We've told the container any overflowing items should break onto the next line by using flex-wrap: wrap.

This means we have 400px of space per row before a line break happens. Since we've said that each button should take up 25% of the space available, each button should have a width of 100px (since 25% of 400px is 100px) giving us 4 buttons per row. By inspecting the dev tools, we can see this is true:

Alt Text

If we wanted 2 buttons per row, we should change the percentage in our CSS:

button{
  flex: 1 50%;
} 

Alt Text

Why do we need to do this?!

This makes it easier to align/arrange items in a few lines of code. If you look at our HTML and CSS, there isn't very much there. Should our design change in the future (e.g change of button layouts, more responsive design, etc) it will be less complex to change.

I encourage you to stop here and play about with some of the flex properties we talked about, so you really understand what's going on. Have a look at This Guide To Flexbox on CSS-Tricks for reference. It's amazing. I'm going to get a coffee while you do that, then we'll get into JavaScript mode!

Let's wrap up our UI by adding some styling to the buttons. Add the following to your CSS:

button{
  flex: 1 25%;
  padding: 15px;
  font-size: 26px;
} 

.btn-number {
  background-color:grey;
}

.btn-operator {
  background-color:orange;
}

.btn-equals {
  background-color:green;
}

.btn-clear {
   background-color:crimson;
}

And let's remove the dummy data in the display. In your HTML, remove the 989 value from the div:

  <div class="display"></div>

Our finished UI should look like this:

Alt Text

Look at our amazing calculator! It looks nice but does nothing yet. So let's get everything wired up.

NOTE: See the CodePen at the bottom to see the completed HTML/CSS if you're having issues.

Add the JavaScript

When you're starting the JavaScript-y parts of a project, it can be hard to know where to start. I like to begin by thinking about what functionality I need, and making a list (this is why I built the UI first, so it's easier to visualize the functionality).

Why not take this time to come up with a list of functionality, and compare it to my list below?

Ready? Ok good, here's my list:

  • When the user clicks a number or operator button, the display should update with the value of the button
  • When the clear button is clicked, the display should reset/clear
  • When the equals button is clicked, the display should update with the result of the equation that is displayed

There's probably other stuff I missed that you think is important - you can always expand on this code later to add your own stuff!

Update the display when a number or operator button click

This is a good place to start, we'll worry about the other stuff later. Let's think more about how our JavaScript is going to look before jumping into the code.

"when a button is clicked, the display should update with the value of the button"

Some things that jump out when re-reading that functionality:

  • We'll need to get all the buttons and their values from the DOM
  • We'll need to add an event listener to handle the clicks
  • We're updating the display, so we'll need to get the display from the DOM as well

That should be enough to get us going! Let's start by updating the display when an operator button or number button is clicked. We will handle the clear and equals later.

Why are the equals and clear buttons code separate? Can I not get all the buttons at once and add all the event listeners at once?

The answer is yes, you can do whatever you want as there is no "right" way to write code. However, splitting the code out in this way has a few advantages:

  • It keeps similar logic together, enforcing good separation of concerns for our functions
  • If we have to add more buttons & logic, we don't touch existing code
  • It makes the code easier to read & reason about; it's easier to see which buttons do what

Get the number & operator buttons from the DOM

Add the following to your JS section in CodePen (or to your JavaScript script if you're using an IDE):


const buttons = document.querySelectorAll('.btn-number, .btn-operator')
const display = document.querySelector('.display')

All we're doing here is using querySelectorAll to get our buttons (number buttons and operator buttons) and get our display.

, In this case, it's a good idea to assign DOM elements to top-level variables, as it makes it easier for us to access these elements without having to repeatedly use document.querySelectorAll() throughout the code. It also helps if the querySelector changed for some reason (e.g from .display to .calculator-input-display), as we only have to change the querySelector in one place!

Next, we're going to add a data-num attribute to the HTML of our buttons, which will hold the value of that button. Update your HTML to match the following:

<div class="container">
  <div class="display"></div>
  <div class="buttons">
    <button class="btn-number" data-num="1">1</button>
    <button class="btn-number" data-num="2">2</button>
    <button class="btn-number" data-num="3">3</button>
    <button class="btn-operator" data-num="+">+</button>
    <button class="btn-number" data-num="4">4</button>
    <button class="btn-number" data-num="5">5</button>
    <button class="btn-number" data-num="6">6</button>
    <button class="btn-operator" data-num="-">-</button>
    <button class="btn-number" data-num="7">7</button>
    <button class="btn-number" data-num="8">8</button>
    <button class="btn-number" data-num="9">9</button>
    <button class="btn-operator" data-num="*">X</button>
    <button class="btn-clear">C</button>
    <button class="btn-number" data-num="0">0</button>
    <button class="btn-equals" data-num="=">=</button>
    <button class="btn-operator" data-num="/">/</button>
  </div>
</div>

"Why do we need to do this?! Why can I not get the value from the button text?"

Good question! This technique is used to separate data from presentation. Take the example of the multiply sign:

    <button class="btn-operator" data-num="*">X</button>

The text of the button is X but the value is actually *, since this is how we multiply in JavaScript. Using this approach means the text can be whatever we want, but the value never changes. e.g:

<!-- If we ever change the text of any of the buttons, the value stays the same  -->
<button class="btn-operator" data-num="*">X</button>
<button class="btn-operator" data-num="*">Multiply</button>
<button class="btn-operator" data-num="*"><img src="link-to-multiply-image"/></button>

OK! So we've stored the data for each button and got the DOM elements. Next, let's handle what happens when a button is clicked.

Add the event listeners

When we use querySelectorAll(), this returns a NodeList (this is basically a list of matching elements, which we can loop over). This means we can loop over our buttons variable and attach an onClick listener.

Add the following to your JavaScript:

const buttons = document.querySelectorAll('.btn-number, .btn-operator')
const display = document.querySelector('.display')

//add an eventListener to each of the buttons
buttons.forEach(button => {
  button.addEventListener('click', () => { 
      // logic that run when the button is "clicked"
      alert("button was clicked!")
  })
})

As you can hopefully see, we're using the forEach() higher-order function to loop over the buttons that were returned from our querySelectorAll() function.

For each button we are adding a click eventListener. When a button is clicked, it should show an alert box:

Alt Text

Hurray! Success!

(If you're having issues and not seeing the alert box, check out the CodePen at the end of the article to see the finished solution).

Get the value of the button that was clicked

Next, we need to get the value of the clicked button from the data-num attribute we added earlier. When we want to get attributes from DOM elements, we can use the getAttribute(name) function. Add the following to the event listener logic in your JavaScript:

const buttonValue = button.getAttribute('data-num');  
alert(buttonValue + " was clicked!")

The JavaScript so far looks like this:

const buttons = document.querySelectorAll('.btn-number, .btn-operator')
const display = document.querySelector('.display')

//add an eventListener to each of the buttons
buttons.forEach(button => {
  button.addEventListener('click', () => { 
    // logic that run when the button is "clicked"
    const buttonValue = button.getAttribute('data-num');  
    alert(buttonValue + " was clicked!")
  })
})

When you click a button, the alert should show the value of the button that was clicked.

Update the display

Now that we have working event handlers, and can get the value of the button that was clicked, we can start updating our display.

Remember, it's good practice to separate data from presentation whenever possible. So let's create a variable called displayData to hold the data for our display. In your JavaScript add the following:


let displayData = "";

and update the eventListener logic by replacing the alert with this line:

displayData += buttonValue;
alert("Display is now: " + displayData)

The completed JavaScript so far looks like this:


//get our buttons from the DOM
const buttons = document.querySelectorAll('.btn-number, .btn-operator')
const display = document.querySelector('.display')

let displayData = "";

//loop over the buttons
buttons.forEach(button => {

    //for each button, we want to add a "click" event listener
    button.addEventListener('click', () => { 

        //get the value of the clicked button from the attribute
        const buttonValue = button.getAttribute('data-num');

        //update our displayData variable with the value of the clicked button
        displayData += buttonValue;
        alert("Display is now: " + displayData)
    })
})

Now when you click a button, you will see an alert with the equation displayed!

Alt Text

Now we know what our displayData successfully updates when a button is clicked, we can go ahead and update the actual display with this data. Remove the alert in your event listener and add the following:

display.textContent = displayData;

Now, our display should update when a number or operator button is clicked:

Alt Text

Woohoo! Looking good so far, but you will have noticed the equals and clear buttons do not work yet. So let's wire those up.

Update the display with the result

If we take some time to think about what we need to do here, it'll make our life easier when writing the code.

  • We need to get the equals button from the DOM
  • When the button is clicked, we want to evaluate the expression that is displayed
  • We want to show the result of the expression in the display

Why not try this next part yourself first before following on? HINT: We've used similar patterns already, referencing other code for help is a good way to solve problems!

Get the Equals button from the DOM

Just like before, we will get the equals button from the DOM. Add the following to your JavaScript:

const equalsButton = document.querySelector('.btn-equals')

Notice we use querySelector here instead of querySelectorAll as we know there is only one element with this selector. querySelector returns a single element instead of a NodeList which means we don't need to use a loop!

Next we'll add an eventListener, and use the eval() function to evaluate the displayData expression. We'll set this evaluated expression to our displayData variable, and update our display:

equalsButton.addEventListener('click', () => { 
    displayData=eval(displayData)
    display.textContent = displayData
})

The complete JavaScript so far looks like this:

//get our buttons from the DOM
const buttons = document.querySelectorAll('.btn-number, .btn-operator')
const equalsButton = document.querySelector('.btn-equals')
const display = document.querySelector('.display')

let displayData = "";

//loop over the buttons
buttons.forEach(button => {

    //for each button, we want to add a "click" event listener
    button.addEventListener('click', () => { 

        //get the value of the clicked button from the attribute
        const buttonValue = button.getAttribute('data-num');

        //update our displayData variable with the value of the clicked button
        displayData += buttonValue;

        //output the displayData value to the display element
        display.textContent = displayData;
    })
})

// add an event listener to the equals button
equalsButton.addEventListener('click', () => { 

    // use the eval() function to evaluate the expression and output it to the display
    displayData=eval(displayData)
    display.textContent = displayData
})

And that's it - if you perform an equation e.g "2+2", you should see the result in the display.

Clear the display

The last thing we need to do is clear the display when the C button is clicked. Just like before, we will get the clear button from the DOM. Add the following to our JavaScript:

const clearButton = document.querySelector('.btn-clear')

And add our eventListener:

clearButton.addEventListener('click', () => { 
  displayData = "";
  display.textContent = displayData;
})

Here, we are resetting our displayData variable to empty string, which effectively clears the display. Finally, we're updating our display in the usual way.

If all went well, you should have a fully working calculator! If not, check out the CodePen at the bottom so see the finished example.

Thing's to Try

Before we wrap up, here are some thing's you can try:

  • The numbers will overflow if the user types in too many numbers, how would you fix this?
  • The calculator won't work correctly if the user enters too many operators, e.g 2++2. How can you prevent the user from doing this?
  • Why not try adding an undo feature?

Finished Example

See the Pen simple-js-calculator by Chris Blakely
(@chrisblakely01) on CodePen.

Thanks for reading!

If you enjoyed this article, why not subscribe to my newsletter?

Every week I send out a list of 10 things I think are worth sharing β€” my latest articles, tutorials, tips and interesting links for upcoming developers like you. I also give out some free stuff from time to time :)

Top comments (4)

Collapse
 
conermurphy profile image
Coner Murphy

Great article. It's really helped me cement my understanding of concepts in JS that you didn't even directly cover like arrow functions. By reading others code and explaining it to myself it helps build that knowledge.

The only thing I wanted to question in regards to your calculator is how does it handle BODMAS / BIDMAS? Being unfamiliar with the eval() function does this include this logic? Or, would this need to be build in separately?

Once again, great article. πŸ˜€

Collapse
 
chrisblakely01 profile image
Chris Blakely

Hey! Thanks for your kind words, I'm glad this article was helpful to you. In regards to the BODMAS question, maybe this stackoverflow answer will help clarify for you:

stackoverflow.com/questions/303462...

Thanks again for reading!

Collapse
 
budescueftimie profile image
Budescu-Eftimie

Thank you for the article, it helped me understand flexbox better, i am a begginer and this kind of articles of more experienced developers help me in my path to become a developer myself . One of the reason your article was so helpfull to me is because i was following your code and gotten different results, my buttons container and parent or main container have different widths. and after trial and error i saw that the mistake is in the css you set width of buttons class at 400px when in fact it should be 100%, later i saw that in the code pen example you have set the width of the main container at 300px unlike in the article where there is set to max content. Also in the code pen example you have set the width of the .display twice 100% and 300px , this is confusing for a begginer like me but also it helped me learn better and that's why i am grateful for the amazing work you and your peers do. Thank you verry much.

Collapse
 
john_pels profile image
Ajeigbe John Oluwaseyi

A great and educative article, I just learned new JS tricks.