Lesson Weekend

Goals

We're going to practice using node modules to create a JavaScript application a little similar to one we previously built. This project is called "Ping-Pong". It's a bit like the "Beep Boop" project we did in Intro to programming; we insert a number, and the application counts up to that number in sequence, replacing certain types of numbers in this sequence with words.

Here are the objectives for Ping Pong:

Create a web application that takes a number from a user and returns a range of numbers from 1 to the chosen number with the following exceptions:

* Numbers divisible by 3 are replaced with "ping"
* Numbers divisible by 5 are replaced with "pong"
* Numbers divisible by 15 are replaced with "ping-pong"

First we will put together the file structure and logic for the project. After the project is functional we will refactor our JavaScript into multiple files, separating our business logic and user interface. Then, we will apply Object Oriented techniques to create a Calculator object. This will include our pingPong() function as a method which can be called on a Calculator object.

These steps can be used as a guideline for the classwork problems this week. We start with some basic HTML and jQuery, creating functions to take care of whatever logic is necessary. Then, we refactor by using objects and splitting our JavaScript into multiple files, separating the user interface and business logic.

HTML

The Ping-Pong app looks like this. I have a project folder called ping-pong, containing the following index.html file:

index.html
<!DOCTYPE html>
<html>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
    <script type="text/javascript" src="js/pingpong.js"></script>
    <title>Ping Pong</title>
  </head>
  <body>
    <form id="ping-pong-form">
      <label for="goal">Enter a number:</label>
      <input id="goal" type="number">
      <button type="submit">Submit</button>
    </form>
    <ul id="solution"></ul>
  </body>
</html>

JavaScript

The project directory also contains a js folder containing a pingpong.js file that looks like this:

js/pingpong.js
function pingPong(goal) {
  var output = [];
  for (var i = 1; i <= goal; i++) {
    if (i % 15 === 0) {
      output.push("ping-pong");
    } else if (i % 3 === 0) {
      output.push("ping");
    } else if (i % 5 === 0) {
      output.push("pong");
    } else  {
      output.push(i);
    }
  }
  return output;
}

$(document).ready(function() {
  $('#ping-pong-form').submit(function(event) {
    event.preventDefault();
    var goal = $('#goal').val();
    var output = pingPong(goal);
    output.forEach(function(element) {
      $('#solution').append("<li>" + element + "</li>");
    });
  });
});

In our index.html file, we load jQuery using the Google CDN (content delivery network), and we are loading pingpong.js, which holds our pingPong() function, and the code to display its output. Note that we are using the standard lower camelCase when naming our function: It is named pingPong() not PingPong(), pingpong(), or ping_pong(). It is a good idea to get used to this standard early.

If we run our index.html file in the browser, we can enter any integer into the form and then see the numbers from 1 up to the goal printed to the screen inside of our <ul id="solution"></ul>, with numbers divisible by 3 replaced by "ping", numbers divisible by 5 replaced by "pong", and numbers divisible by both replaced by "ping-pong".

Separation of Front-End and Back-End Logic

This all looks pretty straightforward, but all our logic is squished into one file! This is a fine way to begin a project, but now that our basic functionality is present, let's clean this up.

As we discussed in Intro to Programming, our front-end should only contain code responsible for communicating with users. It should gather form values, validate and interpret them, and pass the user input to the back-end file. This is why the front-end is often referred to as the user interface.

The back-end should handle 'behind the scenes' actions, all the heavy lifting such as mathematical calculations and any API calls (which we will learn about later!).

In Intro to Programming we placed all JavaScript into one file, and separated front and back-end logic into different areas. However, now that our programs are growing in complexity, we'll separate our front-end logic and back-end logic into separate files entirely. We'll learn how to link them together by creating something called Node Modules, using a special keyword called exports .

Now, technically, all our JavaScript will be provided to the browser after we package it with the build tools we will learn about shortly. Not just the front-end. But we are separating our logic for organizational reasons. Remember, we want to write clean, well-organized, professional code. Because user interface and business logics have separate purposes, their code should always be separate. This might seem like a lot of work for a simple Ping-Pong app, but we are practicing on smaller, more simplistic applications before jumping into more complex projects.

Exporting Modules

Let's start by splitting out the part of js/pingpong.js responsible for the user interface, from the part responsible for the logic. We'll create a new file called js/pingpong-interface.js and move this code over there:

js/pingpong-interface.js
$(document).ready(function() {
  $('#ping-pong-form').submit(function(event) {
    event.preventDefault();
    var goal = $('#goal').val();
    var output = pingPong(goal);
    output.forEach(function(element) {
      $('#solution').append("<li>" + element + "</li>");
    });
  });
});

Now that we have our front-end interface file, our back-end js/pingpong.js file should only contain the following:

js/pingpong.js
function pingPong(goal) {
  var output = [];
  for (var i = 1; i <= goal; i++) {
    if (i % 15 === 0) {
      output.push("ping-pong");
    } else if (i % 3 === 0) {
      output.push("ping");
    } else if (i % 5 === 0) {
      output.push("pong");
    } else  {
      output.push(i);
    }
  }
  return output;
}

Much cleaner! But, sadly, this doesn't work in the browser anymore. This is because our script tags are only referencing js/pingpong.js. Let's add an extra script tag for our interface file, so the browser may have access to both:

index.html
<!DOCTYPE html>
<html>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
    <script type="text/javascript" src="js/pingpong.js"></script>
    <script type="text/javascript" src="js/pingpong-interface.js"></script>
    <title>Ping Pong</title>
  </head>
  <body>
    <form id="ping-pong-form">
      <label for="goal">Enter a number:</label>
      <input id="goal" type="number">
      <button type="submit">Submit</button>
    </form>
    <ul id="solution"></ul>
  </body>
</html>

That's great, but now let's pretend this is part of a larger program.

Using Objects in Multiple Files

Suppose we are building a calculator app. Let's also suppose that we want our users to be able to choose some pretty looking skins for the appearance of their calculator, such as "retro green and black" or "hot pink". They will also want their calculator to perform the pingpong calculation and display the results on the screen.

Let's use our Object Oriented techniques to create a blueprint for Calculator objects in our back-end file. We'll add a constructor, and then we will use a prototype to turn our pingPong() function into a method which can be called on instances of a Calculator object. Our constructor will take one argument: skinName. For the purposes of our example, this would be the string like "retro green and black" or "hot pink" representing which picture to load for our calculator. We'll store it in a property named skin.

We are really just using this skin property to practice creating a constructor. Later, we could have our users choose an option for which skin they want from a menu, and this could trigger a new Calculator object to be instantiated. But for right now, we don't want to get distracted by user interfaces - we want to practice using object declarations in this new project structure. Instead we'll continue using our regular ping-pong form to trigger instantiating a Calculatorobject, we'll have its skin property hard coded to "hot pink". Then, we'll makepingPong() a method of the Calculator object.

Here is our new version of the back-end file refactored to allow us to create objects rather than just holding a single bare function:

js/pingpong.js
function Calculator(skinName) {
  this.skin = skinName;
}

Calculator.prototype.pingPong = function(goal) {
  var output = [];
  for (var i = 1; i <= goal; i++) {
    if (i % 15 === 0) {
      output.push("ping-pong");
    } else if (i % 3 === 0) {
      output.push("ping");
    } else if (i % 5 === 0) {
      output.push("pong");
    } else  {
      output.push(i);
    }
  }
  return output;
}

Now, if we submit the ping-pong form in the browser, we get an error in the console saying that pingPong is not defined. That makes sense because the pingPong function has become a method which must be called on a Calculator object, rather than a bare function. It is part of the Calculator object declaration because we used a prototype, so we can't just call var output = pingPong(goal);. We need to create a Calculator object in our front-end file next, then call its pingPong method.

js/pingpong-interface.js
$(document).ready(function() {
  $('#ping-pong-form').submit(function(event) {
    event.preventDefault();
    var goal = $('#goal').val();
    var simpleCalculator = new Calculator("hot pink");
    var output = simpleCalculator.pingPong(goal);
    output.forEach(function(element) {
      $('#solution').append("<li>" + element + "</li>");
    });
  });
});

We have created a Calculator object, and passed our goal number from our form to its pingPong method, which should give us our array of output to print to the screen.

This is where Node comes in. We are going to learn how to export the blueprint for our Calculator objects by turning it into a Node module. Incidentally, this would allow us to run tests on it in the Node environment from the command line if we wanted to. Add this line to the bottom of the file:

js/pingpong.js
function Calculator(skinName) {
  this.skin = skinName;
}

Calculator.prototype.pingPong = function(goal) {
  var output = [];
  for (var i = 1; i <= goal; i++) {
    if (i % 15 === 0) {
      output.push("ping-pong");
    } else if (i % 3 === 0) {
      output.push("ping");
    } else if (i % 5 === 0) {
      output.push("pong");
    } else  {
      output.push(i);
    }
  }
  return output;
}

exports.calculatorModule = Calculator;

exports is provided by Node and lets us export things from one file and bring them into another. Technically, our constructor function and our pingPong method have now become part of a module. A module is a group of JavaScript functions and data that comprises some sort of functionality.

Think of exports as a giant, global JavaScript object. We are creating a new property on it called calculatorModule, and we are setting this property equal to our Calculator constructor function. And because we used a prototype to attach our pingPong() method, it gets dragged along as part of Calculator and stored in the new property of exports called calculatorModule.

Now we just need to bring this calculatorModule into our receiving file, js/pingpong-interface.js.

js/pingpong-interface.js
var Calculator = require('./../js/pingpong.js').calculatorModule;

$(document).ready(function() {
  $('#ping-pong-form').submit(function(event) {
    event.preventDefault();
    var goal = $('#goal').val();
    var simpleCalculator = new Calculator("hot pink");
    var output = simpleCalculator.pingPong(goal);
    output.forEach(function(element) {
      $('#solution').append("<li>" + element + "</li>");
    });
  });
});

Structure and Variable Names

This will not work in the browser yet. We still have to learn about how to use our build tools to package everything up. But first, let's make sure we understand the general structure of our two files, and the two key lines that we have added to allow them to communicate with one another.

In the snippet below, code has temporarily been removed in order to highlight key pieces of this file. You are not required to do this in your own code. This is only for demonstration purposes.

First, let's look at the back-end file:

js/pingpong.js
function Calculator(constructorParameter) {
  // constructor
}

Calculator.prototype.pingPong = function(methodParameter) {
  // method code
}

exports.calculatorModule = Calculator;

And in our front-end file:

js/pingpong-interface.js
var Calculator = require('./../js/pingpong.js').calculatorModule;

$(document).ready(function() {
  var simpleCalculator = new Calculator("a string");
  var output = simpleCalculator.pingPong(1000);
});

Let's trace through the flow. We start with the back-end file that we are pulling into our front-end file. The program travels in this direction because we couldn't use our Calculator without declaring it first. We have created a constructor function called Calculator. We have attached a method to it called pingPong in the usual way. Then we attach the whole Calculator declaration to a property on the exports global object called calculatorModule.

Then, in the front-end file, we pull in everything attached to exports by using require('./../js/pingpong.js'). Next, we specify which property of exports we are interested in by saying require('./../js/pingpong.js').calculatorModule. Since we stored Calculator in the calculatorModule property, it is pulled out using dot notation and stored in a new variable that we also call Calculator. This line is what allows us to instantiate the new Calculator object below and store it in a variable called simpleCalculator. Finally, we can call our pingPong method on our simpleCalculator object, and store the result inside of another variable called output. Then we can do whatever we want in the browser using good old fashioned jQuery.

Matching naming for modules, exports and variables

Now, a couple of these variable names need to match in the correct places, but we could name them whatever we want. Example:

js/pingpong-interface.js
var Whatever = require('./../js/sillyBackEndFile.js').thingy;

What would we have to change to make this work? First, we would need to change the name of our pingpong.js file to sillyBackEndFile.js. Then, we would have to change one thing in our front-end pingpong-interface.js file:

js/pingpong-interface.js
var Whatever = require('./../js/sillyBackEndFile.js').thingy;

$(document).ready(function() {
  var simpleCalculator = new Whatever("a string");
  var output = simpleCalculator.pingPong(1000);
});

Now, we need to change how we instantiate our Calculator object: new Whatever(); below. We also need to make a change in the back-end file:

js/sillyBackEndFile.js
function Calculator(constructorParameter) {
  // constructor
}

Calculator.prototype.pingPong = function(methodParameter) {
  // method code
}

exports.thingy = Calculator;

We are now storing the entire Calculator declaration inside of exports.thingy because when we use it in the front-end file we are saying var Whatever = require('./../js/sillyBackEndFile.js').thingy;

Note that we are still capitalizing the variable Whatever. This is because object constructor functions are named using UpperCamelCase instead of lowerCamelCase, which is used for all other functions, methods and variables in JavaScript. This is also the same for Java and many other languages.

If, by the end of this lesson, modules and exports are still feeling unclear, we strongly recommend you check out video six and seven of The Net Ninja's youtube series on Node.js, they are under 10 minutes each and easy to understand.

Also, don't worry too much yet about why we are using the ./../ at the beginning of our file path above. To summarize, this is because soon we will have a new folder in our structure, which will hold a version of our interface file - these conventions are the same as navigating up, down and across directory structures in the terminal.

Now, I know you're itching to see this work in the browser again! But fear not, we will get there soon! Read on to see how to create an asset pipeline with the new build tools I keep alluding to.