Kyle Banks

Part 2: Get Started Programming with Dart - Building a Calculator

Written by @kylewbanks on Apr 14, 2013.

Note: For best results using the following demo I recommend using Google Chrome

Welcome back for Part 2 of my Dart Tutorial Series, where we will be building the calculator you see above. This tutorial will give you a better understanding of the fundamentals of Dart including basic math operations, dynamic vs. static typing, type conversion, conditional statements, and loops.

In case you missed it, I recommend working through Part 1 before attempting this section. In addition, the full source code for this tutorial is available here.

Getting Started

The first thing we have to do, of course, is create a new project called DartCalculator. The project is a Web application, and you can go ahead and leave Generate sample content checked. We will be overwriting most of the code anyways, but it's a nice starting point.

Because this tutorial is about Dart, I've provided the necessary CSS/HTML and will not go over it too much. You can download the two necessary files here. Copy dartcalculator.css and dartcalculator.html into your project, overwriting the original files. I recommend having a quick look through dartcalculator.html so you have an idea of the elements we will be working with.

Go ahead and get rid of the unnecessary code in dartcalculator.dart so you are left with the following:

import 'dart:html';

void main() {

}

Let's Get Coding

Alright, we are now ready to get started. The first thing we are going to do is create references to the calculator's screen, all numeric buttons, the decimal button, clear, and clear everything. We will come back to the operation (+ - / *) and equals buttons later on.

import 'dart:html';

DivElement screen;
List<DivElement> numbers;
DivElement decimal;
DivElement clear;
DivElement clearAll;

void main() {
    screen = query("#calculator_screen");
    numbers = queryAll(".number");
    decimal = query("#decimal");
    clear = query("#clear");
    clearAll = query("#clear-everything");
}

Now, there are few things to note here. Rather than using var like in JavaScript, we have chosen to define each variable statically, meaning they are type-checked, constraining them to the appropriate class (and subclasses). If you prefer, you are still free to use var the same way as you would in JavaScript.

The second thing to notice is that because numbers is a List, and because query returns a single element, we are using queryAll to get a list of all elements with the class number. I don't know about you, but I'd rather not type out a query for each number, and store each in it's own variable.

Adding Click Events

Adding click events to each element is nothing new, however because numbers is a List we are going to have to iterate through it and assign a click event to each one. Again, I'm lazy, so I'd like to have each of them make use of the same click event. In your main method, after you assign the variables:

void main() {
   ...

   for(DivElement number in numbers) {
      number.onClick.listen(numberClicked); 
   }
   decimal.onClick.listen(decimalClicked);
   clear.onClick.listen(clearScreen);
   clearAll.onClick.listen(clearEverything);

   clearEverything(null); //Ensures a fresh slate at application launch
}

//Stub out each of the click event handlers like so
void numberClicked(MouseEvent event) {
   //TODO:
}
...

Make sure to stub out each click event handler like I did with numberClicked above. We will come back to them in a second. Before we go on to handling each event, I'd like to take a look at what we did, specifically with the numbers list.

What we wrote there is called a foreach loop. This type of loop is very common in programming, but is not available in JavaScript, something that Dart aims to fix. Essentially it gives you access to each DivElement in the list numbers by assigning it to the variable number. Within the loop, we can do anything we like to number, in this case assigning it a click-event handler.

Implementing Click Events

Most of the code here is pretty self-explanatory, so I won't go over it in much detail. Have a look through it, and make sure you understand what's going on.

/**
 * Called when a number button (0-9) is clicked.
 */
void numberClicked(MouseEvent event)  {
    //If the number is currently '0', clear it
    if(screen.text == "0") {
        screen.text = "";
    }
    
    //Add the number clicked to the screen, if the screen number's length is less than 10
    if(screen.text.length < 10) {
        screen.text = screen.text + event.toElement.text;
    }
}

/**
 * Add a decimal to the current number, if there is not already a decimal on screen.
 */
void decimalClicked(MouseEvent event) {
    if(!screen.text.contains(".")) {
        screen.text = screen.text + ".";
    }
}

/**
 * Clear the screen of the current number
 */
void clearScreen(MouseEvent event) {
    //Reset the screen number back to 0
    screen.text = "0";   
}

/**
 * Clear the screen and all locally stored variables
 */
void clearEverything(MouseEvent event) {
    clearScreen(event);
    //TODO: Once we start storing variables, we will need to reset them here.
}

At this point you should be able to run the application, put some numbers and a decimal point on the calculator's screen, and clear it. It's not much, but it's a start.

One thing I want to make sure you notice is in numberClicked where we get the number of the button without having to know which button it was. This is because the MouseEvent class gives you access to the element that was clicked through the toElement method. By using this, we can get the text of the button that was clicked (0, 1, 2, 3, etc...) and put it on the screen, all while never needing to know which button it was.

Implementing Operations

Once you are satisfied with the previous code, we can start to make it actually do something useful. So here's how the calculator is going to work.

  1. The user enters a number (or leaves it at zero), and selects an operation type. When the operation type is selected we will highlight the button so they can remember which one it was. If it is the first time hitting an operation, we will store the number in a variable, and allow the user to enter the next number, otherwise we simply switch the operation type.
  2. Once the second number has been typed, the user can click the equals button (if they are still on the first number, we do nothing). At this point, we perform the selected operation type, and output the number to the screen. Once this is done, we need to clear all stored data and 'refresh' the calculator. The output of the operation will become the first number of a new operation, and the cycle continues.

So first up, let's define some constant variables that represent the different operation types, and one variable to contain the selected one. At the top of dartcalculator.dart (after the import statement):

const int unknown = -1;
const int OperationTypeAdd = 1;
const int OperationTypeSubtract = 2;
const int OperationTypeMultiply = 3;
const int OperationTypeDivide = 4;

int operationType = unknown;

With the constants defined, we are ready to query the elements and add click events. We will also need that variable we discussed to store the first number entered in each operation:

double number1;

DivElement add;
DivElement subtract;
DivElement multiply;
DivElement divide;

void main() {
    ...

    add = query("#add");
    add.onClick.listen(addClicked);
    subtract = query("#subtract");
    subtract.onClick.listen(subtractClicked);
    multiply = query("#multiply");
    multiply.onClick.listen(multiplyClicked);
    divide = query("#divide");
    divide.onClick.listen(divideClicked);
    
    ...
}

...

void addClicked(MouseEvent event) {
    operationType = OperationTypeAdd;
    operationTypeChanged(event.toElement);
}
void subtractClicked(MouseEvent event) {
    operationType = OperationTypeSubtract;
    operationTypeChanged(event.toElement);
}
void multiplyClicked(MouseEvent event) {
    operationType = OperationTypeMultiply;
    operationTypeChanged(event.toElement);
}
void divideClicked(MouseEvent event) {
    operationType = OperationTypeDivide;
    operationTypeChanged(event.toElement);
}
/** 
 * Each operation button calls this method after their click-event is processed
 */
void operationTypeChanged(Element selected) {
    //Remove the selected class from each operation button (+ - / *)
    List<Element> operationElements = queryAll(".operation");
    for(Element element in operationElements) {
        element.classes.remove("selected");
    }
    
    if(operationType != unknown) {
        //Add the selected class to the element that was just clicked
        selected.classes.add("selected");
        
        //Assign the 'number1' variable, if it isn't already
        if(number1 == null) {
            number1 = double.parse(screen.text);
            screen.text = "0";
        }
    }
}

Alright this is a big chunk of code, so let's break down the bits that may not be obvious. Basically when each operation button is clicked, we assign it's constant value to a global variable. This will allow us to access it later, without having to go to the DOM and check which button has the selected class.

Speaking of which, each of the operations' click events calls operationTypeChanged with it's element upon completion. This method will remove the selected class from each of the operation buttons as you can see in the foreach loop above. Once that's done, it adds the selected class to the element that was clicked. Finally, if there is no number1 variable set, it parses the text of the calculator's screen as a double and sets it's text to 0.

Now there is one more thing we have to do before we can implement the equals functionality. Remember that clearEverything event? Well we have some variables to clean-up in there now.

/**
 * Clear the screen and all locally stored variables
 */
void clearEverything(MouseEvent event) {
    clearScreen(event);
    operationType = unknown;
    operationTypeChanged(null);
    number1 = null;
}

You'll see that we are calling operationTypeChanged from clearEverything. This is to have the selected class removed from each button, without having to duplicate the code. It's safe to pass null because we set the operationType to unknown prior to doing so. When the operation type is unknown, operationTypeChanged doesn't make use of the passed element.

Bringing It All Together

Time has now come to implement the equals method. As usual, we need to define the element variable, query the DOM, and assign a click event:

DivElement equals;

...

void main() {
    ...

    equals = query("#equals");
    equals.onClick.listen(equalsClicked);
}

Now let's take a look at the equalsClicked method:

void equalsClicked(MouseEvent event) {
    if(number1 != null && operationType != unknown) {
        double result;
        double number2 = double.parse(screen.text);

        switch(operationType) {
            case OperationTypeAdd:
              result = number1 + number2;
              break;
            case OperationTypeSubtract:
              result = number1 - number2;
              break;
            case OperationTypeMultiply:
              result = number1 * number2;
              break;
            case OperationTypeDivide:
              result = number1 / number2;
              break;
        }
        
        clearEverything(null);
        String displayString = result.toString();
        if(displayString.length > 10) {
            displayString = displayString.substring(0, 10);
        }
        screen.text = displayString;
    }
}

We only want to do anything if the user has entered their first number and selected an operation type. If not, there is nothing for us to do yet. If the data looks good, we are going to parse the second number from the screen, and do a switch on the operation type. Depending on the operation type, we assign the variable result to the value of the appropriate operation.

Once we have the result, we call clearEverything to reset all data. We won't be needing any of it anymore. The one thing we will need though, is the new result. This now becomes number1 and is printed to the calculator's screen. The one thing we need to ensure though, is that only the first 10 digits of the number are displayed. Anything more than that will overflow the screen.

Now What?

Hopefully this calculator has been a good starting point in your Dart programming career, but it is far from finished. There are two main bugs to sort out, and one feature that would be a good challenge to implement.

The first bug is dividing by zero, but not in the way you would expect (try it out, I'm sure you will find the problem). The second is a little more complicated. When we display the result of an operation, we only show the first 10 digits. This works fine for numbers like 3.33333.... or really anything where there is a decimal before digit 9. However, what happens if the decimal is at digit 10, or any digit after that? What if the number is more than 10 digits, and there is no decimal?

If you are looking for a more difficult challenge, why not make the calculator work on key press? That means that users could perform calculations by typing the numbers, and selecting the operations from their keyboard. Perhaps use backspace and escape for the clear and clear-all functionality?

If you come up with a solution for either of the mentioned bugs, or implement the keyboard calculator, be sure to comment with your solution.

Let me know if this post was helpful on Twitter @kylewbanks or down below!