Make an active site
- By Rob Miles
- 7/3/2023
- Get input from a user
- Storing data on the local machine
- JavaScript heroes: let, var, and const
- Making page elements from JavaScript
- What you have learned
Making page elements from JavaScript
We have seen how the Document Object Model (DOM) is built in memory by the browser, which uses the contents of the HTML file that defines the website. The browser then renders the DOM to display the content of the pages for the user. We’ve also seen how a JavaScript program can interact with the elements in the DOM by changing their properties and how these changes are reflected in what the user sees on the page. We used this to change the time displayed by a paragraph in the clock.
Now we will discover how a JavaScript program can create elements when it runs. This is a very important part of JavaScript programming. Some web pages are built from HTML files that are entirely JavaScript code. When the page loads, the JavaScript runs and creates all the elements that are used in the display. We will show how this works by creating a game called Cheese Finder. It turns out to be quite compelling.
Figure 3-3 shows the Cheese Finder game, which is played on a 10x10 grid of buttons. One of the buttons contains a piece of cheese. Before you start, you agree whether you are playing to find the cheese or avoid it (if you don’t like cheese). Then each player, in turn, presses a button. If the button does not contain the cheese, it turns pink and displays the distance that square is from the cheese. If the button is the cheese, a message is displayed, the cheese button turns yellow, and the game is over. Reloading the page creates a brand-new game and moves the cheese to a new location.
FIGURE 3.3 Cheese Finder game
Cheese Finder
You can have a go at the game by visiting the example page at
https://begintocodecloud.com/code/Ch03-Build_interactive_pages/Ch03-07_Cheese_Finder/index.html
Place the buttons
To make the game work, we need a web page that contains 100 buttons. It would be very hard to make all these buttons by hand. Fortunately, we can use loops in a JavaScript program to make the display for us. Below, you can see the paragraph that will contain the buttons:
<p id="buttonPar"> </p>
This paragraph is empty in the HTML file. The buttons will be added by a function called when the page is loaded. The paragraph has the buttonPar id so our code can locate it in the document.
This is the function that creates the buttons and sets the game up. Let’s work through what it does.
The statement below creates a local variable called container, which refers to the paragraph that will contain all the buttons on the page. The paragraph has the id buttonPar:
let container = document.getElementById("buttonPar");
These two statements create a pair of for loops, one nested inside the other. The outer loop will be performed for each row of the button grid. The inner loop will be performed for each column in each row. The y variable keeps track of the row number, and the x variable keeps track of the column number.
for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) {
This next statement is something we’ve not seen before. The document object provides a method called createElement that creates a new HTML element. We specify the kind of element we want by using a string. In this case, we want a button. Note that creating an element does not add it to the DOM; we must do that separately.
let newButton = document.createElement("button");
The following statement sets the className for the button, determining the style used to display the button:
newButton.className = "upButton";
Following are the styles that are used for the buttons. There are some common style items (the font family, alignment, and minimum width and height) along with different colors for each button state.
.upButton,.downButton,.cheeseButton { font-family: 'Courier New', Courier, monospace; text-align: center; min-width: 3em; min-height: 3em; } .upButton{ background: lightblue; } .downButton { background: lightpink; } .cheeseButton { background: yellow; }
This statement sets the initial text content of the button, which will be replaced by the distance value when the button is clicked.
newButton.textContent = "X";
The following two statements set up a couple of attributes on the new button that give the button’s location in the grid. We are going to bind a function to the button’s onclick event. We don’t want to create a different function for each button press because that would mean creating 100 functions. Instead, we want to store location values in each button so that a single button function can work the position of a particular button. We have already written code that sets existing attributes on an element (to change the class or the textContent of a paragraph). The two statements below create attributes called x and y that contain the x and y positions of the button. This is a very powerful technique. It makes elements in the DOM an extension of your variable storage.
newButton.setAttribute("x", x); newButton.setAttribute("y", y);
The last statement that sets up the button is below. It binds the doButtonClicked method to the button’s onClick event. If the button is clicked, this function will run. All buttons will call the same function when they are clicked. You might wonder how the doButtonClicked function will know which button has been clicked. Let’s look at the JavaScript statement assigned to onClick to find out how this works.
newButton.setAttribute("onClick", "doButtonClicked(this);");
When executed in the context of a JavaScript statement running from HTML, the value of this is set to a reference to the element generating the event. Each time doButtonClicked is called, it will be given an argument that refers to the button clicked. This is terribly useful. It makes it very easy for an event handler to know which element caused the event.
doButtonClicked(this);
If you are having trouble understanding what is happening here, remember the problem that we are trying to solve. We have 100 buttons. Each button can generate an onClick event. We don’t want to make 100 functions to deal with all these onClick events. It’s preferable to write just one function. But if we only have one function, it needs to know which button it has been called from. The this reference is an argument to the doButtonClicked call that is fed into the function when the button is clicked. In this context, the value of this refers to the button that has been pressed. So, doButtonClicked is always told the button that has been clicked. This will make more sense when we look at what the doButtonClicked function does. Way back at the start of this description, we set up a container variable, which was a reference to the paragraph that will hold all our buttons. The container provides a method called appendChild, which is given a reference to the new element and adds it. This means that the paragraph now contains the newly created button. New elements are appended in order. So, the first element will be button (0,0), the second element will be button (0,1), and so on.
container.appendChild(newButton);
The following two statements are performed after we have added all the buttons in a row. They create a break element (br) and append it to the paragraph container. This is how we separate successive rows in the grid:
let lineBreak = document.createElement("br"); container.appendChild(lineBreak);
Figure 3-4 shows how we can use the Elements tab from the Developer Tools to look at all the buttons that our code has created. Remember that the original HTML for the page did not contain any buttons. These have all been created by our code. You can see that all the buttons have the properties that you would expect.
FIGURE 3.4 Cheese Finder buttons
Place the cheese
The next thing the game needs to do is place the cheese somewhere on the grid. For this, we need random numbers. JavaScript has a random number generator that can produce a random value between 0 and 1. It lives in the Math library and is called random. We can use this in a helper function to generate random integers in a particular range:
function getRandom(min, max) { let range = max - min; let result = Math.floor(Math.random() * (range)) + min; return result; }
The getRandom function is given the minimum and maximum values of the random number to be produced. It then creates a value between the two values. The maximum value is an exclusive upper limit and is never produced. The function uses Math.random to create a random number between 0 and 1 and Math.floor to truncate the fractional part of a number and generate the integer value we need.
cheeseX = getRandom(0, width); cheeseY = getRandom(0, height);
These two statements set the cheeseX and cheeseY variables to the cheese’s position. These variables have been made global, so they are shared between all game functions.
var cheeseX; var cheeseY;
Making these values global makes the game a bit less secure, but it also keeps the code simple.
Respond to button presses
The final necessary behavior is the function that responds to a button press. If you look at the buttons’ definitions in Figure 3-4 earlier in this chapter, you will see that the onClick attribute of each button makes a call to the doButtonClicked function. Let’s have a look at this function:
The doButtonClicked function has a single parameter, which refers to the button that has been clicked. This is obtained from the this reference, which is added when the event is bound in the element definition.
The first two statements in the function read the values in the button’s x and y attributes. These give the button’s grid location:
let x = button.getAttribute("x"); let y = button.getAttribute("y");
The next set of statements checks to see if this button is at the location of the cheese. If both the x and the y values match, the statements set the button’s className style to "cheeseButton". This causes the button to turn yellow, and an alert is displayed, telling players the game is over.
if (x == cheeseX && y == cheeseY) { button.className = "cheeseButton"; alert("Well done! Reload the page to play again"); }
The final part of this function is the behavior that is performed if the button is not the cheese. The first three statements use the laws of Pythagoras (the square of the hypotenuse is equal to the sum of the squares on the other two sides of a right-angled triangle) to work out the distance from this button to the cheese. It then sets the text content of the button to this value and changes the style to downButton, which turns the button red.
else { let dx = x - cheeseX; let dy = y - cheeseY; let distance = Math.round(Math.sqrt((dx * dx) + (dy * dy))); button.textContent = distance; button.className = "downButton"; }
Playing the game
The game is quite fun to play, particularly with two or more opponents. If you want to make the game larger (or smaller), you just change the call of playGame, which is bound to the onload event in the HTML body. This is where the number of rows and columns is set:
<body onload="playGame(10,10);">
The grid is made up of rows of buttons separated by line breaks. If the user makes the browser window too small, the rows of buttons wrap around. We could fix this by displaying the buttons in an HTML table element. We could create the table programmatically (as we have done when creating the buttons) and then add elements to the table to make the required rows and columns.
Using events
The present version of Cheese Finder works perfectly, but there is a neater way of connecting events to JavaScript functions. Currently, we are using this statement to connect an event to an object:
newButton.setAttribute("onClick", "doButtonClicked(this);");
This works by creating an onClick attribute on a new button and then setting it to a string of JavaScript that calls the desired method (and uses this to provide a pointer to the button being clicked). This works because it is exactly what we would do if we set the event handler for an element in the HTML file. However, this is not the best way to do it when we are creating an HTML element from JavaScript code.
The major limitation of this technique is that we can only connect one event handler. We might have a situation where we want several events to fire when the button is clicked, but this is not possible because an HTML element can only have one of each attribute. However, we can use a different mechanism to connect the button click handler.
The statement below uses the addEventListener method provided by the newButton object to add an event listener function to a new button. The string specifies the event’s name; in this case, the event we want is “click.” The second parameter is the method’s name to be called when the button is clicked—buttonClickedHandler. After the event listener has been added to the function, buttonClickedHandler will be called when the button is clicked.
newButton.addEventListener("click", buttonClickedHandler);
In the event handler code shown earlier, we used this to deliver a reference to the button that has been clicked. How does the buttonClickedHandler function know which button it is responding to? Let’s look at the function code:
The buttonClickedHandler function is declared with a single parameter called event, which describes the event that has occurred. One of the event object properties is called target. This is a reference to the element that generated the event. The buttonClickedHandler function extracts this value from the event and sets the value of button to it. The function then works in the same way as the earlier version. You can find this code in the Ch03-08_Cheese_Finder_Events example.
Improve Cheese Finder
The game is quite fun, but you might like to make some improvements. Here are some ideas for things that you might like to do:
You could add a counter that counts the number of squares visited. Then you could have a version where the aim is to find the cheese in the smallest number of tries.
You could add a countdown timer, so a player must find the cheese in the shortest time (clicking as many squares as they like).
You could change the way the distance to the cheese is displayed. Rather than putting a number in the square, you could use a different color. You would need to create 10 or so new styles (one for each color), and then you could use an array of style names that you index with the distance value to get the style for the square. This might make for some nice-looking displays as the game is played.