Frontend JavaScript Lab
In this lab, you’ll be introduced to jQuery, a popular library that helps speed up JavaScript web development to build interactive web pages.
Overview
The goal of this lab is to create a web app that will let us play the game “Galumphing Banderwoozles.” I didn’t come up with the name: the Spring 2014 15-251 TA’s did.
Nonetheless, the rules of the game are as follows:
- You are given a board composed of 15251 by 15251 tiles.
- Any tile can be one of three colors: red, green, or blue.
- Initially, all tiles are blue, except for the tile in the top left corner, which is green.
- The object is to turn the entire board red.
- Green tiles can be clicked to turn them red. Once a tile is red, it stays red.
- Clicking a green tile also affects it’s neighbors to top, right, bottom, and left.
- When a green tile is clicked, all neighbors that are currently blue become green, and all those that are green become blue.
- Recall that the red tiles stay red.
These rules are pretty confusing. If you want to get a feel for how the game works, you can play a working demo of the game here. Simply enter a board size (say, 5 rows by 5 columns) and start clicking on the green tiles.
15251 by 15251 is pretty big, so for our purposes, we’re going to let the player decide how big to make the board. Going one step further, this tutorial in general requires you to recall the rules of this game very rarely. As long as you know what goal we’re trying to achieve (which will almost always be stated), the only struggle should be figuring out the JavaScript required to implement it.
Diving in
The code and tutorial for this lab will be hosted on GitHub. Each step is a commit, allowing you to see what changed between steps. Links will be provided to the code of the step in question. Most of the explanations will not make sense if you do not also take a look at the corresponding code, so be sure to do that.
Step 1: Initial HTML and CSS
download starter code, diff for this step, code at this step
The first step takes care of getting the starter code. You can download it using this link. The starter code should contain two files when unzipped: a file called index.html and one called styles.css.
For more information about how to create things like this, check out the Web Dev Weeks talk on HTML & CSS.
Skimming over this step’s code, you can see that we’ve included our styles.css file containing all of our CSS, as well as an external file called normalize.min.css which will also help make things appear correctly.
You can also see a rough sketch of how the app will look: we’ve got a
couple of text inputs to get the board size, we’ve got a couple of
counters to keep track of some game stats, and we’ve got the
tile-wrapper
, which will hold the code for all of our tiles.
If you’re interested in this, you should definitely check out the HTML & CSS workshop, which shows you how to come up with the styles and content of a web page.
Step 2: Load the external scripts
diff for this step, code at this step
Now that we have all the HTML & CSS in place, we need to tell the
browser where to look to load the JavaScript files that we’ll be using.
This is done with the <script>
tag.
For this project, we’ll be using two different JavaScript sources. First, we’ll load the jQuery, a JavaScript library that helps out tremendously with adding event listeners, manipulating the DOM, and doing other routine tasks. jQuery is a library written and hosted by someone else: this means we need to go grab an external link to it. To find it, search the Web for “Google jQuery CDN”. (A CDN, or content delivery network, is basically a super-fast network designed for hosting commonly-requested files.) On the first result page, you should see a large list of Google hosted libraries:
- Find the one titled jQuery,
- Copy the
<script>
tag under it, and - Paste it into the file
index.html
.
Now that we’ve loaded our helper JavaScript library, we need to load the
file which will contain our core application code. We’ll be calling this
file main.js
, so add a <script>
tag that looks similar to the one
you just copied, but with a source of src="main.js"
instead:
<script src="main.js"></script>
With these lines in place in index.html
, we should be good to go! Now
we can start writing the core JavaScript code for our game.
Step 3: Add starter code in main.js
diff for this step, code at this step
We’re finally at the point where we can add some code to our main.js
file!
With the first few lines, we get the opportunity to see one of the many quirks about JavaScript: something that’s not really necessary but that makes life a little bit easier.
We’re going to add the following lines to our JavaScript file:
;(function() {
})();
If none of this makes sense, just copy the code into main.js
and move
on. It’s not worth stressing over for this lab.
What these lines do is terminate any previous, dangling JavaScript
statement (with the first semi-colon), create an anonymous function
(with the function
keyword), and then immediately call it (with the
()
towards the end). This ensures that we have our own execution
context for all the code that we write. You can read more about it
here,
but you’re almost always going to want to follow this convention when
writing JavaScript files.
Step 4: Listen for ready event
diff for this step, code at this step
If you’re familiar with a language like C/C++ or Java, you’ll know that
every application has an entry point that’s usually called main
. In
Python there’s a similar concept using globally defined variables like
__name__ == '__main__'
.
JavaScript has no such entry point. When JavaScript is run in a browser, each line is run as it is encountered. While this is useful sometimes, a lot of the time what we actually want is for our JavaScript to be run after the page has finished loading; i.e., when it’s “ready” for us.
To make this happen, we use event listeners to run a particular
function once a corresponding event occurs. Events are generated for
many different actions and conditions within the browser; one of these
is the ready
event, which is fired on the document
object whenever
the page is, well, ready for us.
We can attach a handler, or a specialized function, to this event as follows. It effectively allows us to run a piece of code whenever the event fires. When writing event handlers, people tend to use anonymous functions:
$(document).ready(function() {
// empty
});
Attaching a function to an event and having it be run at a later point is an incredibly common and unusually powerful paradigm in JavaScript. We call functions used like this callbacks.
What this does is attach an empty function to the ready
event of the
document
element. Note the funny $(document)
syntax at the
beginning. This actually calls the function named $
with an argument
of document
, a global variable representing the page. $
is a
function defined by jQuery that lets us do tons of things, including
attach event handlers like we do with the .ready()
method in this
example.
Step 5: Add play
function
diff for this step, code at this step
Now that the “ready” event listener is in place, we can add some boilerplate functions where we’ll write our core game logic.
Make sure you’re also following along in the diff for this step!
First, we’ll need a function called play
. Right now, it’ll remain
empty. This function will also be an event handler, so it’ll take a
single argument representing the event that triggered the function to be
called.
You may notice that the anonymous function we registered on the ready
event from the last step was also an event handler. A logical question
to ask might be, “Why didn’t our ready handler have to take an argument
e
?” The answer is that we could have declared our handler like this,
but the body of that handler isn’t going to need to use it, so we just
leave it out. JavaScript won’t complain if the number of arguments a
function is called with and how many it’s declared with don’t match up.
For more information, see here.
Now, with our empty play
function in place, we’re going to add it as
an event handler on the click
event of the submit button. This button
has an id
of submit
(check the HTML!), so we can use
jQuery to select that element and bind the click event on that element
to our play
function.
Here we see our second use of $
, the jQuery function. You should be
thinking of this function as “selecting” the element specified by its
argument.
Step 6: Get the user’s input for rows and columns
diff for this step, code at this step
So now we have a function that will be executed every time the user clicks on our submit button. Let’s start filling it in with something useful.
We said one of the things we’d have to do in order to set up the game state was to let the user choose how many rows and columns there should be. We can do just this using the two text inputs already present in the page.
By the time the user clicks on our submit button and fires off the
play
function, the text inputs with id’s of rows
and cols
will
be holding values corresponding to the number of rows and columns that
our board should have. We can use jQuery to extract these values, using
the .val()
function. We’ll go ahead and store these values in global
variables.
Question: what should happen if the user clicks “submit” twice? Should we start a fresh game? Add or remove rows from the game in progress? There are many ways we could handle this situation.
If you tried this but clicking “Play!” doesn’t do anything, try reading the next step :P
For sake of simplicity, we’re just going to remove the submit button and
both text inputs entirely. After selecting the form with id args
that
wraps the text inputs and submit button, we can use the .detach()
method to remove them from the DOM.
Step 7: Prevent form submission event from bubbling up
diff for this step, code at this step
- Prefix the "//cdnjs..." URL in index.html with "http": "http://cdnjs..."
- Do the same with the jQuery link in index.html
Once make the changes introduced in the previous step, go ahead and see
if clicking the button removes the #args
form. The best way to
preview your work is to start a simple Python webserver running in your
project’s directory with
$ python -m SimpleHTTPServer
then open http://localhost:8000 in your browser. If you need help with this, flag down a mentor! Otherwise, just open the index.html file in your browser.
What you’ll probably notice is that the form didn’t disappear, and it looked like the website reloaded the page. If you look carefully, you’ll probably also see that the URL changed to something with variable names, question marks, and ampersands. This happened because of something called “bubbling” which is another JavaScript feature which can seem like a quirk at times.
Whenever there is a button inside a form and the button is clicked, the
browser thinks that the values of the inputs within that form need to be
submitted. If you don’t specify where to submit this information on the
<form>
tag, then it assumes that you want it to be submitted to the
current URL, effectively reloading the page.
This is pretty annoying, because we’d actually like to run our own code
instead of having the browser do something for us. To turn this feature
off, we have to understand what’s actually going on. Nearly every action
a user has with a page generates some sort of event. Even though an
event might have a particular element from which it originated, the
event it self propagates up through the DOM tree, from that element to
it’s parent element, all the way until it reaches the <html>
tag. This
is what’s known as “bubbling”, because the event bubbles up the DOM.
Our particular event handler is listening on the button itself, whereas
the form submission event is attached to the form element; we want to
shut off the bubbling before it can get from the button to the form. To
do this, we can use the argument e
, which gives us information about
the current event. We can stop the “default” action (i.e., the event
bubbling upwards) from occurring by calling e.preventDefault()
.
Try adding this line and see what happens. You should see that
the #args
form detaches and stays detached. If not, flag down a
mentor!
By now, you’re probably starting to understand that there are a lot of idiosyncrasies in JavaScript whose solutions are not obvious. In cases like these, your best debugging tool is the ability to carefully describe what problem you’re having and searching for it on Google. At least when you’re starting out, it’s likely the problems you will experience will have relatively simple, though non-intuitive, solutions. Being able to Google effectively is invaluable in times like these.
Step 8: Initialize the game’s starting state
diff for this step, code at this step
Now that we can accept the user’s input with our form, we can actually begin setting up the initial board configuration. Specifically, the user has told us how many rows and columns to use, so we can use a nested for loop to set up the grid with the right dimensions.
Throughout this step, it’s a good idea to have a picture of how this step is supposed to work. Try drawing it yourself based on this description, or ask a mentor for clarification!
Since HTML elements are display and drawn across and then down, we’re
going to draw one row at a time (as opposed to one column at a time). To
make things easier, we’re going to wrap each row in a div with the class
tile-row
so that we can access arbitrary rows.
To create a new DOM element (basically, an element in the page), we use
the jQuery function ($
) with a string argument representing the HTML
that we should use to construct that object. We just need to create a
<div>
with a class of tile-row
, so the string we want is '<div
class="tile-row"></div>'
. We can then store this in a variable named
$curTileRow
(note that $
is a perfectly valid character to use in a
JavaScript variable name). Finally, we use the jQuery append method to
append this tile row to the tile wrapper.
We’ll be using a multidimensional array to maintain what color each
board piece is. JavaScript arrays aren’t fixed in length (because
they’re actually just special objects), so we’ll be dynamically adding a
row every time we need a new one. What that means for us is that within
the outer loop, where code will be executed once per row, we should add
a row to our multidimentional array. This is done with the code
boardColors[curRow] = [];
, which sets the contents of the curRow’th
row to an empty array.
Now that we’ve done the required setup for each row, we can work on adding the individual tiles that will ultimately constitute the columns. We’ll add a for loop inside our outer for loop that ranges over the entire number of columns.
The first thing this inner loop should do is create the DOM element for
the tile residing in that column. This is done with the jQuery append
method that we used before to add a row to the outer, wrapper div. Next,
since all but the top left tiles should start out blue, we’ll set the
color of this tile to blue (and worry about the one green one later).
I’ll use a helper function called setColor
to set the color of a tile.
It’ll take a row, column, and a string representing a color. We’ll write
this function after the next step. For now, just know that it takes care of
setting the color for us.
Finally, we’ll have to set the click event on the div to handle what should happen when the user “makes a move,” or attempts to change the color of a tile. This part is actually very sophisticated! We’re going to be using a technique called closures, which is a very powerful concept that is used quite frequently in JavaScript.
The basic paradigm stems from the fact that JavaScript maintains a context of all the variables and their values for every function that is created. That means that we can create special contexts that bind additional variables that a function would find useful to use.
First you’ll note the helper function getTile
. We’ll write this helper
function in after next step as well, for but for now just now that it’s
basically a wrapper around the $
function that we’ve been using this
whole time. What that means is that it returns to us the same jQuery
DOM object that we would normally get back, so we can set the .click
attribute of that result to bind a function to the click event of that
particular DOM element.
Next, we have an anonymous function wrapped in parentheses,
(function(r, c) {…}), followed by (curRow, curCol)
. When you see
this, a function definition wrapped in parentheses immediately followed
by parentheses, this will create an anonymous function and immediately
call it with the arguments specified in the second set of parentheses.
This is called a self executing function, and you can read more about it
here.
Remember that the argument to the .click
function needs to be a
function which takes one variable: the event object e
that triggered
the click event. We’re actually going to use this self-executing
function to return a different function. What’s special about this
returned function is that it will have access to the parameters of the
outer function, namely the current row and current column. We can then
reference those inside our returned function; in our case, we’re going
to pass them on to move
, a third helper function that will take care
of the brunt of the game logic for us. We’ll start writing that function
in three steps.
Since the function we pass to click must take an event object as an
argument, we’ll make the function we return take just that argument, and
we’ll pass it on to move
just in case we need it.
Somehow this step ended up being much, much longer than I wanted it to be. It’s much easier to explain in person, so if it doesn’t make sense as stated, be sure to flag someone down.
Phew, that was a long step, but that’s it for the processing we need to do within the loop! If you remember from before, I said that we’d take care of changing the upper left tile to green later, so let’s go ahead and do that now. Remember that we use a helper function for this before, so we’ll use the same function again now.
Step 9: Set the initial values on the “scoreboard”
diff for this step, code at this step
One last thing to take care of before we start going nuts with our game is to set up number of greens, blues, and total tiles, and then display these values.
We’ll initialize a few global variables called reds
, greens
, and
blues
to 0, set greens
to 1 (for the upper left tile), and set
blues
to the remaining number of tiles. Obviously, the total number of
tiles is rows * columns
, and this will be important because we will
know the user has won if the number of reds is equal to this value.
Finally, we’ll use a couple more jQuery calls to display these values.
The .html
function on a jQuery object will set the inner HTML of that
attribute to the text specified (if you pass something that’s not a
string, it will try and cast it to a string). That means we can use this
method, passing it blues
and greens
respectively, to set these
colors to the right values. Remember that the HTML elements with id’s
blues
and greens
house the data we want to display. Note that we
don’t have to worry about setting the number of reds because it should
already be set to 0 in the HTML (you can verify this).
Step 10: Define a couple useful helper functions
diff for this step, code at this step
As I mentioned two steps ago, we’re going to now define the helper function we used to both set a tile’s color given its indices, and to get the DOM node of an element given its indices.
The first of these, getTile
, is a one-liner, though it is a bit
complicated. We’ll be using the jQuery/CSS :nth()
pseudo-selector,
which basically allows us to access the nth element of a particular
class. We’ll also be using the >
selector, which selects the children
of a particular node. Remember that we wrapped every row in a div with
the class tile-row
, so to get the nth row we can use `.tile-row:nth(‘
- row + ‘)
. Then we want to access an individual tile (an element with the class
tile), which is a child of a
tile-row. That means the next selector needs to be a
>. Finally, we want the nth tile, so in a similar fashion to before, we want to use
.tile:nth(‘ + col + ‘). If you hadn't already figured it out, the
:nth()` pseudo-selector takes as an argument an integer which represents the index of which element it should select.
Next up is setColor
. We have two things to do: update our internal
representation of the colors of the tiles, and then actually change the
color of the element. Remember that we’ve been storing our internal
representation of the board’s colors in a multidimensional array called
boardColors
. We want to access the element at [row][col]
and set
it’s contents to color
. We’ll use this stored value later in
processing what to do when the user clicks on a tile.
The second thing to do is change the actual color of the tile. In the
CSS stylesheet (styles.css), there are a bunch of handy classes that
will turn our element a particular color depending on which class is
applied. First, we want to get the DOM element represented by the row
and column we are dealing with, which is simple enough because we have a
helper function to deal with that! We’ll next use the .removeClass
jQuery method to remove any previous color class that had previously
been attached to this element, and then the .addClass
method to add
the class corresponding to the color we want to make this tile.
There are a couple of things to note here. The first is that if
removeClass
doesn’t find the class you specified to remove, it doesn’t
do anything. The second is that you can chain jQuery calls. Note that
there are no semicolons after each line, only the last line. Every time
we call a jQuery method that’s operating on a DOM element like this, we
can chain as many calls as we want on top of each other to keep
modifying the current jQuery DOM element.
Step 11: Start writing move function: get adjacent tiles
diff for this step, code at this step
We’re approaching the home stretch! move
is the last function we’ll
need to implement.
Recall that move
will be called every time someone clicks a tile
element, and it will have access to the current row and column because
of the closure we used when initializing the click event. We’ll use this
method to handle processing all the rules of the game that we outlined
in the Overview.
In the rules, we specified that the only tiles we can actually click on are the green ones, so let’s check if the current tile is green.
Remember that we have access to the row and column of the clicked tile
from the parameters to the move
function, and we have a
multidimensional array which will give us the color of a tile given a
row and a column. Once we have this stored in a variable, we can check
to see whether that color is 'green'
.
If it is green, we need to change it’s color to red, increment the number of reds, and decrement the number of greens.
Step 12: Get and check whether the neighbors are valid
diff for this step, code at this step
Once we’ve toggled the current tile to red, we need to process the current tile’s neighbors. If we know what the current row and column are, we can easily get the row and column of the neighbors by adding or subtracting one to the appropriate variable.
Note that some of these cells will actually not be valid; for example, if we are in the 0th row, trying to access the 0 - 1 = -1th row will be invalid. We can check whether a variable is valid by checking the appropriate condition… beware of off by one errors!
To make things easier, we’re going to utilize the fact that JavaScript
variables are dynamically typed. Since we don’t have to declare a
variable’s type, we could assign either a jQuery DOM element or null
to a variable and let the JavaScript interpreter determine this at
runtime. Combining this with the JavaScript ternary operator,
<condition> ? <value if true> : <value if false>
, we can very
conveniently check the edge conditions, get the neighbor if safe, and
evaluate to null
if not.
Once we’ve done this, I’m going to stick all four variables into an array with their corresponding coordinates so that we can conveniently iterate over the array, and do the same processing for all four.
Step 13: Use Array.forEach to iterate over each neighbor
diff for this step, code at this step
This time, when iterating over each potential neighbor, we’re going to
use a different style of JavaScript for loop called Array.forEach
.
This is a function present on all JavaScript arrays that allows you to
loop over all the elements of a list without using indices.
The way this works is you specify a function to the .forEach()
method.
This function takes as parameters the current element of the list, the
index of the current element, and the a reference to the array the
function was called on (in our example this isn’t necessary because we
have access to the original array variable, but there are times when
this isn’t the case). Since this function is a callback, it will be
evaluated once for each element of the array, and the appropriate values
will be substituted in.
Step 14: Process each neighbor within Array.forEach
diff for this step, code at this step
Within the forEach callback, we want to check to make sure that the
current neighbor we’re processing exists, i.e. is not null. Since the
actual element we’re iterating over is an object containing the
properties tile
, row
, and col
, we need to check the
$neighbor.tile
property to see if the element itself exists. Since
JavaScript values are all truthy, an object is only ever falsy if it’s
null, we can simply use if($neighbor.tile)
to check whether the
neighbor exists or not.
Now that we know whether the neighbor exists, we can do the appropriate
processing. Particularly, what we need to do depends on the color of the
current element. Let’s get the color of the current neighbor using the
same method we did for the clicked tile (making care to draw the row and
column from the properties we set on the current $neighbor
variable).
Now we can case on the color of the current element. If it’s green, we’ll need to toggle it’s color to blue and adjust the number of blues and greens, and vice versa it the neighbor is blue.
Step 15: Update scoreboard and check for winning conditions
diff for this step, code at this step
The only thing left to do is update the scoreboard and check whether the player has won the game.
If you recall the code to update the scoreboard from early, we will use the same code now, but add an additional line to update reds (because reds will have definitely changed this time).
If the number of reds is equal to the total number of tiles, that means
that every tile has been flipped, so the user has won. If the number of
greens is 0, then the user lost, because there aren’t any tiles that can
be flipped. There’s a JavaScript function alert
that lets you display
a message in a dialog box, so we can use this to report a win or a loss.
It turns out that some boards are winnable and some boards aren’t (if you’re interested you should try to prove it). Regardless of this, I thought it’d be funny when I made this game and distributed it to my fellow 251 classmates to tell them that a non-winnable board was in fact winnable… it had some interesting consequences. Feel free to change the winning and losing messages to something more “agreeable.”
The final thing we’ll take care of is reload the page for if the user
want to play again. We can do with with location.reload()
, which will
take the current page’s URL and reload that addresss.
That’s it! You should be able to try everything out and have it work in the browser. If you’re still stuck, or you’ve got errors, flag down a mentor or ask someone for help and we’d love to give you some hints to fix everything up.
Thanks!
I take great pride in writing good code, documentation, and tutorials. If you spot something that’s not quite right or you spot something confusing, let me know! Open an issue on https://github.com/ScottyLabs/wdw.
Written by Jake Zimmerman, July 2014