100 Days of TypeScript (Day 8)

At the end of the last article, I said that we were going to start looking at working with TypeScript on web pages. In this post, we are going to create a simple web-based calculator that shows how we can let TypeScript interact with the contents of a web page. While developing the code, I want to show how we can use “old-school” JavaScript style functions in TypeScript.

As always, the code for this article is available on GitHub.

The design

I want to constrain the input to the calculator so that button presses are limited to a set of well-known inputs. Basically, I want each button to either trigger a number or an operator of some description. In order to choose which operators we are going to work with, I have decided to create a TypeScript file called calculator.ts and add an Operator enumeration to it. I have added this file to the root of my solution.

enum Operator {
    add = '+',
    subtract = '-',
    multiply = '*',
    divide = '/',
    period = '.'
}

Before I show the TypeScript for our calculator, let’s set up the HTML page.

<!DOCTYPE html>
<head>
    <title>100 Days of TypeScript - Calculator</title>
</head>
<body>
    <table border="1">
        <th colspan="4"><input id="display"></input></th>
    </table>
    <script src="scripts/calculator.js"></script>
</body>

What we have here is an HTML page that shows a table with a header row, 4 columns wide, that will display the results of any calculations. I have also added a script tag to load the calculator script. You will notice that, although the source for the calculator TypeScript file is in the root folder, the src is pointing to the scripts folder. To get TypeScript to write to this folder, we need to adjust the tsconfig.json file. I have trimmed my tsconfig file right down to this.

{
  "compilerOptions": {
    "lib": [
      "DOM", "ES2015"
    ], 
    "outDir": "./scripts",
    "strict": true,
    "noUnusedLocals": true,
  }
}

There are two things of real interest in these entries. The outDir key is where we set our output directory; this is how I am writing the calculator.js file into the scripts directory. The lib entry is interesting as well because I have added a DOM entry there. When we specify entries in lib, we are telling TypeScript what libraries it needs to bring in, so entries such as ES2015 bring in the ECMA capabilities. One of the things we are going to want our calculator to do is to copy the input to the clipboard, which is a JavaScript operation on something called window.navigator. In order to access the navigator, we have to bring in the DOM library which allows us to perform interactions with the browser Document Object Model. What can be a little bit confusing is that you don’t need the DOM library to interact with other standard features such as the window document.

Before I add the calculator buttons to my table, there is one more piece of setting up I can do. I know that my table has an input field called display. I want to interact with this later on, so I am going to write a little helper function that will make my life easier when working with it. What I am going to do is create a value that will store a reference to the input. As I am going to bind to this value I am going to add a helper method that will lazily populate this reference when it is first accessed, and return the stored version in subsequent calls.

let displayElement: HTMLInputElement | null = null;
function getDisplay(): HTMLInputElement {
    if (!displayElement) {
        displayElement = <HTMLInputElement>document.getElementById('display');
    }
    return displayElement;
}

What this code is saying is that displayElement can either be of type HTMLInputElement, or it can be null to indicate we haven’t hooked it up to the display element yet. TypeScript is really useful in that we can strongly type our web page inputs so, what would have been an object in JavaScript can be constrained to the actual type. This is useful because it tells us what properties and operations we have available to us. In order to populate displayElement we use document.getElementById. This is a standard browser method that allows us to choose elements based on their id (this is set in the actual tag in the web page). Now, the return type of document.getElementById is object so we need to set it to the appropriate type using a technique called type casting. TypeScript offers a few different ways to cast an object but, in our case, we are using <> to specify the appropriate type.

Let’s add the rest of our table.

<tr>
    <td colspan="2"><input type="button" onclick="clearAll()" value="Clear" /></td>
    <td colspan="2"><input type="button" onclick="copyToClipboard()" value="Mem" /></td>
</tr>
<tr>
    <td><input type="button" value="1" onclick="display(1)"/> </td>
    <td><input type="button" value="2" onclick="display(2)"/> </td>
    <td><input type="button" value="3" onclick="display(3)"/> </td>
    <td><input type="button" value="/" onclick="display(Operator.divide)"/> </td>
 </tr>
 <tr>
    <td><input type="button" value="4" onclick="display(4)"/> </td>
    <td><input type="button" value="5" onclick="display(5)"/> </td>
    <td><input type="button" value="6" onclick="display(6)"/> </td>
    <td><input type="button" value="-" onclick="display(Operator.subtract)"/> </td>
 </tr>
 <tr>
    <td><input type="button" value="7" onclick="display(7)"/> </td>
    <td><input type="button" value="8" onclick="display(8)"/> </td>
    <td><input type="button" value="9" onclick="display(9)"/> </td>
    <td><input type="button" value="+" onclick="display(Operator.add)"/> </td>
 </tr>
 <tr>
    <td><input type="button" value="." onclick="display(Operator.period)"/> </td>
    <td><input type="button" value="0" onclick="display(0)"/> </td>
    <td><input type="button" value="=" onclick="solve()"/> </td>
    <td><input type="button" value="*" onclick="display(Operator.multiply)"/> </td>
 </tr>

Each button is wired up to one of four functions depending on what we are trying to do. Let’s start with the display function that is tied to the numbers and the operators we added to our enumeration earlier on.

function display(value: number | Operator): void {
    const htmlElement = getDisplay();
    htmlElement.value = htmlElement.value.trim() + value;
}

This function accepts either a number or one of the operators. I love the fact that TypeScript gives us union operators to say that values can be of one or another type using the |.

Inside the function, we get the reference to our input element using the function we wrote above. Once we have this element, we get the value from it and add the number or operator to it. I call the trim() operation on htmlElement.value, just in case the user has put a space at the end of the input.

If I want to clear the input, I can use the following method.

function clearAll(): void {
    const htmlElement = getDisplay();
    htmlElement.value = '';
}

This is very similar to our display function in that it gets the html input element (in fact, we’ll see that all of our functions will do this). Once it has the reference, it interacts directly with the value and sets it to an empty string.

You might think that the solve function would be complicated, parsing our input and performing calculations on it. The reality is, with the aid of a standard JavaScript function called eval, which evaluates the results of an input, this function is trivial.

function solve(): void {
    const htmlElement = getDisplay();
    const output = eval(htmlElement.value);
    htmlElement.value = output;
}

Finally, we move on to our code to copy the input onto the clipboard. As I mentioned earlier, we are going to make use of the navigator object, which required us to import the DOM library.

function copyToClipboard(): void {
    const htmlElement = getDisplay();
    navigator.clipboard.writeText(htmlElement.value);
}

You might remember that I said that the navigator object was in window.navigator. For convenience, our code normally doesn’t need to specify the window part so we can go directly to navigator.

Final note:

You might wonder why I chose to go with input type="button" rather than just using a button element. I chose the input route because people will be familiar with it and I don’t need the ability to set other content inside the button such as displaying an image.

Conclusion

That’s it. That’s our first web page, powered by TypeScript. I hope you are impressed with just how easy it is to hook TypeScript up to HTML. In Day 9, we are going to continue with our journey into the world of web development looking at creating a simple share reporting application.

5 thoughts on “100 Days of TypeScript (Day 8)

  1. Pingback: The Morning Brew - Chris Alcock » The Morning Brew #3459

  2. Pingback: Dew Drop – April 27, 2022 (#3672) – Morning Dew by Alvin Ashcraft

  3. Pingback: 100 Days of TypeScript (Day 9) – Confessions of a coder

  4. Great articles! I love the style of writing it’s easy to follow along and the explanations are to the point. I wish all writing was like this. I might be missing something but I wasn’t able to get the Day 8 code to run. I was able to successfully compile the Day 8 code to JS. When I click on any of the calculator buttons I get an error in the console stating “Uncaught TypeError: display is not a function as HTMLInputElement.onclick”
    It seems that the html isn’t able to call the calculator.js file. I’ve double checked for any type-o’s. I’ve also tried running “node scripts/calculator.js” Do you have any suggestions on what I might be doing incorrectly?
    Thanks

    1. peteohanlon

      Thanks for the question Charlie. If you look in the debug window in your browser, take a look and see if it’s picking up the script from the correct location (as defined in the tsconfig file). Check that the script src location points to scripts/calculator.js.

      If all else fails, I would recommend downloading the source from github and comparing the entries there.

Leave a comment