Creating User Interactive Applications 1
Creating User Interactive Applications Part 1
Creating an Interactive Application 1
As we have seen in previous lessons, Promises and Intro to SVG Animation and SVG Graphics 1, SVG objects can be animated. This makes them useful to use to demonstrate certain processes, such as manipulating an array. This will be useful in coming up with an example of an interactive web application.
When the World Wide Web (WWW) was first made available in 1993, JavaScript was not yet introduced. So, originally, web pages were made to simply share things online. The idea was to make it so that this sharing could take place over as wide a range as possible. When JavaScript was introduced in 1995, this introduced the potential to make web pages more interactive. JavaScript could be used to allow the user to interact with the web page and affect the way the web pages were displayed. This means that unlike with the printed encyclopedia (popular from the 19th century to the 1990s), the user could now modify what was being displayed. This is why we want to learn JavaScript programming just enough to take advantage of this way of sharing information.
Today, many people create WWW content. But, most of them don’t know how to make their web pages interactive in a way that enhances the content of their pages. One way to be in that minority of people who do know how to do this, is to learn the parts of JavaScript that makes this possible. That is what this website is about, and hopefully this is part of the reason why you are here. This means that you want to go beyond using some simple tricks to make your pages useful. You want to understand enough of the fundamentals to be able to control how users interact with your pages, so that your pages have your intended effect. This means you will benefit from learning how to create an actual web application that uses interactive elements. This is a challenge in any programming language, and JavaScript has its own unique hurdles to overcome. This is why we will create a relatively simple example application that still has most of the main parts that even more complex web applications would have. This is also why we will break the example into three parts. That will reduce the amount of new things you have to think about at a given time.
A Demo of where we are going
For this lesson, we will start off differently from earlier lessons. We will start by going to some pages that show a demo of what we might want to create for an interactive web application. The two demo pages will show two ways of sorting a group of numbers. In computing, being able to sort items is a very important process to be able to carry out. One thing that we do practically every day, is we compare things to help us make a decision. The larger the number of things we need to compare, the more important it is that we be able to sort them based on some property. If we are buying some computer part we might want to buy the one that has the lowest price, or the part that has the highest user ratings, or the part that was bought by the most people in some time period. To be able to return that kind of information, we need to be able to sort the information.
Having data that is sorted also allows us to search much more rapidly throught that data when we want to find something. That is because sorted data allows us to perform what is called a binary search. A binary search is much more rapid than a linear search. This is because when the data is sorted, you can skip over chunks of that data when you know what you are looking for is not in a given chunk. One way to get a feel for this is to consider something like a printed copy of the phonebook. For regular customers (white pages), the phone numbers are listed sorted in alphabetical order by the person’s last name. So, if you are looking for Janet Wilson’s phone number, you can skip over all the pages that have last names starting with the letters A through V. You can also skip over all the pages with last names starting with the letters X through Z. Then, you can look at the page headings to see if Wilson is within the range of names on the page. If not, you can quickly skip forwards or back until you find a page that Wilson should be found in. This is a form of binary search. The item is either in the range or not (two choices) and being in sorted order, you readily know where to look next. We don’t use printed phone books much nowadays, because it is much more convenient to search using an electronic database. By the way, a linear search through the phone book would involve starting on the very first page and seeing if the first entry was Janet Wilson. If it is not, then you go to the very next line, and see if that is for Janet Wilson and so on. It is clear that this kind of linear search would make it practically impossible to look up someone’s phone number in a reasonable amount of time. You may think that this is ridiculous, but suppose you did not know the name of the person you are trying to reach, but only know a phone number and are trying to figure out the name of the person with this phone number. Since the phone book is not sorted by phone number, this would mean you have to do a linear search. Of course, this is not a problem if you are searching in an electronic database of phone listings.
While we are talking about electronic databases, you should think about how that data is stored in a database. If we cannot sort the data, then we have to do linear searches and this would be too slow. So, all databases that are used by businesses or any large group of people use indexes for the database records that allows the data to be sorted. This what allows databases to be searched through in a relatively short amount of time, because the software can use binary searches to look for matching items. The largest database of information in the world is the one that is used by the Google search engine. Every single record in Google’s database is indexed. This allows them to perform binary searches instead of linear searches, and also allows them to break up the record by ranges of the index so that one computer can search in a specified range, while another computer is searching in a different range. That is done over probably many thousands of computers, which is part of the reason why Google’s search engine works so quickly. This is an oversimplification, but it should give you an idea why sorting is important in general for computing purposes.
Here is a link to a page demonstrating how the Selection Sort works:
Here is a link to a page demonstrating how the Insertion Sort works:
Starting to write the interactive web application
Don’t worry if those sorts don’t make sense to you just yet. As we build our web application, we will discuss those sorts in more detail so that you will understand better how those sorts work. What we want to do to start off our web application is to build up the main parts of the application that interact with the user. This means that we will work on the parts of the HTML markup that go into index.html that will allow for user input. As we do this, we will first try a relatively naive approach that uses some global variables. If you write a web application that just has a few global variables, that is okay. But, as you will hopefully see when we create the key user interactive parts of the application, we will run into more than just one or two global variables. This usually indicates a bad program design. So, we will rewrite the application so that we eliminate the use of global variables. This will be done in a way that can be used for any web application that is powered using JavaScript. That will make this a generalized approach that you can use for bigger more complex web applications.
Starting the application using CodeSandbox
Let’s use a starter template to begin the project for this lesson. Connect to your CodeSandbox account. In a different tab, go to the following link: Frontend for practicalcompute.cc. This will open up the template in CodeSandbox. You will see a view that looks like the following screen shot. Click on the Fork drop-down menu and select your CodeSandBox project name:
Once you have forked the template, go to the title bar, click on the drop-down list to select Rename and change the name to sort1.
After renaming, the new name should show up in the title bar:
Starting the application using the Vite template
Change into your ~/Documents folder and run the following command to create a project named sort1:
$ cd ~/Documents
$ npx degit takebayashiv-cmd/vite-template-my-project sort1
$ cd sort1
$ npm install
$ npm run dev
On line 1, we change into the ~/Documents folder where we create all our projects. On line 2 we download the vite template to create the project and directory named sort1. On line 3 we change into the project directory. On line 4 we run npm install to install the Node packages that are used by the template. Finally, on line 5, we start up the vite server to serve out our project site.
Editing the index.html file
Let’s start by editing the index.html file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script type="module" src="index.mjs"></script>
<title>Sorting demo 1</title>
</head>
<body>
<h1>Selection Sort</h1>
Enter a comma separated list of numbers, then click <b>Ok</b>:
<input type="text" id="numbox">
<button id="ok_button">Ok</button>
</body>
</html>
The new lines are 6, 9 and 10-12. Line 6 changes the <title> contents to "Sorting demo 1". Line 9 changes the contents of the <h1> element to "Selection Sort". Lines 10-12 put in a prompt for the input text box and the Ok button that is placed after the input text box. Take note of the id values, as we need these to get references to those elements.
The following shows a screen shot of the app viewed in a browser:
Next, we can add in the other elements for the applications interface. Here is the new version of index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script type="module" src="index.mjs"></script>
<title>Sorting demo 1</title>
</head>
<body>
<h1>Selection Sort</h1>
Enter a comma separated list of numbers, then click <b>Ok</b>:
<input type="text" id="numbox">
<button id="ok_button">Ok</button>
<br /><br />
Status: <label id="status_label"></label>
<br /><br />
<button id="compare_button">Compare</button>
<button id="swap_button">Swap</button>
<br /><br />
min: <label id="min_label"></label>
minPos: <label id="minPos_label"></label>
comparisons: <label id="comparisons_label"></label>
swaps: <label id="swaps_label"></label>
</body>
</html>
The new lines are 13-22. Line 13 adds two break elements to put a blank line between the Ok button and the Status label. The <label> element is an element that we can place text within. This text is not editable by the user. On line 14, we are putting in that <label> so that we can use it to show the status of the sorting procedure. Line 15 adds two break elements to put a blank line betwee the Status label and the Compare and Swap buttons. Lines 16 and 17 define the <button> elements for the Compare and Swap buttons, respectively. Line 18 adds two break elements to put a blank line after the Compare and Swap buttons and before the 4 labels that follow. Line 19 defines a <label> where we will display the min value. Line 20 defines a <label> where we will display the minPos value. Line 21 defines a label where we will display the number of comparisons made, and line 22 defines a label where we will display the number of swaps made. Note the values at the end of lines 19-22. These are non-breaking space values that will put an actual space in that location. By default, web browsers will collapse all white-space (non-printing characters) into a single space. So, to prevent those labels from being too close together, we use the non-breaking space characters.
Here is what a screen shot of the application looks like now:
The only thing we need to add to index.html is the <svg> element where we will draw the SVG graphics within. Here is the next version of index.html with that <svg> element put in place:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script type="module" src="index.mjs"></script>
<title>Sorting demo 1</title>
</head>
<body>
<h1>Selection Sort</h1>
Enter a comma separated list of numbers, then click <b>Ok</b>:
<input type="text" id="numbox">
<button id="ok_button">Ok</button>
<br /><br />
Status: <label id="status_label"></label>
<br /><br />
<button id="compare_button">Compare</button>
<button id="swap_button">Swap</button>
<br /><br />
min: <label id="min_label"></label>
minPos: <label id="minPos_label"></label>
comparisons: <label id="comparisons_label"></label>
swaps: <label id="swaps_label"></label>
<br />
<svg
id="svg_area"
width="1000"
height="120"
xmlns="http://www.w3.org/2000/svg"
></svg>
</body>
</html>
The new lines are 23-29. Line 23 adds a break element so that the <svg> element starts on the line below the labels defined just above. Lines 24-29 define the <svg> element. Line 25 defines the id attribute, line 26 defines the width attribute, line 27 defines the height attribute and line 28 defines the xmlns attribute. This means that the area we will draw our SVG objects within will be 1000 pixels wide by 120 pixels high.
Since we are not actually placing any of the SVG objects on the page yet, the application’s appearance in the browser is unchanged from the previous screenshot.
Editing the index.mjs file
Now that we have our HTML markup in place, we can work on index.mjs to start making the user interface of the application work. We will start with a naive approach. Although we will eventually change our approach, it is important to see why the naive approach is not a good practice. So, here is our first set of modifications to index.mjs:
//import { Student } from "./myclasses.mjs";
if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
function handleOk() {
console.log('handleOk() called');
}
function init() {
console.log('init called');
const numbox = document.getElementById("numbox");
const ok_button = document.getElementById("ok_button");
const status_label = document.getElementById("status_label");
const compare_button = document.getElementById("compare_button");
const swap_button = document.getElementById("swap_button");
const min_label = document.getElementById("min_label");
const minPos_label = document.getElementById("minPos_label");
const comparisons_label = document.getElementById("comparisons_label");
const swaps_label = document.getElementById("swaps_label");
ok_button.addEventListener('click', handleOk);
}
The new lines are 1, 9-11 and 15-24. Line 1 comments out the import of the Student class, as we won’t be using that class for this project. Lines 9-11 define our first version of the handleOk() function. Lines 15-23 obtain references to all the HTML elements that are part of the user interface for this application. Line 24 makes it so that clicking on the Ok button will cause the handleOk() function to be called. If you run the application now, you can see that clicking on the Ok button does cause "handleOk() called" to be printed to the console, as shown in the screen shot below.
This works okay so far, but we need to start thinking of some of the things that we need to do with all the elements that we have references to. Here is a short description of how we will make use of those references:
-
numbox - This will be needed to obtain the comma separated list of numbers when we hit the Ok button. This means that we need to refer to numbox inside the handleOk() function.
-
ok_button - We are already using this to call the handleOk() function so this is taken care of for the moment.
-
status_label - We will need to write to this when we hit the ok_button, the compare_button and the swap_button. Since we will have functions to handle the latter two events, status_label needs to be accessible in those handling functions.
-
compare_button - We need to be able to associate clicking on this button with some function to handle the compare case. This can likely be done inside of init().
-
swap_button - We need to be able to associate clicking on this button with some function to handle the swap case. This also can likely be done inside of init().
-
min_label - This is used to hold the current min value. So, this must be accessible from the function that handles compare and the function that handles swap. Both the compare and swap cases will result in the min_label needing to be accessed.
-
minPos_label - Just as with min_label this must be accessible within both the compare and swap cases.
-
comparisons_label - This used to display the total number of comparisons made. So, this needs to be accessible within the compare case.
-
swaps_label - This is used to display the total number of swaps made. So, this needs to be accessible from the compare case.
So, while the references to buttons can be made inside of init(), the other references should be made inside the handling functions that need access to the label references.
Let’s assume that we tried to set our index.mjs to handle what we have so far. Assume that we have handleCompare and handleSwap as the two additional handling functions besides handleOk. Then, a naive way to set this up might be something like this. We will need three global variables since those variables must be accessed from more than one function. So, here is the next version of index.mjs.
//import { Student } from "./myclasses.mjs";
if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// global variables that are accessed by more than one handling function
var status_label = document.getElementById("status_label");
var min_label = document.getElementById("min_label");
var minPos_label = document.getElementById("minPos_label");
function handleOk() {
const numbox = document.getElementById("numbox");
console.log('handleOk() called');
}
function handleCompare() {
const comparisons_label = document.getElementById("comparisons_label");
console.log('handleCompare() called');
}
function handleSwap() {
const swaps_label = document.getElementById("swaps_label");
console.log('handleSwap() called');
}
function init() {
console.log('init called');
const ok_button = document.getElementById("ok_button");
const compare_button = document.getElementById("compare_button");
const swap_button = document.getElementById("swap_button");
ok_button.addEventListener('click', handleOk);
compare_button.addEventListener('click', handleCompare);
swap_button.addEventListener('click', handleSwap);
}
The new lines are 9-12, 15, 19-22, 24-27 and 35-36. Line 9 is just a comment showing that global variables are being defined. Lines 10-12 define the global variables status_label, min_label and minPos_label. These are declared as global as they need to be accessed by more than one handling function.
Line 15 shows numbox being defined where it will be used, inside the handleOk() button. Lines 19-22 define the handleCompare() function. In this naive implementation, it would define the comparisons_label reference. Lines 24-27 define the handleSwap() function. In this implementation, it would define the swaps_label reference. Lines 35-36 associate the handling functions with clicking on the Compare and Swap buttons, respectively.
You can run the application as it stands now, and you would be able to click on all the buttons and see that the correct handling functions are called. But, let’s switch to something that will be a better implementation that is more capable of handling even more complex applications than the one we are trying to build.
Using factory functions
A factory function is a function that returns other functions. This is a programming pattern that JavaScript is well suited for, because functions are what is known as "first-class functions" or "function objects". This means, that in JavaScript functions can be passed as arguments to other functions and be returned from other functions. In addition, JavaScript functions can have custom properties like objects in other programming languages. This allows factory functions to make use of something called closure which binds properties to the factory function. This makes factory functions a very good mechanism for avoiding the use of global variables.
We want to use a factory function that returns all of our handling functions. Within the factory function we will define variables that can be used directly by any of those handling functions. This allows us to avoid using global variables.
If you are creating larger and more complex web applications using JavaScript, then using factory functions in this way is a good general approach.
Here is index.mjs, rewritten to make use of a factory function called createHandlers():
//import { Student } from "./myclasses.mjs";
if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
function createHandlers() {
const status_label = document.getElementById("status_label");
const min_label = document.getElementById("min_label");
const minPos_label = document.getElementById("minPos_label");
const comparisons_label = document.getElementById("comparisons_label");
const swaps_label = document.getElementById("swaps_label");
return {
handleOk() {
const numbox = document.getElementById("numbox");
console.log('handleOk() called');
},
handleCompare() {
console.log('handleCompare() called');
},
handleSwap() {
console.log('handleSwap() called');
}
}
}
function init() {
console.log('init called');
const handlers = createHandlers();
const ok_button = document.getElementById("ok_button");
const compare_button = document.getElementById("compare_button");
const swap_button = document.getElementById("swap_button");
ok_button.addEventListener('click', handlers.handleOk);
compare_button.addEventListener('click', handlers.handleCompare);
swap_button.addEventListener('click', handlers.handleSwap);
}
The new lines are 9-32, 36 and 40-42. Lines 9-32 define the factory function createHandlers(). Lines 10-14 define references to all the <label> elements we are using for this application. By placing these reference values inside our factory function, these can be directly accessed anywhere inside of the createHandlers() function. This is an example of closure where these variables cannot be accessed from outside the createHandlers() function. This is a way of using closure to make those variables private to the handler functions defined within createHandlers(). This protects those variables from being modified by any code outside of createHandlers(). So, unlike using global variables, defining a factory function using this method is safer, and is the preferred method.
Lines 16-31 define the return values for createHandlers(). As you can see the return values are three functions, handleOk(), handleCompare() and handleSwap(). Right now, these functions don’t do much, but you can click on the associated buttons and they will show that they are being called.
Line 36 calls the createHandlers() function and stores the result of calling this in the const handlers. So, on lines 40-42, you can see that the name of the handling functions have been changed to include the handlers prefix. So, instead of calling handleOk() as before, we call handlers.handleOk(), and so forth.
You can run the application to see that the console displays the correct message when clicking on the buttons. But, let’s make it so the application makes use of some of the labels. Here is the next version of index.mjs.
//import { Student } from "./myclasses.mjs";
if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
function createHandlers() {
const status_label = document.getElementById("status_label");
const min_label = document.getElementById("min_label");
const minPos_label = document.getElementById("minPos_label");
const comparisons_label = document.getElementById("comparisons_label");
const swaps_label = document.getElementById("swaps_label");
let comparisons_count = 0;
let swaps_count = 0;
return {
handleOk() {
const numbox = document.getElementById("numbox");
status_label.textContent = "Getting Started";
comparisons_label.textContent = comparisons_count;
swaps_label.textContent = swaps_count;
console.log('handleOk() called');
},
handleCompare() {
comparisons_count++;
comparisons_label.textContent = comparisons_count;
console.log('handleCompare() called');
},
handleSwap() {
swaps_count++;
swaps_label.textContent = swaps_count;
console.log('handleSwap() called');
}
}
}
function init() {
console.log('init called');
const handlers = createHandlers();
const ok_button = document.getElementById("ok_button");
const compare_button = document.getElementById("compare_button");
const swap_button = document.getElementById("swap_button");
ok_button.addEventListener('click', handlers.handleOk);
compare_button.addEventListener('click', handlers.handleCompare);
swap_button.addEventListener('click', handlers.handleSwap);
}
The new lines are 15-16, 22-24, 29-30 and 35-36. Line 15 adds a variable called comparisons_count that will keep track of the total number of comparisons made when performing the sort. Line 16 adds a variable called swaps_count that keeps track of the total number of swaps made when performing the sort. Note that these variables are declared using let, as their values will change as the program runs.
Line 22 sets the value of the status_label to "Getting Started". Line 23 sets the value of the comparisons_label to the value of comparisons_count which starts off at 0. Line 24 does the same thing with the swaps_label. So, when running the application, hitting Ok will set these values.
Line 29 updates the comparison_counts value using the ++ operator. This is shorthand for incrementing by one. So, the instruction:
comparisons_count++;
is shorthand for the instruction:
comparisons_count = comparisons_count + 1;
Line 30 updates comparisons_label with the incremented value of comparisons_count.
Lines 35 and 36 do the same thing for the swaps_label and swaps_count.
With these changes, you can run the application to check if it runs correctly. The following animated gif file shows this in action:
Click on Reload gif to replay animation.
As you can see, when you click Ok the status_label is updated. Then, when you click on the Compare and Swap buttons, the comparisons_label and swaps_label get updated. So, the application is working as expected.
Making use of Modules
In the previous lesson (Promises and Intro to SVG Animation), we made use of a module with the file node.mjs. This module contained the definition of the Node class. Here is the node.mjs file:
export class Node {
constructor(x, y, width, height, label, id) {
this.svgNS = "http://www.w3.org/2000/svg";
this.g = document.createElementNS(this.svgNS, "g");
this.g.id = id;
this.g.setAttribute("transform", `translate(${x}, ${y})`);
this.g.setAttribute("data-x", x);
this.g.setAttribute("data-y", y);
this.rect = document.createElementNS(this.svgNS, "rect");
this.rect.setAttribute("x", 0);
this.rect.setAttribute("y", 0);
this.rect.setAttribute("width", width);
this.rect.setAttribute("height", height);
this.rect.setAttribute("fill", "#eeeeee");
this.rect.setAttribute("stroke", "black");
this.rect.setAttribute("stroke-width", 2);
this.text = document.createElementNS(this.svgNS, "text");
this.text.setAttribute("x", width/2);
this.text.setAttribute("y", height/2);
this.text.setAttribute("text-anchor", "middle");
this.text.setAttribute("dominant-baseline", "middle");
this.text.setAttribute("fill", "black");
this.text.textContent = label;
this.g.appendChild(this.rect);
this.g.appendChild(this.text);
}
draw(svg_area) {
svg_area.appendChild(this.g);
}
moveTo(endX, endY, duration) {
return new Promise((resolve) => {
let startTime = null;
const startX = Number(this.g.getAttribute("data-x"));
const startY = Number(this.g.getAttribute("data-y"));
let currentX = startX;
let currentY = startY;
const step = (timestamp) => {
if (startTime === null) {
startTime = timestamp;
}
let progress = Math.min((timestamp - startTime)/duration, 1);
currentX = startX + (endX - startX)*progress;
currentY = startY + (endY - startY)*progress;
this.g.setAttribute("transform", `translate(${currentX}, ${currentY})`);
if (progress < 1) {
requestAnimationFrame(step);
} else {
this.g.setAttribute("data-x", endX);
this.g.setAttribute("data-y", endY);
console.log('this.g', this.g);
resolve(); // animation is done
}
}
requestAnimationFrame(step);
});
}
swap(nd, duration) {
return new Promise((resolve) => {
let startTime = null;
const x1 = Number(this.g.getAttribute("data-x"));
const y1 = Number(this.g.getAttribute("data-y"));
const x2 = Number(nd.g.getAttribute("data-x"));
const y2 = Number(nd.g.getAttribute("data-y"));
let currentX1 = x1;
let currentY1 = y1;
let currentX2 = x2;
let currentY2 = y2;
const endX1 = x2;
const endY1 = y2;
const endX2 = x1;
const endY2 = y1;
const step = (timestamp) => {
if (startTime === null) {
startTime = timestamp;
}
let progress = Math.min((timestamp - startTime)/duration, 1);
currentX1 = x1 + (x2 - x1)*progress;
currentY1 = y1 + (y2 - y1)*progress;
currentX2 = x2 + (x1 - x2)*progress;
currentY2 = y2 + (y1 - y2)*progress;
this.g.setAttribute("transform", `translate(${currentX1}, ${currentY1})`);
nd.g.setAttribute("transform", `translate(${currentX2}, ${currentY2})`);
if (progress < 1) {
requestAnimationFrame(step);
}
else {
this.g.setAttribute("data-x", x2);
this.g.setAttribute("data-y", y2);
nd.g.setAttribute("data-x", x1);
nd.g.setAttribute("data-y", y1);
resolve();
}
}
requestAnimationFrame(step);
});
}
}
You can copy this file to your project and save it as node.mjs. The Node class can be used to help us animate the Selection Sort. The reason why we place this code in the module node.mjs, is that this reduced the amount of code for our main JavaScript file, index.mjs. In addition, placing the code for the Node class in a module, allows us to reuse that module in another project, like the one we are working on here.
Important Features of Modules
Using Modules allows us to reuse code and simplify our main JavaScript file, index.mjs. Let’s look at some of the details that the node.mjs module has, that makes it useful in writing our programs.
Features of Modules:
-
Modules make it easier to reuse code in the module for more than one application. Reusing a module is far less error prone than just copying code from one file into another. In addition, if you find errors in the code of a module later on, you only have to fix the code in one location. If you had copied and pasted the code into several different project files, you would have to find and fix each of the places you pasted the code into.
-
Modules simply the main JavaScript file. Instead of having all the JavaScript code in index.mjs, spreading the files into modules makes it easier to write and debug your programs. This is especially true, if you have a JavaScript file that is designed specifically to test the code in a module. That would allow testing the module code before it is even used in a project. Keeping the index.mjs (main file) shorter makes the code for index.mjs easier to understand. This makes it easier to develop index.mjs and also easier to fix errors in index.mjs.
-
Modules make it easier to write more complex programs. This is related to the last item. When you create a class to be used for an application, the class methods are written so that the hide the details of getting something done. For example, when we created the Node class in the node.mjs module, the constructor for the class does multiple things. The constructor creates the SVG object by creating a SVG group, and then placing a SVG rect element and a SVG text element. These are not details that you want to worry about when writing the main JavaScript for the application. At the main level, you are more concerned with the overall logic of the program and how the user will interact with the program. So, those details are hidden from the person writing the main program to make that task easier. Even if you wrote the class and the main program, this still makes it easier to write the main program. So, this would be even more true if someone else is writing a main program and is just making use of the class (that someone else wrote). Think of how much easier it is to make a Node object move, by calling the moveTo() method. All you need to worry about is passing the correct x-coordinate where you want to move the Node to.
-
Modules can be used to gather together all the important variables for a class. This is very important because the class variables can be accessed by any part of a program that has access to a class object. This minimizes the amount of variables that have to be passed as arguments to functions. This also protects those class variables by being changed in some unexpected way, as you need a class instance (object) to access those variables.
Making use of the Node class
Now that we have discussed the benefits of using modules in your programs, let’s use the Node class from the node.mjs module to draw the array elements as nodes. Here is the next version of index.mjs:
import { Node } from "./node.mjs";
if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
function removeChildren(elem) {
while (elem.childNodes.length > 0) {
elem.removeChild(elem.childNodes[0]);
}
}
function createHandlers() {
const status_label = document.getElementById("status_label");
const min_label = document.getElementById("min_label");
const minPos_label = document.getElementById("minPos_label");
const comparisons_label = document.getElementById("comparisons_label");
const swaps_label = document.getElementById("swaps_label");
let comparisons_count = 0;
let swaps_count = 0;
const svg_area = document.getElementById("svg_area");
return {
handleOk() {
const numbox = document.getElementById("numbox");
removeChildren(svg_area);
let nums = numbox.value.trim().split(",");
let xStart = 30;
let yStart = 60;
for (let i = 0; i < nums.length; i++) {
const num = Number(nums[i]);
const nd = new Node(xStart + i*(50), yStart, 30, 25, num, i);
nd.draw(svg_area);
}
status_label.textContent = "Getting Started";
comparisons_label.textContent = comparisons_count;
swaps_label.textContent = swaps_count;
console.log('handleOk() called');
},
handleCompare() {
comparisons_count++;
comparisons_label.textContent = comparisons_count;
console.log('handleCompare() called');
},
handleSwap() {
swaps_count++;
swaps_label.textContent = swaps_count;
console.log('handleSwap() called');
}
}
}
function init() {
console.log('init called');
const handlers = createHandlers();
const ok_button = document.getElementById("ok_button");
const compare_button = document.getElementById("compare_button");
const swap_button = document.getElementById("swap_button");
ok_button.addEventListener('click', handlers.handleOk);
compare_button.addEventListener('click', handlers.handleCompare);
swap_button.addEventListener('click', handlers.handleSwap);
}
The new lines are 9-13, 23 and 29-37. Lines 9-13 define the removeChildren() function. This function will be used to clear the SVG container where we will draw the nodes. Line 23 gets a reference to that SVG container and stores that reference as svg_area. Lines 29-37 are the new lines needed to draw the array values as nodes. Line 29 clears out svg_area so that each time we hit the Ok button, svg_area is emptied. Line 30 reads the input from the text box, and splits that string into an array of strings using the split() function. Lines 31 and 32 set starting x and y coordinates for the first node. Lines 33-37 define an index-based for loop that will convert each of the array elements into a number and then use that to create a Node object. line 36 draws that Node object into svg_area. The following screen shot shows an example of running the application where the user has entered "4,13,7,6,22" into the input text box and hit Ok:
You can see that the code needed to get the array elements drawn as nodes on the screen is only a few lines and is relatively straightforward to understand. This shows the usefulness of having a Node class defined in the node.mjs module.
We will develop some other modules that define classes that we can use to build our web application. This will be done in the next lesson (Creating User Interactive Applications Part 2)
Summary
-
We started off creating a bigger web application that is made interactive. This app makes use of an input text box, several buttons and a number of labels. This is a significant amount of HTML markup, so we first edited the index.html file. This allowed seeing those elements displayed in the browser, before adding any code that enables those elements.
-
After completing the HTML markup in index.html, we began to edit the main JavaScript file, index.mjs. We introduced factory functions as a way to better organize a web application that has several <button> elements and a number of <label> elements that get updated when that application runs.
-
Factory functions are functions that return other functions. Factory functions are useful for avoiding the use of global variables and for setting up variables that are private to the factory function. If your web application requires a number of handling functions and HTML elements that interact with the user, using a factory function to organize the code is highly recommended.
-
Modules such as node.mjs that defined the Node class are a good idea for more complex web applications. If you find that you are working with a complex object that has multiple properties, you should consider creating a class for this and placing that class in a module. This will simplify the coding of the main JavaScript file, and allow reuse of that module for other projects. We will develop other modules in the next lesson (Creating User Interactive Applications Part 2)