Fall 2020

This is the course homepage for EN.601.280 Full-Stack JavaScript.


These lecture notes are dedicated to the memory of my student Bradlee LaMontagne. His journey as a Full-Stack developer was cut way too short.

Quick Links

  • Campuswire: Q/A, Chat, Live Sessions (Lectures & Office Hours)

  • Codegrade: Homework submission, Exams, Feedback & Grades

  • Panopto: Recorded lectures for asynchronous viewing

Resources

  • Syllabus: Course Syllabus & Policies

  • Schedule: Tentative Schedule of Topics and Events

  • Logistics: Tools we use to survive remote teaching!

Open the sidebar to access course notes. The Appendix contains a listing of lectures (with links to recorded videos and mapping to note sections).

Preface

Welcome to EN.601.280 Full-Stack JavaScript!

A full-stack JavaScript developer is a person who can build modern software applications using primarily the JavaScript programming language. Creating a modern software application involves integrating many technologies - from creating the user interface to saving information in a database and everything else in between and beyond.

A full-stack developer is not an expert in everything. Rather, s/he is someone who is familiar with various (software application) frameworks and the ability to take a concept and turn it into a finished product.

This course is designed to provide a solid introduction to the JavaScript language. In particular, we will explore the features of JavaScript (such as asynchronous programming) that are not familiar to many students who are introduced to programming with languages such as Java, C++ or python.

After exploring the core programming concepts, the class lectures and homework assignments will introduce how JavaScript is used as a popular technology for software development. In particular, we will explore Web development with the MERN stack (MongoDB with Mongoose.js, Express.js, React, Node.js).

In the process, you will also learn about the engineering of modern software applications, emphasizing: object-oriented design, decomposition, encapsulation, abstraction, testing, and good programming style.

Instructional Approach

When it comes to learning a new programming languages for those who already know programming, there are two general type of audience: those who prefer to learn by doing, and those who prefer learning concepts from the ground up. This course is designed to serve both groups.

We alternate between step-by-step tutorial lessons where we learn by doing (building applications), and concept-focused lessons where we dissect a part of the language and learn all there is to learn about it.

For instance, in this lecture we will build a very simple web app (learn by doing). In the next lecture, we will learn about JavaScript basics (how to declare variables, what are the data types, ...).

JavaScript History

  • Created in 1995 by Brendan Eich as LiveScript to enhance web pages in Netscape 2.0
  • Renamed to JavaScript as a marketing ploy to capitalize on Java's popularity (despite the two had very little in common).
  • Standardized as ECMAScript since 1997.
  • ECMAScript 6 (2015) introduced a great many useful addition (Object-Oriented support, Modules, Strict mode, ...)
  • Old JavaScript: Some good ideas, lots of cruft.
  • Modern JavaScript (ECMAScript 6 and beyond): Good ideas alive and kicking, cruft (is gone/can be avoided).
  • Today, it is one of the most popular programming languages. (E.g, see StackOverflow 2020 Developer Survey, PYPL Index, IEEE Spectrum Top Programming Languages 2020)

In 2001, Paul Graham wrote1:

I would not even use JavaScript, if I were you... Most of the JavaScript I see on the Web isn't necessary, and much of it breaks.

In 2007, Jeff Atwood coined Atwood's law2:

Any application that can be written in JavaScript will eventually be written in JavaScript.

This video summaries it:

1

A revised version of the essay can be found at http://paulgraham.com/road.html. The original quote can be found in the book Hackers & Painters.

Why Learn JavaScript?

JavaScript was originally developed to add functionality to web pages but it's now used for much more!

  • JavaScript is great for software prototyping and agile development.
  • JavaScript runs on pretty much any platform from web pages, to server backends and even hardware.
  • There are some great build and deployment tools and frameworks written in JavaScript that are useful in many applications.
  • It's a great introduction to software construction and multi-paradigm programming concepts.
  • JavaScript is easy to learn. But beware - it's hard to master!

SleepTime App

We will build a simple web application similar to sleepyti.me but limited to suggesting "wake up" times based on calculating sleep cycles.

  • A sleep cycle lasts about 90 minutes and a good night's sleep consists of 5-6 sleep cycles.
  • If you wake up in the middle of a sleep cycle, you will feel groggy even if you've completed several cycles prior to waking up.

We do this with the full knowledge that you (most likely) have never done anything like this before. There is going to be many new encounters: syntax, terminology, etc. Don't worry! You are not expected to learn it all in one tutorial. We will revisit every unfamiliar topic again and again throughout the course. In the end, you will look back and find this example an astonishingly easy one! So, buckle up for the ride, and when you face something you don't know about, take it in your stride.

Step 1

  • The browser understands Hyper Text Markup Language, or HTML.

  • Create an empty file and call it index.html

  • Open the file in your favorite text editor!

The extension of your file must be html. The name, index, is just a convention for naming the landing page of websites.

  • HTML document is essentially a text file organized into sections. Each section is contained within <tagname>...</tagname> and is called an HTML element. Here is the minimal structure:
<html>
  <head>
    <!-- meta data about the page -->
  </head>
  <body>
    <!-- content of the page -->
  </body>
</html>
  • Note that HTML elements can be nested.

HTML is fairly easy and you will pick it up as we progress through the course. Please see the Appendix for more information.

  • Copy the following to your index.html
<html>
  <head>
    <title>SleepTime App</title>
  </head>
  <body>
    <p>Hello world!</p>
  </body>
</html>
  • Open index.html in your favorite browser.

Step 2

  • Add the following to the <body> element.
<p>If you go to bed NOW, you should wake up at...</p> 
<button>zzz</button>
  • Add the response which we want to see after clicking on zzz button, after <button>zzz</button>
<p>It takes the average human fourteen minutes to fall asleep.</p>
<p>If you head to bed right now, you should try to wake up at one of the following times:</p>
<p>11:44 PM or 1:14 AM or 2:44 AM or 4:14 AM or 5:44 AM or 7:14 AM</p>
<p>A good night's sleep consists of 5-6 complete sleep cycles.</p>

The wake up times are calculated for someone going to bed at 10:00 PM, and hard-coded here. We will make our app calculate these times dynamically when the zzz button is clicked.

Step 3

The onclick event

  • When we click on the zzz button we want something to happen!

  • Update the button tag and add the following onclick attribute:

<button onclick="window.alert('buzz!');">zzz</button>

HTML attributes provide additional information about HTML elements. They are always specified in the start tag, usually in name/value pairs like: name="value".

  • Save the index.html file; refresh the index page in the browser. Then, click on the zzz button. You must see a pop-up alert window saying buzz!

The window.alert('buzz!'); is a JavaScript statement. The window object represents an open window in a browser.

  • Let's add another statement to onclick event of the zzz button.
<button onclick="window.alert('buzz!');console.log('fizz!');">
  • Save the index.html file; refresh the index page in the browser.

  • In your browser, open "Developer Tools" (typically, you can do this by a right click and selecting "Inspect") and find the "Console" tab.

  • Now, click on the zzz button. In addition to a pop-up alert window with the message buzz!, you must see the message fizz! printed to the console.

console.log() provides access to the browser's debugging console.

The <script> tag

  • It will be difficult to read more than a few (JavaScript) statements for the onclick attribute of the zzz button. It is not advisable to write event handlers (JavaScript statements that are executed in response to an event) using attributes of button (or other HTML) elements. The HTML document will soon become unwieldy.

  • A better approach is putting all JavaScript statements (code) in a dedicated section, perhaps group the statements (pertaining to handling an event) in a function and call that function in the onclick event.

  • Add the following section to the end of the <body> element (right before the closing </body> tag):

<script>
  function handleOnClickEvent() {
    window.alert('buzz!');
    console.log('fizz!');
  }
</script>

The <script> element is used to include JavaScript in HTML.

  • Update the onclick attribute of the zzz button:
<button onclick="handleOnClickEvent();">zzz</button>
  • Save the index.html file; refresh the index page in the browser. Then, click on the zzz button; it must work as before (producing alert window and console message).

Step 4

  • Let's change the presentation of content by aligning to center! Add the following style attribute to the <body> tag.
<body style="text-align: center;">
  • We can further change the text and background colors.
<body style="text-align: center; 
    color: #7FDBFF; 
    background-color:#001f3f;"
>
  • Save the index.html file; refresh the index page in the browser. Notice the changes to the presentation of content.

#7FDBFF and #001f3f are hexadecimal values representing colors. I selected these by inspecting the elements on sleepyti.me website. This sort of inspection can be done using your browser's developer tools. You can also use various HTML color picker tools for such purposes.

The <style> element

  • Suppose we write elaborate styling for a commonly used HTML element like the <p> element (which represents a paragraph). We many have several dozens <p> elements. We would have to rewrite the same elaborate (lengthly) style attribute for each <p> element. This way of writing styles can easily become unwieldy.

  • A better way is to collect all the inline styles into what is called an internal Cascading Style Sheets or CSS using a <style> element.

  • Add the following to the end of <head> element (right before the closing tag </head>):

<style>
    body {
        text-align: center; 
        color: #7FDBFF; 
        background-color: #001f3f;
    }
</style>

More styling!

  • Let's put a border around the "output" (the stuff we shall display once the zzz button is clicked).

  • Wrap the content after zzz button in a <div></div> tag:

<div>
    <p>It takes the average human fourteen minutes to fall asleep.</p>
    <p>If you head to bed right now, you should try to wake up at one of the following times:</p>
    <p>11:44 PM or 1:14 AM or 2:44 AM or 4:14 AM or 5:44 AM or 7:14 AM</p>
    <p>A good night's sleep consists of 5-6 complete sleep cycles.</p>
</div>

HTML <div> tag defines a division or a section in an HTML document. You can think of it as a container for other HTML elements - which is then styled with CSS or manipulated with JavaScript.

  • Now add the following to the end of <style> section (right before the closing tag </style>):
div {
    margin: 1em 5em 1em 5em;
    border: 3px solid white;
}
  • The border property is descriptive; you may want to look up the CSS margin property and CSS units.

  • Save the index.html file; refresh the index page in the browser.

The <div> is a common element. If we were to expand this web app in the future, we would likely have many more div elements. The above styling will be applied to all div elements. It would be forward thinking to ensure the styling is applied only to the div that contains the "output".

<div class="output">
  • Update the div CSS selector to .output
.output {
    margin: 1em 5em 1em 5em;
    border: 3px solid white;
}

Classes allow CSS (and Javascript) to select and access specific elements. Note the style selector for a class starts with a dot as in .output.

  • Save the index.html file; refresh the index page in the browser.

Step 5

  • Preferably we want the "output" to be hidden until we click on the zzz button.

  • Hiding the output is simple; add the following display property to the style selector for output.

.output {
    margin: 1em 5em 1em 5em;
    border: 3px solid white;
    display: none;
}
  • Save the index.html file; refresh the index page in the browser.

  • Update the handleOnClickEvent function as follows:

<script>
  function handleOnClickEvent() {
    let output = document.querySelector('.output');
    output.style.display = 'block';
  }
</script>
  • The document object is the root node of the HTML document. When an HTML document is loaded into a browser, it becomes a document object. The document object is also called the DOM (Document Object Model).

  • The querySelector() method returns the first element of DOM that matches a specified CSS selector in the document.

  • Save the index.html file; refresh the index page in the browser. Notice the output is hidden until you click on the zzz button.

Step 6

  • We must calculate and display the wake up hours when user clicks on the zzz button. Let's focus on the second part: displaying the hours.

  • At the moment, there is a <p> element that holds the hours:

<p>11:44 PM or 1:14 AM or 2:44 AM or 4:14 AM or 5:44 AM or 7:14 AM</p>
  • Here is a sample code to access and update the content of this <p> element:
let hours = // get the <p> element 
hours.innerText = // string containing hours
  • Similar to how we accessed the output <div>, we can access this <p> element by giving it a class attribute such as <p class="hours">
let hours = document.querySelector('.hours'); 
hours.innerText = "placeholder for hours!";
  • We can, alternatively, give it an ID attribute <p id="hours">:
let hours = document.getElementById('hours');
hours.innerText = "placeholder for hours!";
  • The ID attribute specifies a unique id for an HTML element.

The ID must be unique within the HTML document, although your webpage will still work if you use the same ID for multiple elements!

  • The getElementById() method returns the element that has the ID attribute with the specified value.
  • Let's use ID attribute on the <p> element; update the handleOnClickEvent function as follows:
<script>
  function handleOnClickEvent() {
    let output = document.querySelector('.output');
    output.style.display = 'block';

    let hours = document.getElementById('hours');
    hours.innerText = "placeholder for hours!";
  }
</script>

Instead of getElementById('hours') we could use querySelector('#hours'). The difference between the two is not important to us now, but if you are interested, see this stack-overflow query

  • Save the index.html file; refresh the index page in the browser. Notice when you click on zzz button, the output is displayed but the hours are now replaced with placeholder for hours!

Step 7

We are content with the content and styling (pun intended!). Let's focus on the algorithm:

  • When the zzz button is clicked we want to record the current time
  • Allow 14 minutes to fall sleep
  • Create 6 cycles of 90 minutes each
  • Display the cycles as suggested wake up times.

The date object

  • Let's see how we can get the current time (date and time); click on the play button below and "run" the code snippet
let time = Date.now();
console.log(time);
  • Note the output is an integer! Let's find out what this value is; Google: MDN JavaScript Date. Follow the first suggested result and read the docs!

JavaScript Date objects represent a single moment in time in a platform-independent format. Date objects contain a number that represents milliseconds since 1/1/1970 UTC.

Further notice the following methods:

Date.now()

Returns the numeric value corresponding to the current time; the number of milliseconds elapsed since 1/1/1970 00:00:00 UTC, with leap seconds ignored.

Date.prototype.toLocaleTimeString()

Returns a string with a locality-sensitive representation of the time portion of this date, based on system settings.

The keyword prototype indicates toLocaleTimeString() is an instance method (unlike now() which is a static method).

  • Let's try the code below
let today = new Date();
console.log(today.toLocaleTimeString());
  • So we can get the current time and print it out! Next we will write JavaScript code to implement our algorithm.

Step 8

Take a moment and read through the code below; feel free to create a play-ground (click on play button) and experiment with it. I expect that you understand the code (since you've done programming in Java/C++).

let now = Date.now();
let minute = 60 * 1000; // milliseconds

let hours = "";
now += 14 * minute; // fall sleep
// compute sleep cycles
for (let c = 1; c <= 6; c++) {
    now += 90 * minute;
    let cycle = new Date(now);
    hours += cycle.toLocaleTimeString();
    if (c < 6) {
        hours += " or ";
    }
}

console.log(hours);

We need to plug this code into the <script> element:

<script>
function handleOnClickEvent() {
    let output = document.querySelector('.output');
    output.style.display = 'block';

    let now = Date.now();
    let minute = 60 * 1000; // miliseconds

    let hours = "";
    now += 14 * minute; // fall sleep
    // compute sleep cycles
    for (let c = 1; c <= 6; c++) {
        now += 90 * minute;
        let cycle = new Date(now);
        hours += cycle.toLocaleTimeString();
        if (c < 6) {
            hours += " or ";
        }
    }

    let hoursElm = document.getElementById('hours');
    hoursElm.innerText = hours;
}
</script>
  • Save the index.html file; refresh the index page in the browser. You must now have a working application.

Step 9

There are best practices and coding convention when it comes to authoring HTML documents. A good starting point is given on W3School's HTML Style Guide. You can search online for more resources. (You will find many).

We've already followed many of the points made in W3School's HTML Style Guide. We will polish our work by incorporating a few which we've left out.

HTML <!DOCTYPE> Declaration

  • Add to the very top of index.html
<!DOCTYPE html>

The <!DOCTYPE> declaration is not an HTML tag. It is an "information" to the browser about what document type to expect.

Add the lang Attribute and Character Encoding

  • Add to lang attribute to the <html> tag:
<html lang="en-us">
  • Use the <meta> tag to declare the character encoding used in your HTML document. (Place it right after the <head> opening tag.)
<meta charset="UTF-8">

To ensure proper interpretation and correct search engine indexing, both the language and the character encoding should be defined as early as possible in an HTML document.

Setting The Viewport

<meta name="viewport" content="width=device-width, initial-scale=1.0">

The viewport gives the browser instructions on how to control the page's dimensions and scaling. It is needed for responsive web design which aims to make your web page look good on all devices.

Separate the "style" from "content"

It is considered a good practice to separate style from content. This means using <style> element instead of inline style attributes. It is considered a better practice to take this a level further and move all the styling into a separate file.

  • Create a style.css file (in the same folder where index.html is).

  • Move everything inside the <style></style> tag to style.css.

  • Delete the style element from index.html

  • Add the following where <style> element used to be:

<link rel="stylesheet" href="style.css">

Separate JavaScript from HTML

We can make a similar argument to that made earlier about separating style from the content, for separating "scripts" from the content.

  • Create a script.js file (in the same folder where index.html is).

  • Move everything inside the <script></script> tag to script.js.

  • Update the <script> element

<script src="script.js"></script>

Exercise 1

Visit sleepyti.me and click on the zzz button; notice the wake up hours are color coded. Create the same effect in our app (use the same colors).

Solution

We can wrap each wake up hour in a <span></span> tag with a unique ID for each cycle:

<p id="hours">
    <span id="cycle-1">11:44 PM</span> or 
    <span id="cycle-2">1:14 AM</span> or 
    <span id="cycle-3">2:44 AM</span> or 
    <span id="cycle-4">4:14 AM</span> or 
    <span id="cycle-5">5:44 AM</span> or 
    <span id="cycle-6">7:14 AM</span>
</p>

The <span> tag is much like the <div> element, but <div> is a block-level element and <span> is an inline element.

We can then update our style.css file to assign the desired colors to each "cycle":

#cycle-1 {
  color: rgb(168, 39, 254);
}

#cycle-2 {
  color: rgb(154, 3, 254);
}

#cycle-3 {
  color: rgb(150, 105, 254);
}

#cycle-4 {
  color: rgb(140, 140, 255);
}

#cycle-5 {
  color: rgb(187, 187, 255);
}

#cycle-6 {
  color: rgb(143, 254, 221);
}

I picked up the values by inspecting the output on sleepyti.me.

To generate this output programmatically, we need to update our JavaScript code:

function handleOnClickEvent() {
  let output = document.querySelector('.output');
  output.style.display = 'block';

  let now = Date.now();
  let minute = 60 * 1000; // miliseconds

  let hours = document.getElementById('hours');
  hours.innerText = ""; // cleanup exisitng content
  now += 14 * minute; // fall sleep
  // compute sleep cycles
  for (let c = 1; c <= 6; c++) {
      now += 90 * minute;
      let cycle = new Date(now);
      let span = document.createElement("span");
      span.id = "cycle-" + c;
      span.innerText = cycle.toLocaleTimeString();
      hours.appendChild(span);
      if (c < 6) {
        let or = document.createTextNode(" or ");
        hours.appendChild(or);
      }
  }
}

Exercise 2

Visit sleepyti.me and notice the calculate button which comes after "I plan to FALL ASLEEP at..." clause. Give it a try and understand the functionality. Then, try to reproduce it.

Solution

We need to make some changes to the content:

<p>I plan to fall sleep at...</p>

<select id="hh">
    <option>(hour)</option>
    <option>1</option>
    <option>2</option>
    <option>3</option>
    <option>4</option>
    <option>5</option>
    <option>6</option>
    <option>7</option>
    <option>8</option>
    <option>9</option>
    <option>10</option>
    <option>11</option>
    <option>12</option>
</select>

<select id="mm">
    <option>(minute)</option>
    <option>00</option>
    <option>05</option>
    <option>10</option>
    <option>15</option>
    <option>20</option>
    <option>25</option>
    <option>30</option>
    <option>35</option>
    <option>40</option>
    <option>45</option>
    <option>50</option>
    <option>55</option>
</select>

<select id="ampm">
    <option>PM</option>
    <option>AM</option>
</select>

<br>
<br>

<button onclick="handleOnClickEvent();">CALCULATE</button>

We further need to adjust the output content:

<div class="output">
    <p>If you fall sleep at the specified time above, you should try to wake up at one of the following times:</p>
    <p id="hours">11:44 PM or 1:14 AM or 2:44 AM or 4:14 AM or 5:44 AM or 7:14 AM</p>
    <p>A good night's sleep consists of 5-6 complete sleep cycles.</p>
</div>

Now, we must update our JavaScript code to base its sleep cycle calculation on the time specified by the user (using the drop-down lists). So, take out the following statement:

let now = Date.now();

and replace it with the following:

let hh = document.getElementById("hh").value;
let mm = document.getElementById("mm").value;
let ampm = document.getElementById("ampm").value;
hh = ampm === "PM" ? hh + 12 : hh;
let now = new Date();
now.setHours(hh);
now.setMinutes(mm);
now = now.valueOf(); 
  • The value property sets or returns the value of the selected option in a drop-down list.

  • The valueOf() method returns the primitive value of a Date object.

Finally, we must adjust our JavaScript code to not take into account the "14 minutes" to fall sleep since the assumption is that the user falls sleep at the specified time. So, comment out the following line:

now += 14 * minute;

There is a bug 🐛 in the solution provided here. We will revisit this in future lectures as an opportunity to work with the built-in debugger in Chrome/FireFox Developer Tools.

JavaScript Basics

In this chapter, we will explore JavaScript values and variables!

There are a few different ways to run JavaScript code. The easiest to get started is to use the browser console; open the development tools and the console tab.

Console

When creating a web application, you can use console.* to output information to the browser console.

Console is a very handy tool for quickly debugging problems.

Interestingly, there is no specification for how console.* methods behave in ECMAScript. Each browser (environment) adds its own methods and behavior. However, almost all environments support the following:

  • Logging information such as printing out a message or the current value of a variable

    console.log("This is my message");
    
  • Sending a warning to the console.

    console.warn("Resource not changed")
    
  • Sending an error message to the console.

    console.error("File not found!")
    

You should not include any console.* methods in production code.

Variables

In JavaScript, variables must be declared to be used. Here is an example:

let firstName; // declare
firstName = "Ali"; // initialize
console.log(firstName); // use

let lastName = "Madooei"; // declare and initialize
console.log(lastName); // use

Unlike in languages like Java and C++ where you declare a variable by indicating its type, in JavaScript you must use a variable declaration keyword:

  • let
  • const
  • var

JavaScript is a dynamically-typed language which means a variable is assigned a type at runtime based on the variable's value at the time.

Dynamic typing means you can also change a variable's type at any time:

let num;
num = 2; // num is a Number
num = "two"; // now num is a String
num = { value: 2 }; // now num is an Object!

If you declare a variable but not give it a value, it's type and value are undefined

let num;
console.log(num);
console.log(typeof num);

You can use the typeof operator to check the type of a value.

let

A variable declared with let behaves for most part the way you expect; for example:

  • It throws an error if you use it before it is declared:
    console.log(num);
    let num = 2;
    
  • It throws an error if you redefine it:
    let num = 2;
    let num = "two";
    console.log(num);
    
  • It has a block scope:
    for (let num = 1; num < 10; num++) {
      // do something with num
    }
    console.log(num);
    

const

Declares a read-only named constant.

let firstName = "Ali";
firstName = "John"; // Ok

const lastName = "Madooei";
lastName = "Doe"; // Error!

Constants are block-scoped, like variables defined using the let keyword. The value of a constant can't be changed through reassignment, and it can't be redeclared.

An initializer for a constant is required.

const lastName; // Error!
lastName = "Madooei";

The const declaration creates a read-only reference to a value. It does not mean the value it holds is immutable (just that the variable identifier cannot be reassigned).

const numbers = [1, 2, 7, 9];
numbers[2] = 4; // Ok

Prefer const over let: it provides an extra layer of protection against accidentally reassigning your variables.

var

The variable declaration keywords let and const are relatively new additions to JavaScript. The old way of declaring a variable in JavaScript is using var:

var firstName = "Ali";

Variables declared with var are not block scoped (although they are function scoped). Also, no error is thrown if you declare the same variable twice using var.

Don't use var!

Undeclared Variables!

Technically, you can declare a variable in JavaScript without using a variable declaration keyword. Variables defined as such become global variables. This is another cruft from the early days of JavaScript and it must be avoided.

firstName = "Ali";
console.log(window.firstName);

Using an undeclared variable throws ReferenceError under JavaScript's "strict mode", introduced in ECMAScript 5.

Resources

Syntax

Semicolon

Statements in JavaScript must end in a ;. However, JavaScript has Automatic Semicolon Insertion (ASI) which means that, if you omit a semicolon, JavaScript will automatically add it where it thinks it should go.

In this class, we always remember to insert semicolons.

Why?

Because omitting a semicolon in certain situations can cause problems.

Here is a completely contrived example:

x
++
y

should it be interpreted as:

x;
++y;

or:

x++;
y;

Depending on the implementation of ASI, it might be interpreted as the first or the second one.

Thus, it's recommended to insert the ; where it is intended. Most JavaScript linters such as Prettier insert semicolons to your code.

ASI bites you even if you conscientiously put all semicolons! For instance if you accidentally hit enter after return:

return  // Semicolon inserted here
  someComplicatedExpression;

Identifier naming rules

Identifiers (names of any function, property or variable) may contains letters (only ASCII letters), numbers, dollar signs or underscores.

The first character must not be a number.

Why?

Read this discussion on StackOverflow.

It is also recommended to not use $ or _ as the first character of your variables.

Why?

For clarity mainly! Some popular JavaScript libraries (such as jQuery, UnderscoreJS, Lodash) use these characters as their identifiers.

Keep in mind JavaScript is case sensitive.

In this course we use camelCase naming conventions (so name your function calcTax not calc_tax). Refer to Google JavaScript Style Guide for more information.

Reserved words cannot be used as identifiers.

Like most programming languages, there are a number of reserved words in JavaScript that you cannot use to name your functions and variables (e.g. var, let, new). For a complete list, refer to MDN web docs; JavaScript reference: Keywords.

Comments

Comments in JavaScript are similar to Java and C++:

// I am a single line comment!

/*
I am a block comment,
  and I can be expanded over several lines!
*/

let /* hourly */ payRate = 12.5; // dollars

Types

There are two data types in JavaScript: primitives and objects. The following are the primitive types:

  • boolean
  • string
  • number
  • undefined
  • null
  • symbol
Primitive vs. Object

Primitive values are atomic data that are passed by value and compared by value. Objects, on the other hand, are compound pieces of data that are passed by reference and compared by reference.

typeof operator

You can check the type of a value by using the typeof operator

console.log(typeof "two");
console.log(typeof 2);
console.log(typeof true);
console.log(typeof undeclaredVariable);
console.log(typeof { value: 2 });

See typeof on MDN web docs for more details.

boolean

The Boolean type is similar to most other programming languages with two options: true or false.

let isRequired = true;
let isOptional = false;

See Boolean on MDN web docs for more details.

string

To be discussed shortly.

number

To be discussed shortly.

Primitive types and their Wrappers

The three primitive types string, number and boolean have corresponding types whose instances are objects: String, Number, Boolean.

const name = "Ali";
const firstname = new String("Ali");

console.log(name);
console.log(firstname);

console.log(typeof name);
console.log(typeof firstname);

console.log(name instanceof String);
console.log(firstname instanceof String);

You can use the instanceof operator to check the type of an object.

undefined and null

JavaScript has two "bottom" values!

  • An uninitialized variable is undefined.

  • The null value denotes an "intentionally" absent value.

Although you can, but don't deliberately set a value to undefined

JavaScript has lots of quirks and a bunch of them are around null and undefined (and how they behave, relate and differ). We will see some of these in later sections. For now, enjoy this!

console.log(typeof undefined);
console.log(typeof null);

For more details, Brandon Morelli has a nice article: JavaScript — Null vs. Undefined. For a complete reference, visit null and undefined on MDN web docs.

Symbol

Symbol is a new addition to the JavaScript language which enables Metaprogramming. It is beyond the scope of this course. You can consult the following resources if you are interested to learn more.

String

Strings in JavaScript are sequences of Unicode (UTF-16) characters.

const greeting = "Hello 🌐";
console.log( greeting);

There isn't a separate type for representing characters in JavaScript. If you want to represent a single character, just use a string consisting of a sing

Delimiters are single or double quotes:

const greeting = "Hello 🌐";
const congrats = 'Congratulation 🎉';

You can also use template literals, using the backtick character `.

const greeting = `Hello 🌐`;

Backtick delimiters are useful for multiline strings and embedded expressions:

const name = "Ali";
const greeting = `**********
Hello ${name}!
**********`;
console.log(greeting);

You can call methods and properties available in String wrapper object directly on a primitive string value.

const name = "Ali";
console.log(name.length);
console.log(name.charAt(1));
console.log(name.toUpperCase());

The statements above work because JavaScript secretly converts the primitive string to its wrapper object type String.

Since ECMAScript 2015, you can access string characters similar to accessing array elements (using square bracket notation):

const animal = "cat";
console.log(animal[0], animal[1], animal[2]);

But you cannot use the square bracket notation to modify the string:

const animal = "cat";
animal[0] = "b";
console.log(animal);

See String on MDN web docs for more details.

Number

JavaScript has only one type of number and that is a 64-bit floating point number.
  • Unlike languages like Java and C++, there is no distinction between whole numbers (integers) and real values (numbers with decimals).
  • JavaScript always stores numbers as double precision floating point, following the international IEEE 754 standard.
let num;
num = 3.14159265;
console.log(typeof num);
num = 3;
console.log(typeof num);

So, an apparent integer is in fact implicitly a float!

const num = 1 / 2;
console.log(num);

There is, like in most programming languages, unavoidable round-off errors:

console.log(0.1 + 0.2);

Number literals

  • Decimal numbers: 42, 0.42
  • Exponential notation: 4.2e-10
  • Hexadecimal: 0x2A
  • Binary: 0b101010
  • Octal: 0o52
// Try any of the above
let num = 42; 
console.log(num);

Special Values

JavaScript has the special values Infinity and -Infinity:

console.log(1 / 0);
console.log(-1 / 0);
console.log(10 * Number.MAX_VALUE);

There is also NaN which means "Not a Number". You get this when e.g. number cannot be parsed from a string, or when a Math operation result is not a real number.

console.log(parseInt("blahblah"));
console.log(Math.sqrt(-1));

You also get NaN when you do weird stuff!

console.log(0 * Infinity);
console.log("JavaScript" / 2);

For a reference, visit MDN web docs on Infinity and NaN.

Number Wrapper object

You can borrow many useful methods defined in Number object. For instance, there is a toString method with optional base argument (between 2 and 36):

const num = 42;
console.log(num.toString(2));

The toPrecision and toExponential can be used for formatting:

const num = 3.14159265;
console.log(num.toPrecision(2));
const val = 1000000; 
console.log(val.toExponential()); 

The Number object has several useful static method and constants as well. For example, there is parseInt method with optional base argument (between 2 and 36):

const num = "0x2A";
console.log(Number.parseInt(num, 16));

There is parseFloat method to convert floating point to number:

const num = "0.001";
console.log(Number.parseFloat(num) * 2);

There are several methods for type checking:

console.log(Number.isFinite(-1 / 0));
console.log(Number.isNaN(1 / 0));
console.log(Number.isInteger(2.1));

And many useful constants:

console.log(Number.MAX_VALUE);
console.log(Number.MIN_VALUE);
console.log(Number.MAX_SAFE_INTEGER);
console.log(Number.MIN_SAFE_INTEGER);
console.log(Number.POSITIVE_INFINITY);
console.log(Number.NEGATIVE_INFINITY);

For a reference, visit Number on MDN web docs.

Big Integer!

JavaScript number type cannot represent integers bigger than 253-1 (Number.MAX_SAFE_INTEGER constant). For integers larger than that limit, you can use a special built-in object, BigInt.

BigInt can be used to represent arbitrary large integers.

BigInt literal have suffix n:

const num = 18889465931478580854784n;
console.log(typeof num);
console.log(num * num);

You can also create BigInt by calling BigInt() function:

const num = Number.MAX_SAFE_INTEGER;
console.log(BigInt(num * num));

You cannot mix BigInt with other numbers

const num = 18889465931478580854784n;
console.log(num + 1);

For a reference, visit BigInt on MDN web docs.

Arithmetics

In JavaScript, you have the usual arithmetics operators:

1 + 2;  // add
9 / 3;  // divide
1 * 5;  // multiply
9 - 5;  // subtract
10 % 4; // modulus (division remainder)
  • The / operator yields a floating-point result.
console.log(9 / 3);
console.log(1 / 2);
  • The % operator works with both integer and non-integer numbers.
console.log(42 % 10);
console.log(1.1 % 0.3);
  • There is ** operator (similar to Python) that raises to a power.
console.log(2 ** 10);
console.log(2 ** 0.5);
  • If an operand is NaN, so is the result
console.log(2 + NaN);

Combine operator and assignment

Similar to Java, C++ and many other programming languages, you can write:

let counter = 1;
counter += 10;
console.log(counter);

Increment and decrement operators:

Similar to Java, C++ and many other programming languages, you can write:

let counter = 1;
console.log(counter++);
console.log(counter);
console.log(++counter);
console.log(--counter);

Type Conversions

Type Coercion is another term for type conversion. The former is more commonly used by the JavaScript community.

What do you think is the output?

console.log("3" - 2);
Explanation

Here, the non-number argument is converted to number.

What do you think is the output?

console.log("3" + 2);
Explanation

Here, + concatenates strings: the non-string argument is converted to string.

Don't mix types!

JavaScript does not say no when you mix up types in expressions. No matter how complex the expression is and what types it involves, JavaScript will evaluate it. The outcome will be surprising and unexpected in many cases. So, never mix types!

I recommend reading freeCodeCamp's article: JavaScript type coercion explained

Math

Math is a built-in object that with many useful methods that implement various mathematical functions. Here is a list of more frequently used ones for your reference.

  • Math.abs(x), Returns the absolute value of x.
  • Math.sign(x), Returns the sign of the x, indicating whether x is positive, negative, or zero.
  • Math.min, Returns the smallest of its arguments.
  • Math.max, Returns the largest of its arguments.

Note: min and max take in any number of arguments.

  • Math.random(), Returns a pseudo-random number between 0 and 1.
  • Math.round(x), Returns the value of the number x rounded to the nearest integer.
  • Math.trunc(x), Returns the integer portion of x, removing any fractional digits.
  • Math.floor(x), Returns the largest integer less than or equal to x.
  • Math.ceil(x), Returns the smallest integer greater than or equal to x.
  • Math.fround(x), Returns the nearest single precision float representation of x.

Caution: be careful with negative arguments to functions like ceil and floor

console.log(Math.ceil(7.004));
console.log(Math.ceil(-7.004));
console.log(Math.floor(5.05));
console.log(Math.floor(-5.05));
  • Math.pow(x, y), Returns base x to the exponent power y.
  • Math.exp(x), Returns Ex, where x is the argument, and E is Euler's constant (2.718..., the base of the natural logarithm).
  • Math.log(x), Returns the natural logarithm of x.
  • Math.log2(x), Returns the base-2 logarithm of x.
  • Math.log10(x), Returns the base-10 logarithm of x.
  • Math.sqrt(x), Returns the positive square root of x.

There are many useful constants built into Math object as well:

console.log(Math.E);
console.log(Math.PI);
console.log(Math.SQRT2);
console.log(Math.SQRT1_2);
console.log(Math.LN10);
console.log(Math.LOG2E);
console.log(Math.LOG10E);

The Math objects includes many other static methods and properties. Please consult the MDN Reference page on Standard built-in Objects -> Math

Caution:

  1. Many Math functions have a precision that's implementation-dependent.
  2. Math functions do not work with BigInt.

Objects

We've already seen some special objects built into JavaScrip, such as the Wrapper objects (Number, String, Boolean), Math, and BigInt objects. There are also Date and RegExp, and others. Although these objects look like objects in Java/C++, the true nature of objects in JavaScript is far from objects in Object-Oriented programming! JavaScript objects are more similar to Dictionaries in Python (or HashMap in Java/Hash Tables in C++).

JavaScript objects are, in a nutshell, a collection of key-value pairs.

const user = { name: "John", age: 21 };
console.log(typeof user);
console.log(user);

Each key-value pair is called a "property"

Syntax
  • Key must be string; you can drop the single/double quotes when key is a valid identifier.
  • Value could be anything; any primitives or objects.
  • A key-value is paired by colon.
  • Properties are separated by commas. It is okay to have a trailing comma!
const user = {
    name: {
        first: "John",
        last: "Smith"
    },
    age: 21,
    "Job Title": "Teacher",
};

You can access property value with dot notation or square bracket notation:

const user = { name: "John", age: 21 };
console.log(user.name);
console.log(user["age"]);

You can modify property value the same way:

const user = { name: "John", age: 21 };
user["name"] = "Jane";
user.age = 30;
console.log(user.name, user.age);

You can even add new properties the same way:

const user = { name: "John", age: 21 };
user.lastname = "Smith";
user["member since"] = 2007;
console.log(user);

And, you can remove properties:

const user = { name: "John", age: 21 };
delete user.age;
console.log(user.age);

Accessing a nonexistent property yields undefined

You can create an empty object and then add properties to it:

const user = {};
user.name = "John";
console.log(user);
Object constructor syntax

You can make an empty object using object constructor syntax:

const obj = new Object();

But it is more common to use the object literal syntax instead:

const obj = {};

We can use square brackets in an object literal, when creating an object. That’s called computed properties.

const key = "age";
const value = 21;
const user = { [key]: value };
console.log(user);

A common pattern you will see:

const age = 21;
const user = { name: "John", age };
console.log(user);
Explanation

If variable is used as above, a key-value pair is created where key is the variable name and value is the variable value.

It is same as:

const age = 21;
const user = { name: "John", age: age };

Destructuring Objects

Convenient syntax for fetching multiple elements:

const user = { name: "John", age: 21 };
let { name, age } = user;
console.log(name, age);

You can rename the properties

const user = { name: "John", age: 21 };
let { name: userName, age: userAge } = user;
console.log(userName, userAge);

You can even do this:

const user = { firstname: "John", lastname: "Smith", age: 21 };
let { age, ...rest } = user;
console.log(age, rest);

JSON

JSON (JavaScript object notation) is a standard text-based format for representing structured data based on JavaScript object syntax. It was popularized by Douglas Crockford. It is commonly used for transmitting data in web applications (e.g., sending some data from the server to the client, so it can be displayed on a web page, or vice versa).

JSON closely resembles JavaScript object literal syntax except:

  • Property names (keys) must be enclosed in double quotes.
  • Property values are numbers, strings, true, false, null, arrays, and objects.
  • Trailing commas, skipped elements (inside arrays) or undefined values not allowed.

The special built-in JSON object JavaScript contains methods for parsing JSON and converting values to JSON. For a reference, visit the documentation on MDN website.

Arrays

Arrays in JavaScript are a special type of object. They resemble Python Lists (a collection which is ordered and changeable; allows duplicate members and members of different types).

const numbers = [1, 2, 2, "three", 4];
console.log(numbers);
console.log(typeof numbers);

You can use the bracket notation to access and modify the array elements.

const numbers = [1, 2, 3, 4];
for (let i = 1; i < numbers.length; i++) {
  numbers[i] = numbers[i] + numbers[i - 1];
}
console.log(numbers);

You can create an empty array and then add values to it:

const numbers = [];
numbers[0] = 10;
numbers[1] = 11;
numbers.push(12);
numbers.pop();
numbers.push(13);
console.log(numbers);
Array constructor syntax

You can make an empty array using array constructor syntax:

const numbers = new Array();

But it is more common to use the array literal syntax instead:

const numbers = [];

Madness: In an array, you can leave some elements undefined!

const numbers = [, 2, , 4, ];
for (let i = 0; i < numbers.length; i++) {
  console.log(numbers[i]);
}

Note how trailing comma does not denote an undefined element!

Allowing undefined elements leads to more maddening behaviors.

const numbers = [];
numbers[99] = "hundred";
console.log(numbers.length);
console.log(numbers[0]);
console.log(numbers[99]);
console.log(numbers[100]);

length is calculated as one more than the highest index!

And the craziest thing is that you can overwrite length property!

const numbers = [1, 2, 3, 4];
numbers.length = 0;
console.log(numbers[0]);
console.log(numbers[1]);

Destructuring Arrays

Convenient syntax for fetching multiple elements:

const numbers = [10, 20, 30, 40];
// let first = numbers[0];
// let second = numbers[1];
let [first, second] = numbers;
console.log(first, second);

You can even do this:

const numbers = [10, 20, 30, 40];
let [first, second, ...others] = numbers;
console.log(first, second, others);

Multi-dimensional arrays

Use arrays of arrays for multi-dimensional arrays:

const magicSquare = [
  [16, 3, 2, 13],
  [5, 10, 11, 8],
  [9, 6, 7, 12],
  [4, 15, 14, 1]
];

// Use two bracket pairs to access an element:
console.log(magicSquare[1][2]);

Array object comes with many useful built-in operations. We will explore some of these in future lectures. For the impatient, please visit Array on MDN web docs

Date

Date is another built-in object in JavaScript. We used it in development of SleepTime App. Here is a summary of its operations. For a more detailed reference, visit MDN web docs.

Constructing Dates

  • Date measured in "milliseconds" (adjusted for leap seconds) since the "epoch" (midnight of January 1, 1970 UTC).
let now = new Date(); // yields current date/time.
console.log(now);
  • You can pass milliseconds to the constructor; valid range ±100,000,000 days from epoch.
// new Date(milliseconds) constructs Date from milliseconds.
console.log(new Date(10**12));

Caution: Dates are converted to numbers in arithmetic expressions.

  • You can pass the following parameters to Date() constructor to create a specific date:

    • year,
    • zeroBasedMonth,
    • day,
    • hours,
    • minutes,
    • seconds,
    • milliseconds
  • The parameters must be provided in order, and everything starting from "day" are optional.

const brendanEichsBirthday = new Date(1961, 6 /* July */, 4);
console.log(brendanEichsBirthday);

Caution: Out-of-range zeroBasedMonth, day, hours, etc. silently roll.

const invalidDate = new Date(2019, 13, -2); // 2020-01-28
console.log(invalidDate);
  • For UTC, construct with date string in format YYYY-MM-DDTHH:mm:ss.sssZ (with literal T, Z):
const firstMillennialUTCnoon = new Date('2000-01-01T12:00:00.000Z');
console.log(firstMillennialUTCnoon);

Caution: Other string formats may be accepted by the constructor, but their format is not standardized.

Caution: Date(...) without new constructs string (in non-standard format).

const now = Date();
console.log(now);

Static Date Functions

  • Date.now() is current date/time but it yields milliseconds, not Date object.
const now = Date.now();
console.log(now);
  • A very useful static function is Date.parse(dateString). It also yields milliseconds but you can pass that to Data constructor to get a Date object.
let aliBirthday = Date.parse("May 9, 1985");
console.log(aliBirthday);

aliBirthday = new Date(aliBirthday);
console.log(aliBirthday);

Caution: support for dateString in YYYY-MM-DDTHH:mm:ss.sssZ format is guaranteed. Support for other formats are implementation-dependent.

  • Another useful static function is Date.UTC(year, zeroBasedMonth, day, hours, minutes, seconds, milliseconds) where arguments after year are optional.

Getters and Setters

  • The Date object has traditional getter/setter methods getHours/setHours.
const now = new Date();
console.log(now.getFullYear());
console.log(now.getMonth());  // 0-11
console.log(now.getDate());   // 1-31
console.log(now.getHours());  // 0-23
console.log(now.getMinutes());  
console.log(now.getSeconds());
console.log(now.getMilliseconds());
let aliBirthday = new Date();
aliBirthday.setYear(1985);
aliBirthday.setMonth(4);    // May
aliBirthday.setDate(9);    
console.log(aliBirthday);
  • For UTC, there are UTC variants getUTCFullYear, setUTCFullYear, getUTCMonth, setUTCMonth, and so on.

Date Formatting

  • toISOString yields string in YYYY-MM-DDTHH:mm:ss.sssZ format.
let now = new Date();
console.log(now.toISOString());
  • toString, toDateString, toTimeString yield "humanly readable" string in local time zone, or only the date/time portion:
let now = new Date();
console.log(now.toString());
console.log(now.toDateString());
console.log(now.toTimeString());
  • toUTCString yields "humanly readable" string in UTC:
let now = new Date();
console.log(now.toUTCString());
  • toLocaleString, toLocaleDateString, toLocaleTimeString yield localized string:
let now = new Date();
console.log(now.toLocaleTimeString("en-US"));
console.log(now.toLocaleTimeString("zh-Hans-CN-u-nu-hanidec"));

SleepTime App with Git

In this chapter, we will rebuild our SleepTime App! This time around, we do things a little differently. And more importantly, we will track changes using Git VCS.

VCS

Version Control System, or VCS, allows you to save a snapshot of your project at any time you want. It's like making a copy of your project for backup and safe keeping, except that VCS typically does this in a more efficient fashion. It also comes with facilities to restore to an earlier copy (version).

Git

Git is the world's most popular VCS. It can keep a complete history of the changes made to code, and revert back to old versions when needed. This feature comes handy when you want to make changes to code without losing the original.

Git also facilitates synchronizing code between different people, thus making collaboration in a team very easy. This feature leads to increases productivity in particular in large software project that involves many developers.

Every time you save a new version of your project, Git requires you to provide a short description of what was changed. This helps to understand how the project evolved between versions.

I use Git for almost all my coding project, even when the project is small and I am the only one working on it. I do this because the history of changes helps me understand what happened, when I visit the project later.

Install Git

Follow the instructions provided here to setup Git on your computer.

Checking if Git is installed

Open terminal in Linux or MacOS, or "Git Bash" on Windows. Then run the following command:

git --version

Configuring Git

Tell Git who you are

git config --global user.email "you@example.com" 
git config --global user.name "Your Name"

GitHub

GitHub is a website that stores Git repositories on the internet to facilitate the collaboration that Git allows for. We will be using GitHub in this class. If you don't already have an account, please make one by visiting github.com/

Step 1

Create a folder where you want to store the files for today's lecture. I'm going to call this folder sleeptime-git.

Open the terminal, change directory to sleeptime-git. (Alternatively, open this folder in VSCode and then open the integrated terminal.)

In the terminal, run the following command.

git init

Git is now ready to track all changes within the folder sleeptime-git. In Git jargon, we created a (local) repository inside sleeptime-git folder.

A Git repository is a collection of files tracked by Git.

Create a new file README.md inside sleeptime-git folder. Next, run the following command in the terminal.

git status

You will get a list of untracked files.

The git status command displays the state of the Git repository.

Next, run the following command in the terminal.

git add README.md

Git now has taken a snapshot of the README.md. This is like pressing command + c to make a copy in the memory (but the copy is not completed until you press command + v). Copy is not a great analogy because what is contained in a snapshot is mostly only the changes made.

In Git's jargon, we say changes in README.md are staged to be committed.

You can run git status now to see the state of our repository. The README.md must appear in green color under "changes to be committed".

Next, run the following command in the terminal.

git commit -m "Create README file"

Git now has saved (committed) the snapshot you've created earlier using the add command. This is like pressing command + v (after having pressed command + c to make a copy). Commits are like versions of your repository where you can access at any future point. A commit is part of the history of your repository.

To see a log of your commits, you can run the following command in the terminal.

git log

On my computer, git log produced the following output:

commit f034c1ea747a6ab6726681d60a7bd5b097ff40b8 (HEAD -> master)
Author: Ali Madooei <madooei@jhu.edu>
Date:   Sat Sep 5 10:21:54 2020 -0400

    Create README file

The command git log lists the commits made in reverse chronological order. Each commit has a commit ID (a hash identifier), the author's name and email, the date written, and the commit message.

Step 2

Add this content to README.md:

# SleepTime App

A simple web application
similar to sleepyti.me but
limited to suggesting "wake up" 
times based on calculating sleep cycles.

* A sleep cycle lasts about 90 minutes
and a good nights sleep consists of
5-6 sleep cycles.
* If you wake up in the middle of a
sleep cycle, you will feel groggy 
even if you've completed several 
cycles prior to waking up.

Save the file and then run the following command.

git status

Git should inform you that README.md has been modified, and that changes made to it has not been staged for commit.

Run the following command to stage the changes for commit:

git add README.md

And then, commit the changes:

git commit -m "Add description for this app"

Feel free to check the status (git status) and logs (git log) now.

More changes!

I want to make the following changes to the README.md file:

  • Fix a type: change a good nights sleep to a good night's sleep
  • Link to sleepyti.me web page: change sleepyti.me to [sleepyti.me](https://sleepyti.me/)

I can edit and then commit the changes in one go. I would prefer, however, to commit changes one by one. So, let's fix the type, and then commit the changes.

git commit -am "Fix typo"

Notice I skipped git add and instead used the flag -am with the commit command.

The flag -a says to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected.

Next, let's link to sleepyti.me web page. Then, commit the changes.

git commit -am "Link to sleepyti.me URL"

Looking at the logs (using git log), I have the following history

commit 28bca5f1424cdeba23624db9deae765e4c97d793 (HEAD -> master)
Author: Ali Madooei <madooei@jhu.edu>
Date:   Sat Sep 5 11:36:56 2020 -0400

    Link to sleepyti.me URL

commit cb53f8da1ba1940486c4b5d2997929e6d7040b21
Author: Ali Madooei <madooei@jhu.edu>
Date:   Sat Sep 5 11:34:26 2020 -0400

    Fix typo

commit 79cbc98629aa98b11003c48b5a3a4cf79c78f292
Author: Ali Madooei <madooei@jhu.edu>
Date:   Sat Sep 5 11:15:49 2020 -0400

    Add description for this app

commit f034c1ea747a6ab6726681d60a7bd5b097ff40b8
Author: Ali Madooei <madooei@jhu.edu>
Date:   Sat Sep 5 10:21:54 2020 -0400

    Create README file

Step 3

The following command displays the history (logs) in a more compact format:

git log --pretty=oneline

On my computer, it produces the following output:

28bca5f1424cdeba23624db9deae765e4c97d793 (HEAD -> master) Link to sleepyti.me URL
cb53f8da1ba1940486c4b5d2997929e6d7040b21 Fix typo
79cbc98629aa98b11003c48b5a3a4cf79c78f292 Add description for this app
f034c1ea747a6ab6726681d60a7bd5b097ff40b8 Create README file

Git Diff

You can see the changes made from one to another commit, using the following syntax

git diff <commit> <commit>

where <commit> is a commit ID. (The first commit ID is typically pointing to a commit made earlier than the second commit ID.)

The result will be the changes made between the first and second commits (identified by their IDs in form of a diff file.

For example, here is the diff between the latest commit (Link to sleepyti.me URL) and the one where I added description for this app.

diff --git a/README.md b/README.md
index ccabd2e..7463b25 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,12 @@
 # SleepTime App
 
 A simple web application
-similar to sleepyti.me but
+similar to [sleepyti.me](https://sleepyti.me/) but
 limited to suggesting "wake up" 
 times based on calculating sleep cycles.
 
 * A sleep cycle lasts about 90 minutes
-and a good nights sleep consists of
+and a good night's sleep consists of
 5-6 sleep cycles.
 * If you wake up in the middle of a
 sleep cycle, you will feel groggy 

To better understand the diff format, consult this guideline.

The git diff command is a handy tool to quickly check the changes made since the last commit.

Example: Add the following sentence to the end of README.md

The SleepTime App is made using basic HTML, CSS, and JavaScript.

Save the file, and then run git diff in the terminal. This is the output on my computer:

diff --git a/README.md b/README.md
index 7463b25..a6cab69 100644
--- a/README.md
+++ b/README.md
@@ -11,4 +11,6 @@ and a good night's sleep consists of
 * If you wake up in the middle of a
 sleep cycle, you will feel groggy 
 even if you've completed several 
-cycles prior to waking up.
\ No newline at end of file
+cycles prior to waking up.
+
+The SleepTime App is made using basic HTML, CSS, and JavaScript.
\ No newline at end of file

Step 4

Revert Changes

You can use the following command to reset (revert back changes) to an earlier commit:

git reset --hard <commit>

where <commit> is a commit ID.

For example, I will reset my repository to the commit I've made after adding description of this app (before fixing the typo and linking to the sleepyti.me URL).

git reset --hard 79cbc98629aa98b11003c48b5a3a4cf79c78f292 

Looking at the log (using git log --pretty=oneline), I get the following:

79cbc98629aa98b11003c48b5a3a4cf79c78f292 (HEAD -> master) Add description of the app
f034c1ea747a6ab6726681d60a7bd5b097ff40b8 Create README file

Also, looking at the README.md, the changes I've made (typo correction and URL addition) are gone! It is like I traveled back in time!

There are other commands, namely git revert and git checkout, that can be used to undo some kind of change in your repository. Consult this tutorial to learn more about each.

Commit Messages

Let's correct the typo and link to the sleepyti.me URL again. Let's also add that line about the technology used for building SleepTime App. Then, commit the changes:

git commit -am "Update description"

Look at the logs now:

03fefbc9cb2549b123cf16f9dc49eef3a19552d8 (HEAD -> master) Update description
79cbc98629aa98b11003c48b5a3a4cf79c78f292 Add description of the app
f034c1ea747a6ab6726681d60a7bd5b097ff40b8 Create README file

The last commit message is an example of a bad commit message! You should try to avoid using general descriptions like the one I just used and instead use more specific messages (like those I wrote earlier).

I recommend reading How to Write Good Commit Messages: A Practical Git Guide.

Step 5

Add a new file, index.html to sleeptime-git folder with the following content:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SleepTime App</title>
</head>
<body>
  
</body>
</html>

Stage this file and commit the changes:

git add index.html
git commit -m "Add boilerplate html"

Staging multiple files

Add two more files to the working directory, style.css and script.js, with no content (empty files).

Link to these files in index.html:

<link rel="stylesheet" href="style.css">
<script src="script.js"></script>

I want to add these two files, as well as the changes I've made to the index.html, through a single commit. However, style.css and script.js are not yet staged for changes. I must explicitly add them using the git add command.

One way to add multiple files is to list them one by one (space separated):

git add style.css script.js

Another way, which is commonly used, is to use the following command (wildcard notion):

git add .

The git add . will add all changed and untracked files to the staging area (to be committed). You must be careful with this command as it will add "all" files, including those you probably don't intend to include (such as system files) to your Git repository. To avoid this, we will supply a .gitignore file first.

gitignore

A .gitignore file specifies intentionally untracked files that Git should ignore.

Create a file named .gitignore in sleeptime-git folder (notice the leading dot). I am going to add the following content to this file

.DS_Store
__MACOSX

My computer is a MacBook; the Mac OS generates system files .DS_Store and __MACOSX which are typically hidden (so you will not see them by default but Git will see and track them unless you tell it not to).

You can use this online tool to generate gitignore for different operating systems, project environments, etc.

Let's commit the .gitignore to our repository:

git add .gitignore 
git commit -m "Add gitignore"

Now, add and commit the changes made to our project earlier!

git add .
git commit -m "Link to external CSS and script files"

Step 6

Create HTML elements with JavaScript!

Add the following div element inside the body of index.html

<div id="root"></div>

Add the following content to script.js

let root = document.getElementById("root");

let p = document.createElement("p");
p.innerText = "If you go to bed NOW, you should wake up at...";
root.append(p);

let zzz = document.createElement("button");
zzz.innerText = "zzz";
root.append(zzz);

Save the files and open index.html in your favorite browser.

Notice we are using JavaScript to create content!

I'm personally not a fan of using JavaScript in this capacity. However, many JavaScript frameworks for building front-end applications work this way.

Let's add more; open script.js and append it with the following content:

let output = document.createElement("div");
output.className = "output";
root.append(output);

output.innerHTML = `
<p>It takes the average human fourteen minutes to fall asleep.</p>
<p>If you head to bed right now, you should try to wake up at one of the following times:</p>
<p id="hours">11:44 PM or 1:14 AM or 2:44 AM or 4:14 AM or 5:44 AM or 7:14 AM</p>
<p>A good night's sleep consists of 5-6 complete sleep cycles.</p>`;

Commit the changes:

git commit -am "Add basic content"

We can even add styling using JavaScript, but that would be excessive for now!

Step 7

Styling

Add the following content to style.css:

body {
    text-align: center;
    background-color: #001f3f;
    color: #7fdbff;
    font-size: 150%;
}

.output {
    border: 1px solid white;
    margin: 20px;
    display: none;
}

Commit the changes:

git commit -am "Add basic styling"

Event Listener

Add the following to the end of script.js:

function zzzOnClick() {
  let output = document.querySelector(".output");
  output.style.display = "block";
  let hours = "";

  // get current time
  let now = Date.now(); // in milliseconds
  let minute = 60 * 1000;  // milliseconds
  let cycle = now;

  // allow 14 minutes to fall sleep 
  cycle += 14 * minute;

  // calculate 6 sleep cycles (each 90 minutes)
  for(let i = 0; i < 6; i++) {
    cycle += 90 * minute;

    // append the sleep cycles to hours string
    hours += new Date(cycle).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });

    if (i < 5) {
      hours += " OR ";
    }
  }

  // output hours
  let hoursElm = document.querySelector("#hours");
  hoursElm.innerText = hours;
}

The code above is what we've developed in previous lectures.

We must now add an event listener to the zzz button. You can add this anywhere after the zzz element is created, but perhaps it is best situated right before the zzz element is appended to root:

index 871b173..b98de79 100644
--- a/script.js
+++ b/script.js
@@ -6,6 +6,7 @@ root.append(p);
 
 let zzz = document.createElement("button");
 zzz.innerText = "zzz";
+zzz.addEventListener("click", zzzOnClick);
 root.append(zzz);

Commit changes:

git commit -am "Add event listener for zzz button"

And with that, we have completed the SleepTime App, once again! 🎉🎉

Step 8

GitHub

Let's put our SleepTime App on GitHub for the world to see! Go to GitHub, login, and then create a new repository:

Once the repository is created, GitHub provides a few useful suggestions to start working with it.

Since we already have a local repository, we follow the instructions for "...or push an existing repository from the command line." For the repository I've created, this is the command I must use (the GitHub repo URL will be different for you):

git remote add origin https://github.com/cs280fall20/sleeptime-app.git
git branch -M master
git push -u origin master

Run the above commands (but use the ones generated for your repository) in the terminal. Make sure your current working directory is the sleeptime-git folder.

Next, refresh the page on GitHub. You must see the content of sleeptime-git folder uploaded to your GitHub repository.

Notice how GitHub automatically loads the content of README.md as the description of your repository.

In your GitHub repository, you can click on any of the files to open it. There is even a built-in editor.

Moreover, there is a list of all commits (similar to git log but fancier!).

In the view above, you can click on any commit to see a list of diffs associated with it.

Step 9

Let me emphasize Git and GitHub are not the same! Git is a version control system, whereas GitHub is a repository hosting service and a collaboration platform. GitHub provides a lot of useful services for hosting software projects. We will explore some of these features in this course.

GitHub Pages

Let's start with a fun one! GitHub Pages allows you to host a website from your repository. By default, GitHub Pages uses a static site builder called Jekyll. We, however, already built our website. We don't need Jekyll! To let GitHub know that, we must add a .nojekyll file to our repository.

Go to your local repository (the sleeptime-git folder on your computer) and add the file .nojekyll. The file has a leading dot in its name and no content (it's empty).

Commit the changes:

git add .nojekyll        
git commit -m "Add .nojekyll"

Although the changes are committed on your local repository, the remote repository on GitHub is behind your local repository.

To sync the local and the remote repositories, you need to push the latest local commits to the remote repository. You can do so simply by running the following command:

git push

Now go to GitHub, to your repository page. There must be a .nojekyll in there.

Next, open the "settings" tab in your GitHub repository. Find the section for "GitHub Pages" and set its source to the "master" branch as seen in the image below.

As soon as you save the changes, GitHub will show a message indicating "your site is published at ..."

The GitHub Page I've created is available here. Go ahead and share yours with your friends!

Step 10

Clone a remote repository!

The nice thing about a GitHub repository is that you can link multiple local repositories to it (very useful to sync between multiple devices/developers). Let's simulate this by linking another local repository to the remote one on GitHub. For this, we are going to clone the GitHub repository into a new directory.

Open the terminal and make sure you are not in the sleeptime-git directory where you currently store your local SeepTime App. Then, run the following command:

git clone <repository>

where <repository> is the URL of your GitHub repository. For me, the URL is https://github.com/cs280fall20/sleeptime-app.

Open the new directory created by the clone command. (On my computer, this directory is sleeptime-app.) This is a clone (a copy) of the GitHub repository.

Push vs Pull

Let's make some changes in this new local repository! We are going to implement the solution to Exercise 1 from Lecture 1.

Update script.js according to the diff
diff --git a/script.js b/script.js
index b98de79..9babecb 100644
--- a/script.js
+++ b/script.js
@@ -23,7 +23,8 @@ output.innerHTML = `
 function zzzOnClick() {
     let output = document.querySelector(".output");
     output.style.display = "block";
-    let hours = "";
+    let hours = document.querySelector("#hours");
+    hours.innerText = ""; // cleanup existing content
 
     // get current time
     let now = Date.now(); // in milliseconds
@@ -38,14 +39,14 @@ function zzzOnClick() {
         cycle += 90 * minute;
 
         // append the sleep cycles to hours string
-        hours += new Date(cycle).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+        let span = document.createElement("span");
+        span.id = "cycle-" + (i + 1);
+        span.innerText = new Date(cycle).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+        hours.appendChild(span);
 
         if (i < 5) {
-            hours += " OR ";
+            let or = document.createTextNode(" OR ");
+            hours.appendChild(or);
         }
     }
-
-    // output hours
-    let hoursElm = document.querySelector("#hours");
-    hoursElm.innerText = hours;
 }

Append the style.css
#cycle-1 {
  color: rgb(168, 39, 254);
}

#cycle-2 {
  color: rgb(154, 3, 254);
}

#cycle-3 {
  color: rgb(150, 105, 254);
}

#cycle-4 {
  color: rgb(140, 140, 255);
}

#cycle-5 {
  color: rgb(187, 187, 255);
}

#cycle-6 {
  color: rgb(143, 254, 221);
}

Commit the changes:

git commit -am "color-code the wake up hours"

Now the changes are committed to this local repository. You must push them to the remote repository on GitHub.

git push

Go to the sleeptime-git folder (which we created earlier in this lecture). This folder contains a local repository that is behind the changes made to your remote (and the other local) repository. To update and sync it with remote, execute the following command:

git pull

The command above will "pull" (fetch and merge) the latest changes from the remote (GitHub) repository. You will see a message similar to the one below:

remote: Enumerating objects: 7, done.
remote: Counting objects: 100% (7/7), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 4 (delta 2), reused 4 (delta 2), pack-reused 0
Unpacking objects: 100% (4/4), done.
From https://github.com/cs280fall20/sleeptime-app
   3c9d384..6279f94  master     -> origin/master
Updating 3c9d384..6279f94
Fast-forward
 script.js | 15 ++++++++-------
 style.css | 25 +++++++++++++++++++++++++
 2 files changed, 33 insertions(+), 7 deletions(-)

By the way, visit your deployed GitHub Page; it must reflect the latest changes - if not, refresh the page (while holding down the shift button).

Step 11

Merge Conflict

A common issue when working on multiple copies (clones) of a repository is a "merge conflict".

A merge conflict can occur when combining different versions of code, e.g. using git pull, if the different versions have different data in the same file.

Git will try to take care of merging automatically, but if two users edit, for example, the same file, a merge conflict will have to be manually resolved.

Let's simulate this! We have two local SleepTime App repositories. (On my computer, these are stored in folders sleeptime-git and sleeptime-app.) We will edit the style.css in both repositories!

In one of the repositories, open style.css and add the following property for body:

background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23006095' fill-opacity='0.4'%3E%3Cpath opacity='.5' d='M96 95h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm9-10v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-10 0v-9h-9v9h9zm-9-10h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9zm10 0h9v-9h-9v9z'/%3E%3Cpath d='M6 5V0H5v5H0v1h5v94h1V6h94V5H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");

The above background-image is a pattern in SVG format.

Commit and push the changes:

git commit -am "Add pattern to background"
git push

In the other local repository, open style.css and add the following:

button {
  padding: 20px;
  border-radius: 20px;
  text-transform: capitalize;
  color: #001f3f;
}

Commit and push the changes:

git commit -am "Style the Zzz button"
git push

The push command must be rejected with an error message similar to the following:

To https://github.com/cs280fall20/sleeptime-app
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'https://github.com/cs280fall20/sleeptime-app'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

The message says, in a nutshell, "the remote contains work that you do not have locally." The "work we don't have" are changes we've made earlier to style.css. You must first pull those changes from the remote (GitHub) repository before pushing new changes! So, run the following command:

git pull

The pull command will result in a merge conflict:

remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/cs280fall20/sleeptime-app
   6279f94..ee19cbc  master     -> origin/master
Auto-merging style.css
CONFLICT (content): Merge conflict in style.css
Automatic merge failed; fix conflicts and then commit the result.

Resolving merge conflicts

To resolve a merge conflict, simply remove all lines and code that are not wanted and push the results.

Open style.css (in the local repository where merge conflict occurred). You should see the additions you've made to style.css are annotated in a fashion similar to this

<<<<<<< HEAD

button {
  padding: 20px;
  border-radius: 20px;
  text-transform: capitalize;
  color: #001f3f;
}
=======
>>>>>>> ee19cbce36c878c0cbfccb4be983eafbf595b23e

Simply remove the annotations, and then commit and push changes!

git commit -am "Resolve merge conflic"
git push

When working in a team, it is the responsibility of the person who pushed their code last (and thus triggered the conflict) to resolve merge conflicts.

Step 12

Branching

Branching is a feature of Git that allows a project to move in multiple different directions simultaneously. There is one master branch that is always usable, but any number of new branches can be created to develop new features. Once ready, these branches can then be merged back into master.

When working in a Git repository, HEAD refers to the current branch being worked on. When a different branch is "checked out", the HEAD changes to indicate the new working branch.

As an example, we will implement the solution to Exercise 2 from Lecture 1 in a new branch.

Open the terminal and go to sleeptime-git folder. Run the following command:

git branch

The git branch command lists all the branches currently in a repository.

Let's create a new branch

git branch exercise-2

Switch current working branch to exercise-2

git checkout exercise-2
Update index.html
diff --git a/index.html b/index.html
index 45b3e44..845ef6c 100644
--- a/index.html
+++ b/index.html
@@ -7,7 +7,58 @@
     <link rel="stylesheet" href="style.css">
 </head>
 <body>
-    <div id="root"></div>
+
+  <p>I plan to fall sleep at...</p>
+
+  <select id="hh">
+      <option>(hour)</option>
+      <option>1</option>
+      <option>2</option>
+      <option>3</option>
+      <option>4</option>
+      <option>5</option>
+      <option>6</option>
+      <option>7</option>
+      <option>8</option>
+      <option>9</option>
+      <option>10</option>
+      <option>11</option>
+      <option>12</option>
+  </select>
+
+  <select id="mm">
+      <option>(minute)</option>
+      <option>00</option>
+      <option>05</option>
+      <option>10</option>
+      <option>15</option>
+      <option>20</option>
+      <option>25</option>
+      <option>30</option>
+      <option>35</option>
+      <option>40</option>
+      <option>45</option>
+      <option>50</option>
+      <option>55</option>
+  </select>
+
+  <select id="ampm">
+      <option>PM</option>
+      <option>AM</option>
+  </select>
+
+  <br>
+  <br>
+
+  <button onclick="calcOnClick();">CALCULATE</button>
+
+
+  <div class="output">
+      <p>If you fall sleep at the specified time above, you should try to wake up at one of the following times:</p>
+      <p id="hours">11:44 PM or 1:14 AM or 2:44 AM or 4:14 AM or 5:44 AM or 7:14 AM</p>
+      <p>A good night's sleep consists of 5-6 complete sleep cycles.</p>
+  </div>
+
     <script src="script.js"></script>
 </body>
-</html>
\ No newline at end of file
+</html>

Update script.js
diff --git a/script.js b/script.js
index a396733..26a44eb 100644
--- a/script.js
+++ b/script.js
@@ -1,39 +1,23 @@
-let root = document.getElementById("root");
-
-let p = document.createElement("p");
-p.innerText = "If you go to bed NOW, you should wake up at...";
-root.append(p);
-
-let zzz = document.createElement("button");
-zzz.innerText = "zzz";
-zzz.addEventListener("click", zzzOnClick);
-root.append(zzz);
-
-let output = document.createElement("div");
-output.className = "output";
-root.append(output);
-
-output.innerHTML = `
-<p>It takes the average human fourteen minutes to fall asleep.</p>
-<p>If you head to bed right now, you should try to wake up at one of the following times:</p>
-<p id="hours">11:44 PM or 1:14 AM or 2:44 AM or 4:14 AM or 5:44 AM or 7:14 AM</p>
-<p>A good night's sleep consists of 5-6 complete sleep cycles.</p>`;
-
-
-function zzzOnClick() {
+function calcOnClick() {
     let output = document.querySelector(".output");
     output.style.display = "block";
     let hours = document.querySelector("#hours");
     hours.innerText = ""; // cleanup existing content
 
     // get current time
-    let now = Date.now(); // in milliseconds
+    let hh = document.getElementById("hh").value;
+    let mm = document.getElementById("mm").value;
+    let ampm = document.getElementById("ampm").value;
+    hh = ampm === "PM" ? hh + 12 : hh; 
+    
+    let now = new Date();
+    now.setHours(hh);
+    now.setMinutes(mm);
+    now = now.valueOf();
+
     let minute = 60 * 1000;  // milliseconds
     let cycle = now;
 
-    // allow 14 minutes to fall sleep
-    cycle += 14 * minute;
-
     // calculate 6 sleep cycles (each 90 minutes)
     for(let i = 0; i < 6; i++) {
         cycle += 90 * minute;
@@ -49,4 +33,4 @@ function zzzOnClick() {
             hours.appendChild(or);
         }
     }
-}
\ No newline at end of file
+}

Run the application and check if it works correctly!

There is a bug 🐛 in the solution provided here. In class, we will use the built-in debugger in Chrome/FireFox Developer Tools to debug it.

Debug

The bug is in script.js. Here is a fix:

-    hh = ampm === "PM" ? hh + 12 : hh; 
+    hh = ampm === "PM" ? Number.parseInt(hh) + 12 : hh;

Commit the changes:

git commit -a -m "Let user specify sleep time" -m "Solution to Exercise 2"

Notice we can supply two messages to the commit command:

git commit -m "what I changed" -m "why I changed it"

Push the changes

git push

You will get an error message similar to the following:

fatal: The current branch exercise-2 has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin exercise-2

It says, in a nutshell, the local branch exercise-2 does not exist on GitHub (the remote repository). It also tells you how to "properly push" to fix the issue:

git push --set-upstream origin exercise-2

Visit your GitHub repository; you must have two branches now!

Use the branch dropdown to select the branch you would like to view:

Switch between branches

Locally, you can switch between branches using the git checkout command. For example, switch back to the master branch:

git checkout master

Note how the mater branch is at the state where you have left it (see the content of index.html and script.js).

Checkout a remote branch

Open the other local repository of SleepTime App. Run the following command in the terminal:

git branch

The output must be

* master

In order to check out the new remote branch, you must run the following commands:

git fetch origin
git checkout exercise-2

Clone a specific branch

You can clone a specific branch with the following command

git clone -b <branch> <remote_repo>

where <branch> is the specific branch name and <remote_repo> is the URL of the remote repository. For example, to checkout the branch exercise-2 form my SleepTime App GitHub repository, you run the following command:

git clone -b exercise-2 https://github.com/cs280fall20/sleeptime-app

Step 13

GitHub Issues

Most software projects have a bug tracker of some kind. GitHub's tracker is called Issues, and has its own section in every repository.

Although it was originally intended to track bugs, Issues are now used to keep track of enhancements, product road-map, for planning, feature requests, etc.

Issues is at the heart of GitHub and acts as a kind of chatroom/forum/email where all members of your team can communicate about your software project.

GitHub Collaborators

Go to the "settings" of your GitHub repository! Click on "Manage access".

Here you can invite other GitHub members to collaborate with you.

I have a second account on GitHub, ali-the-student; I've invited myself as a collaborator here!

GitHub Pull Request

A pull request can be made to merge a branch of a repository with another branch of the same repository (or even a different repository). Pull requests are a good way to get feedback on changes from collaborators on the same project.

Let's create a pull request. Go to GitHub, to your SleepTime App repository. Go to the "Pull request" tab and click on "New pull request".

Create a pull request from exercise-2 branch to the master branch:

You can assign collaborators to code review and approve the pull request. We will show you how this works in class.

After the request has been approved by reviewers, and if there is no merge conflict, you can "Merge pull request".

Now the master branch has the latest updates from the exercise-2 branch.

GitHub Flow

The following workflow is known as the GitHub flow:

  • Communicating using GitHub Issues
  • Creating Git branches (for fixing bugs, adding/updating features, etc)
  • Using GitHub Pull request to introduce changes
  • Merging changes

You are expected to follow this workflow for homework.

Here is a poster that summarizes GitHub Flow.

GitHub Forks

A "fork" of a repository is an entirely separate repository which is copy of the original repository. A forked repository can be managed and modified like any other, all without affecting the original copy.

Feel free to fork the repository for this lecture at: https://github.com/cs280fall20/sleeptime-app

Open source projects are often developed using forks. There will be one central version of the software which contributors will fork and improve on, and when they want these changes to be merged into the central repository, they submit a "pull request".

Control Flow

In this chapter, we will explore JavaScript control structures.

Equality Operators

JavaScript has two types of equality/inequality operators: the strict and the abstract equality operators.

Abstract Equality

The operator looks similar to that used in languages like Java and C++:

console.log(2 == 2);
console.log(1 == 2);
console.log(3 != 2);

The problem is, when the operands are of different types, the abstract equality operator converts the types first!

console.log("2" == 2);
console.log(1 == true);
console.log(0 == false);
console.log("" == false);
console.log("0" == false); // both are converted to 0

Type conversion before checking for equality is more error prune that it can possibly be desired.

Do not use the "abstract equality" operators == and !=.

Strict Equality

The strict equality uses an extra equal sign, and performs the equality comparison in a more exact identify sort of matching.

When the operands have the same type, their value is compared:

console.log(2 === 2);
console.log(1 === 2);
console.log(3 !== 2);

If the operands don't have the same type, then they are considered unequal:

console.log("2" === 2);
console.log(1 === true);
console.log(0 === false);
console.log("" === false);
console.log("0" === false);

While numbers, boolean values and strings compared by value, object/array references are strictly equal if they refer to the same object in memory:

const student = { name: "John Doe" };
const courseAssistant = { name: "John Doe" };
const headAssistant = courseAssistant;

console.log(student === courseAssistant);        // false
console.log(headAssistant === courseAssistant);  // true

A good read: Object equality in Javascript.

Nuances

Note that undefined and null only equal to themselves (unless using abstract equality).

console.log(undefined === undefined);
console.log(null === null);
console.log(undefined === null);  // false
console.log(undefined == null);   // true

Here is a bit of madness, NaN is not equal to NaN no matter what equality operator you use

console.log(NaN == NaN);
console.log(NaN === NaN);

So JavaScript's equality operator is not an equivalence relation (since it is not reflexive).

If you want to check if a variable is NaN, you should try one of the following static methods:

let num = NaN;
console.log(Number.isNaN(num));
console.log(Object.is(num, NaN));

Object.is is a new addition to JavaScript which works similar to strict equality except for NaN and +0/-0.

console.log(+0 === -0);
console.log(Object.is(-0, +0));

JavaScript Object.is is designed to have the properties of an equivalence relation (it is reflexive, symmetric, and trasitive).

MDN Wed Docs has a great article on JavaScripts "Equality comparisons and sameness" available at this link.

Comparison Operators

Javascript has the following comparison operators:

  • < less than
  • <= less than or equal
  • > greater than
  • >= greater than or equal

Use these to compare numbers with numbers or strings with strings.

Strings are compared lexicographically:

console.log("Hello" < "Goodbye");
console.log("Hello" < "Hi");

There are not "strict" version of comparison operations. So, if you mix types, JavaScript will go about converting types!

console.log("42" < 5);    // "42" is converted to the number 42
console.log("" < 5);      // "" is converted to the number 0
console.log("Hello" < 5); // "Hello" is converted to NaN
console.log([2] < 5);     // [2] is converted to the number 2
console.log([1, 2] < 5);  // [1, 2] is converted to "1,2"
console.log(true > 2);    // true is converted to the number 1

More madness:

console.log(NaN < 1);
console.log(NaN > 1);
console.log(undefined < 1); 
console.log(undefined > 1);     
console.log(null < 1);
console.log(null > 1);  

The logic(!) behind the madness:

  • When one operand is a string and the other is a number, the string is converted to a number before comparison.
  • When the string is non-numeric, numeric conversion returns NaN. Comparing with NaN always returns false.
    • However, null, false, and empty string convert to the number 0. And, true converts to the number 1.
  • When one operand is an object and the other is a number, the object is converted to a number before comparison.

Avoid mixed-type comparisons!

Logical Operators

Javascript has the following comparison operators:

  • && and
  • || or
  • ! not

The logical operators work normally when their operands are boolean expressions, that is, for instance, x && y will return true if both x and y evaluate to true. It will also employ short-circuiting and will not evaluate y if x evaluates to false.

const isCitizen = true;
const age = 28;
console.log(isCitizen && age > 64); 

Let's revisit the earlier statement:

The logical operators work normally when their operands are boolean expressions.

What if their operands are not boolean expressions? Hmm, you may be thinking the operands are probably converted to boolean and then ... let me stop you right there. Embrace yourself for madness!

JavaScript && and || operators actually don't return a boolean!! They always return one of their operands! If the operands happened to be booleans, then it looks like everything works normally. But things get weird when one or more operand are not boolean!

Here is what really happens:

x && y: If x can be converted to true, returns y; else, returns x

console.log(true && "Ali"); 
console.log(false && "Ali"); 

x || y: If x can be converted to true, returns x; else, returns y

console.log(true || "Ali"); 
console.log(false || "Ali"); 

The JavaScript community has (perhaps unfortunately) embraced these features. You will find expressions such as

DEBUG && console.log("Some debugging message");

which is a short form of

if (DEBUG) {
  console.log("Some debugging message");
}

The variable DEBUG is presumably a boolean value but it does not have to be. (See the next section!)

For a more in-dept consideration of logical operators, read Aashni's "Lazy Evaluations and Short Circuit Logic in Javascript"

Boolean-ish

So what is the output here?

console.log("to be" || "not to be"); 
Explanation

The value "to be" is evaluated to "true". The OR logical operator returns the first operant if that operand is evaluated to true.

Boolean-ish: truthy or falsy values!

  • If a value can be converted to true, the value is so-called truthy.
  • If a value can be converted to false, the value is so-called falsy.
  • 0, NaN, null, undefined, empty string ("" or '' or ``) are all falsy.
  • All other values are truthy.

Logical operators (as well as if/loop conditions) embrace Boolean-ish!

console.log(null && "Ali");      // null evaluates to false
console.log(undefined || "Ali"); // undefined evaluates to false 
console.log(!"Ali");             // "Ali" evaluates to true 

The Boolean-ish is embraced by the JavaScript community too. For instance, it is very common to see code like this

if (user) { // Skipped if user is undefined or null

}

Which can easily be converted to

if (user !== undefined && user !== null) {

}

Another common use is to set a default value to a function argument.

function calcWage (hoursWorked, hourlyPayRate) {
  hourlyPayRate = hourlyPayRate || 12.5;
  // calculate the wages!
} 

calcWage(40);     // uses default hourly pay rate.
calcWage(35, 15);

Which can easily be avoided by using default function parameters! (We will properly cover functions in a future lecture.)

function calWage (hoursWorked, hourlyPayRate = 12.5) {
  // calculate the wages!
} 

My advice is to avoid tricky or hacky expressions! Code what you mean! Especially when it is not much more work to do so.

There are cases where it might be more work; for example, the following expression

query = query && query.trim() || "";

does the job of the code below:

if (query !== undefined && query !== null) {
  query = query.trim(); // removes whitespace from both ends 
} else {
  query = ""; 
}

Here is another one:

denominator = denominator || 1;

which does the job of the following code:

if (
  denominator === undefined ||
  denominator === null ||
  Number.isNaN(denominator) ||
  denominator === 0
) {
  denominator = 1;
}

if statement

JavaScript has a if statement similar to languages like Java and C++, except that the condition of an if statement can be anything that can be coerced to a boolean value (see the previous section on Boolean-ish values)

if (condition) {
	// statements
}

The statements inside an if block can contain other control structures, including other if statements (nested if statements).

const age = 17;

if (age >= 13) {
	if (age <= 19) {
		console.log("You are a teenager!");
	}
}

An if condition can be expanded with an else block

const num = 37;

if (num % 2 === 0) { // check if num is even
	console.log(num + " is even!");
} else {
	console.log(num + " is odd!");
}

If you have only one statement, the braces around that statement are optional. Consider it good practice to always include braces.

const age = 12;

if (age >= 13)
	if (age <= 19)
		console.log("You are a teenager!");
else
	console.log("You are NOT a teenager!");

// The else is paired with the nearest if 
// unless you enclose the second if in brackets.

You can chain several if statements:

const score = 73;

// Convert score to letter grade
if (score >= 85) {
	console.log("Grade: A");
} else if (score >= 70) {
	console.log("Grade: B");
} else if (score >= 55) {
	console.log("Grade: C");
} else {
	console.log("Grade: F");
}

The if-else-if ladder exits at the first success. If the conditions overlap, the first criteria occurring in the flow of execution is executed.

Ternary operator

Consider the following expression that uses the ternary operator ?:

let max = a > b ? a : b;

which is the same as

let max;
if (a > b) { 
	max = a;
} else {
	max = b;
}

switch statement

When needing to do a lot of comparisons for a single value, instead of using many if-else-if statements, you can use the switch statement:

const letter = "B-";
let gpa;

switch (letter) {
  case "A+":
  case "A":
    gpa = 4.0;
    break;
  case "A-":
    gpa = 3.7;
    break;
  case "B+":
    gpa = 3.3;
    break;
  case "B":
    gpa = 3.0;
    break;
  case "B-":
    gpa = 2.7;
    break;
  case "C+":
    gpa = 2.3;
    break;
  case "C":
    gpa = 2.0;
    break;
  case "C-":
    gpa = 1.7;
    break;
  case "D+":
    gpa = 1.3;
    break;
  case "D":
    gpa = 1.0;
    break;
  case "D-":
    gpa = 0.7;
    break;
  case "F":
    gpa = 0.0;
    break;
  default:
    gpa = null;
}

if (gpa !== null) { 
  console.log("Your GPA is " + gpa);
} else {
  console.error(letter + " cannot be converted to GPA value");
}
  • The default case in a switch statement is like the last else in an if-else-if chain. It will be reached if none of the previously tested conditions are true.

  • The break statement is needed to break out of the switch statement. In case you omit break, switch will run all the following cases until it encounters break or exits. This can be useful for grouping cases.

Keep in mind that JavaScript uses strict equality for switch statements.

Loops

JavaScript supports the standard loop constructs you have seen in other programming languages:

let counter = 0;
while (counter < 10) {
	console.log(counter);
	counter++;
}

In a do-while loop, the body of the loop is executed once and then, if the condition holds, the program returns to the top of the block.

let counter = 10;
do {
	console.log(counter);
	counter++;
} while (counter < 10);

When it comes to counter-controlled loops (when we know exactly how many iterations we need, e.g. when we go over the elements of an array), the for loop provides a more compact syntax:

for (let counter = 0; counter < 10; counter++) {
	console.log(counter);
}

You can cram in multiple variables, update expressions:

const arr = [10, 20, 30, 40, 50];

for (let i = 0, j = arr.length - 1; i < j; i++, j--) {
  const temp = arr[i]
  arr[i] = arr[j]
  arr[j] = temp
}

console.log(arr);

The while and do-while loops are best for event-controlled loops (when the number of iteration is unknown before runtime, e.g. when we read data from file and we don't know how many lines — or how many values are there).

break & continue

The break statement exits out of a loop, while the continue statement will skip to the next iteration:

const arr = [10, 20, 30, 40, 50];
const target = 40;

let index = -1;
for (let i = 0; i < arr.length; i++) {
  if (arr[i] === target) {
    index = i;
    break; // stop early!
  }
}

console.log(index);
const arr = [10, , 30, , 50];
let count = 0;
let sum = 0;

for (let i = 0; i < arr.length; i++) {
  if (arr[i] === undefined) {
    continue; // ignore missing data!
  }
  count++
  sum += arr[i]
}

const avg = count === 0 ? 0 : sum / count;
console.log(avg);

for..of loop

The for..of loops are used for looping through iterable objects such as arrays:

let arr = [10, 20, 30];

for (let item of arr) {
   console.log(item); 
}

It also works with strings:

const greeting = "Hello!"
for (const ch of greeting) { 
  console.log(ch);
}

for..in loop

The for..in loop allows you to iterate over keys of enumerable object properties.

const user = {
   firstName: "Ali",
   lastName: "Madooei"
};

for (const key in user) {
   console.log(key, user[key]);
}

It works with strings and arrays too, but iterates over the indices:

const arr = [10, 11, 12];
for (const key in arr) {
  // console.log(key);
  console.log(arr[key]);
}
const greeting = "Hello!"
for (const key in greeting) { 
  // console.log(key);
  console.log(greeting[key]);
}

Tic Tac Toe

We are going to implement a two-player Tic-Tac-Toe game similar to (but simpler than) http://playtictactoe.org/.

Tic-Tac-Toe is a paper-and-pencil game for two players, X and O, who take turns marking the spaces in a n x n grid (usually 3 x 3). The player who succeeds in placing n of their marks in a horizontal, vertical, or diagonal row wins the game.

Step 1

Clone the starter code:

git clone -b starter https://github.com/cs280fall20/tic-tac-toe.git

This lesson is designed as a coding exercise. It provides you with an opportunity to practice what we've covered so far, in particular JavaScript Control flow. Moreover, the implementation exhibits a modular structure that uses JavaScript functions. The latter will be covered in detail in the next module.

If you are lucky, you make mistakes and will have a chance to use the debugger as well!

The starter code contains three files:

  • index.html
  • style.css
  • script.js

We will review each in the following sections.

Step 2

The index.html is mostly boilerplate code. The following piece is responsible for structuring the game board.

<div class="game board">
  <div class="row">
    <div class="cell" id="0"></div>
    <div class="cell" id="1"></div>
    <div class="cell" id="2"></div>
  </div>
  <div class="row">
    <div class="cell" id="3"></div>
    <div class="cell" id="4"></div>
    <div class="cell" id="5"></div>
  </div>
  <div class="row">
    <div class="cell" id="6"></div>
    <div class="cell" id="7"></div>
    <div class="cell" id="8"></div>
  </div>
</div>

Step 3

The style.css is not of interest to us except to note we use CSS selectors x and o to style a cell which is marked by the cross or the naught marker. That is, when a cell is marked by a player, you must add the class name x or o to its class attribute. Let's make a concrete example to better understand this.

  • Open index.html in your favorite browser (Chrome is recommended).
  • Open the browser console (Developer Tools).
  • Run the following command to mark the first cell with a cross:
    let cell = document.getElementById("0");
    cell.classList.add("x");
    

Exercise: mark the board's middle cell with a naught.

Solution
cell = document.getElementById("4");
cell.classList.add("o");

Step 4

The script.js contains a partial implementation of the Tic-Tac-Toe game. Your task is to complete the code. But first, let's explore the content of script.js. Here are the first few lines:

let cells = document.querySelectorAll(".cell");
cells.forEach(function (cell) {
  cell.addEventListener("click", game);
});

The querySelectorAll returns an array of all HTML elements with the class name of "cell".

The forEach() method executes a provided function once for each array element. For clarity, let's rewrite the second statement:

cells.forEach(runGameOnClick);

function runGameOnClick(element) {
  element.addEventListener("click", game);
}

In JavaScript, we can pass a function as a parameter to another function!

The original statement declares an anonymous function.

An anonymous function is a function without a name.

One common use for anonymous functions is as arguments to other functions.

cells.forEach(function (cell) {
  cell.addEventListener("click", game);
});

Another way of writing anonymous functions is using arrow functions:

cells.forEach( (cell) => {
  cell.addEventListener("click", game);
});

We will explore these alternative syntaxes to function declaration at a later time.

So, the first few lines are simply saying that the game function must be called every time a user clicks on any of the cells (of the game board).

Note the addEventListener method takes two arguments:

  • A (case-sensitive) string representing the event type to listen for
  • A function that will be called whenever the specified event is delivered to the target element.

In our solution, the addEventListener will call the game function when a user clicks on a cell. It will pass an object, event to the game function. The event object contains many properties about the specified event, including a reference to the target element (cell) which the event occurred.

So, when a user clicks on a cell, you can access that cell and e.g. its id attribute as follows:

function game(event) {
  let cell = event.target;
  console.log(cell.id); 
}

Feel free to experiment with the above simplistic implementation of game; every time user clicks on a cell, its id attribute will be printed to the console.

Step 5

Note the following variables are used to hold the state of the game:

const numRows = 3;
const numCols = 3;
let numEmptyCells = numRows * numCols;
const board = new Array(numEmptyCells);
const markers = ["x", "o"];
let player = 0;
let gameIsOver = false;

The variables above are declared in the script.js (outside of any functions) and therefore have a global scope. It is generally not a good practice to declare globally scoped variables; we leave it at that for now and return to the topic in later modules.

The remainder of script.js contains several function definitions. These functions are defined in standard form using a function keyword, followed by the function name, parameters (if any), and the function body.

Note a JavaScript function can return a value (using the return statement). Unlike functions in Java and C++, functions in JavaScript have no parameter/return types.

Let's look at the function definition of toLinearIndex as an example:

// return the linear index corresponding to the row and column subscripts
function toLinearIndex(row, col) {
  return row * numRows + col;
}

Notice numRows is defined globally and the function has access to it.

The game board is a 3 x 3 grid:

However, we store this board in a 1D array (board):

The toLinearIndex function, as the name suggests, returns the linear index into board corresponding to the row and column subscripts of the game board.

Step 6

The code for any application, even as simple as the Tic-Tac-Toe game, can quickly get out of hand unless we take a modular approach and break the problem down into smaller tasks, each of which can be coded with one or more functions. Here's a modular structure chart for the provided (partial) implementation:

You must carefully study the modular structure and the description of each function before completing the code. I recommend working through the exercise in the following order:

  1. Implement switchPlayer
  2. Implement checkRow
  3. Implement checkColumns
  4. Implement checkColumn
  5. Implement checkMajorDiagonal
  6. Implement checkMinorDiagonal
  7. Complete the implementation of game

We are taking a top-down approach to design step-wise refinement to the implementation of this game.

In this approach, it may not be possible to run the application to test the correctness of your implementation as you progress through (because at any point, some of the functions are not implemented yet). You can use the debugger to step through the code to inspect its implementation. Ideally, we must perform unit testing. For now, to keep things simple, we let go of writing tests (but we will do it later in this course). Luckily, each function is small in scope and the application (game) is simple as a whole. It will not be difficult to get the code working correctly with a few attempts.

Step 7

Here are the solutions. Don't use a solution here until you've finished implementing all the missing pieces and debugged your implementation.

  1. Implement switchPlayer

    Solution
    // switches the player value from 0 to 1 and vice versa
    function switchPlayer() {
      player = player === 0 ? 1 : 0;
    }
    
  2. Implement checkRow

    Solution
    // returns true if all cells in the given row
    // are marked with the same marker, otherwise returns false
    function checkRow(row) {
      for (let col = 0; col < numCols; col++) {
        let index = toLinearIndex(row, col);
        if (board[index] !== markers[player]) return false;
      }
      return true;
    }
    
  3. Implement checkColumns

    Solution
    // returns true if all cells in a column
    // are marked with the same marker, otherwise returns false
    function checkColumns() {
      for (let col = 0; col < numCols; col++) {
        if (checkColumn(col)) return true;
      }
      return false;
    }
    
  4. Implement checkColumn

    Solution
    // returns true if all cells in the given column
    // are marked with the same marker, otherwise returns false
    function checkColumn(col) {
      for (let row = 0; row < numRows; row++) {
        let index = toLinearIndex(row, col);
        if (board[index] !== markers[player]) return false;
      }
      return true;
    }
    
  5. Implement checkMajorDiagonal

    Solution
    // returns true if all cells in the major diagonal
    // are marked with the same marker, otherwise returns false
    function checkMajorDiagonal() {
      for (let row = 0; row < numRows; row++) {
        let index = toLinearIndex(row, row);
        if (board[index] !== markers[player]) return false;
      }
      return true;
    }
    
  6. Implement checkMinorDiagonal

    Solution
    // returns true if all cells in the minor diagonal
    // are marked with the same marker, otherwise returns false
    function checkMinorDiagonal() {
      for (let row = 0; row < numRows; row++) {
        let index = toLinearIndex(row, numCols - row - 1);
        if (board[index] !== markers[player]) return false;
      }
      return true;
    }
    
  7. Complete the implementation of game

    Solution
    // the main event listener and the controller for the game
    function game(event) {
      if (gameIsOver)
        return window.alert(
          "This game has ended. Refresh the page for a new game!"
        );
    
      let cell = event.target;
      let index = cell.id;
    
      if (board[index]) return window.alert("This cell has already been marked!");
    
      // update the board
      board[index] = markers[player];
      cell.classList.add(markers[player]);
      numEmptyCells--;
    
      updateGameStatus();
      switchPlayer();
    }
    

The app must be working now! Enjoy playing the Tic-Tac-Toe game 🎉

You can download the complete implementation from https://github.com/cs280fall20/tic-tac-toe.git.

Step 8

The implementation of checkRow, checkColumn, checkMajorDiagonal and checkMinorDiagonal are fairly similar. Can you refactor the code and extract a function that factors the common behavior?

Solution

It is possible to do this, in particular for checkRow, checkColumn, as follows:

  • Add the following function
    function checkCells(rowFirst, rowLast, colFirst, colLast) {
      for (let row = rowFirst; row < rowLast; row++) {
        for (let col = colFirst; col < colLast; col++) {
          let index = toLinearIndex(row, col);
          if (board[index] !== markers[player]) return false;
        }
      }
      return true;
    }
    ```
    
  • Update checkRow
    function checkRow(row) {
      return checkCells(row, row + 1, 0, numCols);
    }
    
  • Update checkCol
    function checkColumn(col) {
      return checkCells(0, numRows, col, col + 1);
    }
    

To use checkCells for checking major/minor diagonal, we need to employ a more complex (and convoluted) logic.

The changes already make for a less readable code and a more complex and convoluted checkCells will change this situation for the worse. My preference would be keeping the code as is (without using checkCells at all).

Functions

Functions are a core construct in many programming languages. In JavaScript, they are first-class citizens which means that you can treat them like other values:

  • Store functions in variables
  • Pass functions to other functions
  • Return functions from functions
  • and more!

We will go over many of these features in this module.

Defining Functions

Here is a JavaScript function:

function total(x, y) {
  return x + y;
}

It is, in many ways, similar to functions in languages like Java and C++:

  • A function can take zero or more named parameters.
  • A function groups several statements (in the function body).
  • A function creates scope: variables declared in a function are local to that function (even when declared with the var keyword).
  • A function can return a value (using the return statement).
  • A return statement terminates the function immediately.

JavaScript functions are different in several ways from functions in Java and C++. The most visible difference (besides the use of the function keyword) is the lack of types; there are no parameter/return types.

Moreover, JavaScript functions always return a value! If a function terminates without explicitly returning a value, it returns undefined.

The most important difference, however, is that JavaScript functions are values (well, actually, they are [special] objects). You can store functions in variables. You can also pass them as arguments to other functions or return a function from another function.

Terminology: A higher order function is a function that takes a function as an argument (or returns a function).

In contrast, first order functions don't take a function as an argument (nor they return a function as output).

JavaScript supports both first and higher order functions.

Calling Functions

To invoke a JavaScript function, call its name followed by a pair of parentheses with zero or more arguments.

const result = add(2, 1); 

When using a function as a value, use its name without parentheses/arguments.

const sum = add;
// you can use sum as an alias to add
// e.g. const result = sum (2, 1)

Function Expression

The function keyword can be used to define a function inside an expression.

const sum = function total(x, y) {
  return x + y;
};

You will not be able to call the function using its name total. Instead, you must use the alias sum to invoke the function. In fact, in a function expression, the function name can be omitted to create anonymous functions.

const sum = function (x, y) {
  return x + y;
};

In anonymous functions, you will not be able to refer to the function within itself, e.g. for recursion.

Anonymous Functions

An anonymous function is one without a name after the function keyword:

const mean = function (x, y) {
  return (x + y) / 2;
};

In contrast, a named function is one that has a name after the function keyword.

A common use for anonymous functions is as arguments to other functions.

const numbers = [1, 2, 3, 4, 5, 6, 7];

const evens = numbers.filter(function (x) { 
  return x % 2 === 0; 
});

console.log(evens);

Arrow Function Expressions

Relatively new addition to the language, they provide a more compact form for anonymous functions:

const mean = (x, y) => {
  return (x + y) / 2;
};

Notes on the syntax:

  • Function parameters are provided in parentheses proceeding the fat arrow (=>)
    • Parentheses are optional for a single parameter.
    • Use empty parentheses if no parameters.
  • Function body is contained within curly brackets following the fat arrow (=>)
    • Curly brackets are optional when the function body is made up of a single statement.
    • If that single statement is a return statement, you can eliminate the return keyword, too.
    • If the single return statement involves returning an object, to avoid confusion between curly brackets for object literal with curly brackets for the function body, surround the object literal in parentheses.
      const user = username => ({ username });
      
      is the same as
      const user = (username) => {
        return { username };
      };
      

Arrow functions are commonly used for declaring functions as arguments to other functions:

const numbers = [1, 2, 3, 4, 5, 6, 7];

const evens = numbers.filter(x =>  x % 2 === 0);

console.log(evens);

Arrow functions do not exhibit all behaviors of normal (named or anonymous) JavaScript functions. We will explore some of their limitations in later sections.

Hoisting

You can use (call) a function before it is defined!

const result = add(2, 1);
console.log(result);

function add(a, b) {
  return a + b;
}

The observed phenomenon is due to hoisting.

Hoisting is a JavaScript mechanism where variables (declared with var keyword) and function declarations are moved to the top of their scope before code execution.

The caveat is when using function expressions; they are not hoisted!

const result = add(2, 1);
console.log(result);

const add = function (a, b) {
  return a + b;
}

Here is a clever example that uses (mutual) recursion and hoisting:

function isEven(n) { 
  return n == 0 ? true : isOdd(n - 1);
}

function isOdd(n) { 
  return n == 0 ? false : isEven(n - 1);
}

const n = 3; // feel free to change this value!
console.log(`isEven(${n}) -> ${isEven(n)}`); 
console.log(`isOdd(${n}) -> ${isOdd(n)}`); 
Step-by-step execution

Open the visualizer in a new window

Here is a tricky example of hoisting: what does this snippet print?

var num = 1;
print();

function print() {
  console.log(num);
  var num = 2;
}
Explanation

The answer is undefined because the num variable inside the print function is hoisted to the top of its scope (which is the top of the print function). The following snippet shows, roughly, how the hoisting works for the given example:

var num;
num = 1;

function print() {
  var num;
  console.log(num);
  num = 2;
}

print();

Arguments

You can call JavaScript functions with fewer or more arguments than the number they expect!

Fewer Arguments

Missing argument? Parameter is undefined:

function myfunction (x, y) {
  console.log(x, y);
}

myfunction(2);

This feature can be used to create optional parameters; check for undefined parameters and assign them a default value.

In modern JavaScript, you can use default parameter values by assigning a value to a parameter in the function signature.

function average(x, y = x) {
  return (x + y) / 2;
}

Multiple default arguments ok:

function average(x = 0, y = x) {
  return (x + y) / 2;
}

console.log(average(2,3));
console.log(average(2));
console.log(average());

More Arguments

If you provide a function with more arguments than it expects, it will ignore the extra ones!

function myfunction (x, y) {
  console.log(x, y);
}

myfunction(1, 2, 3, 4, 5, 6);

Rest Parameters/Spread Operator

We've seen the "rest parameters" in the destructuring assignment with arrays and objects. You can use the same pattern to capture a variable number of arguments:

function total(...args) {
  let sum = 0;
  for (x of args) sum += x;
  return sum;
}

console.log(total(1));
console.log(total(1, 2));
console.log(total(1, 2, 3));
console.log(total(1, 2, 3, 4));

JavaScript has a "spear operator" which looks identical to the "rest operator" but its purpose is to expand (spread out) the elements of an iterable object.

const numbers = [3, 4, 5]
const result = total(...numbers);

You can create a function that takes both named parameters and rest parameter but in that case the rest parameter must be the last parameter.

function myFunction (arg1, arg2, ...rest) { 
  /* do something awesome with all the arguments! */ 
}

Arguments Object

If you are puzzled by how functions can be called with fewer/more arguments, here is an explanation. It turns out, the named parameters of a function are more like guidelines than anything else. Every function has access to an array-like object called arguments. This object holds all of the values passed to the function.

function myfunction () {
  for (let i = 0; i < arguments.length; i++) {
    console.log(arguments[i]);
  }
}

myfunction(1, 2, 3, 4);

If you declare a function with \(n\) named parameters, JavaScript will assign the first \(n\) elements of arguments to those parameters, in order.

Caution: Arrow function do no have argumnets object.

Function Scoping

When you define a function, it creates a lexical scope.

A lexical scope or static scope in JavaScript refers to the accessibility of the variables, functions, and objects based on their physical location in the source code.

Any value defined within the function will not be accessible by code outside the function.

function myFunction () {
  var num = 4;
}

console.log(num);

Inner Functions

JavaScript functions can be declared inside other functions! Each function creates its scope.

function outer() {
  let a = 1;

  function inner() {
    let a = 2;
    let b = 3;
    console.log(a);
    console.log(b);
  }

  inner();
  console.log(a);
  console.log(b);
}

outer();

The inner functions have access to the variables in their parent function.

function outer() {
  let num = 1;

  function inner() {
    num++;
  }

  console.log(num);
  inner();
  console.log(num);
}

outer();

When one should use inner functions? If a function relies on a few other functions that are not useful to any other part of your code, you can nest those helper functions inside it.

Functional Encapsulation

The fact functions create scope and they can be nested allow us to achieve Object-Oriented Programming (OOP)like encapsulation and information hiding:

A function can bundle several related functions and allow them share variables declared in the parent function. (Similar to what a class does in OOP.)

Recall the implementation of Tic-Tac-Toe: we had a handful of globally-scoped variables. We could wrap the entire content of script.js into a function and then call that function to put its content in action.

function tictactoe() {
  // copy over the content of script.js
}

tictactoe();

We can also use a Self-Executing Anonymous Function (also known as Immediately Invoked Function Expression, or IIFE):

(function () {
  // copy over the content of script.js
})();

This strategy used to be a very common pattern employed by JavaScript programmers to create OOP-like encapsulation and prevent polluting the global scope.

IIFE

An IIFE (Immediately Invoked Function Expression) is a JavaScript function that runs as soon as it is defined.

(function () {
    // statements
})();

It contains two major parts:

  1. The anonymous function enclosed within the Grouping Operator ().
  2. The following () which creates the immediately invoked function expression.

In Modern JavaScript, you should use Modules and Classes instead of IFFEs.

JavaScript Modules and Classes will be explored in near future.

Currying and Closure

Consider this function:

function makeSundae(base, topping) {
  return `Here is your ${base} ice cream with ${topping}!`;
}

const sundae = makeSundae("vanilla", "hot fudge");
console.log(sundae);

The fact functions are values enable us to make a function that returns a function. This can be used in interesting ways!

function makeSundae(base) {
  return function (topping) {
    return `Here is your ${base} ice cream with ${topping}!`;
  }
}

const vanillaSundae = makeSundae("vanilla");
const chocolateSundae = makeSundae("chocolate");

console.log(chocolateSundae("hot fudge"));
console.log(vanillaSundae("chocolate syrup"));

What we have done with makeSundae is called currying in functional programming.

Currying is converting a function that takes \(n\) arguments to return a chain of functions that each take one argument.

Here is a more useful example

function makeLog(base) {
  return function(value) {
    return Math.log(value) / Math.log(base);
  }
}

const log10 = makeLog(10);
const log2 = makeLog(2);

console.log(log10(100));
console.log(log2(8));

Notice the returned inner function retains access to the variables defined in the parent function even after the parent is out of scope. (Since the parent function has returned, our expectation demands that its local variables no longer exist. But, they do still exist!) This, in functional programming, is called closure.

Closure is when a function remembers and continues to access variables from outside of its scope.

function counter(step = 1) {
  let count = 0;
  return function increaseCount () {
    count += step;
    return count;
  }
}

const incBy1 = counter();
const incBy10 = counter(10);


console.log(incBy1());
console.log(incBy1());

console.log(incBy10());
console.log(incBy10());

For more examples, refer the examples on MDN web docs, Closure.

If interested, freeCodeCamp has a good article "JavaScript Closures Explained by Mailing a Package".

Mimic OOP

Closures let you save state. We can use closure with JavaScript objects to mimic objects in Object-Oriented programming (encapsulation: private state + behavior). Here is an example:

function createAccount(initialBalance = 0) {
  let balance = initialBalance;
  return {
    deposit: function(amount) { balance += amount; },
    withdraw: function(amount){ balance -= amount; },
    getBalance: function() { return balance; }
  }
}

// Create any number of accounts
const checking = createAccount();
const saving = createAccount(1000);

saving.withdraw(200);
checking.deposit(200);

console.log(checking.getBalance());
console.log(saving.getBalance());

Modern JavaScript has Classes (to create objects) similar to Java and C++. JavaScript Classes are in fact functions. The process shown above is the underlying mechanics used by JavaScript to mimic Object-Oriented Programming.

Functions are Objects

The typeof operator declares functions as a type.

function add (x, y) {
  return x + y;
}

console.log(typeof add)

However, functions are in fact objects (well, they are special Function objects that you can invoke). That is where the "functions are values" comes from. The fact that functions are objects enable you to treat them like other values (store them in variables, pass them to functions, etc).

You can go so far as to create a function using an object constructor notation!

const multiply = new Function("x", "y", "return x * y;");

console.log(typeof multiply);
console.log(multiply(2, 3));

Functions, like other built-in objects, have instance properties and methods!

function add (x, y) {
  return x + y;
}

console.log(add.name);
console.log(add.length); // number of parameters
console.log(add.toString());

Methods

Since functions are values, we can assign them to object property names. This is the closest thing to "methods" in Object-Oriented languages that JavaScript offers. You can even use the this keyword to access other object properties:

const account = {
  balance: 100,
  deposit: function(amount) { 
    this.balance += amount; 
  }, 
  withdraw: function(amount) { 
    this.balance -= amount; 
  }
}

account.deposit(50);
account.withdraw(20);
console.log(account.balance);

The difference between account object and createAccount function from the earlier section on closure is that balance is "publicly" accessible in account object. In createAccount function, we used closure and lexical scoping to hide balance.

Yet another function syntax

Starting with ECMAScript 2015, a syntactic sugar can be used to declare "methods" which looks more like method syntax in Object-Oriented programming languages.

const account = {
  balance: 100,

  deposit(amount) { 
    this.balance += amount; 
  },

  withdraw(amount) { 
    this.balance -= amount; 
  }
}

account.deposit(50);
account.withdraw(20);
console.log(account.balance);

this and Arrow Functions

Arrow functions does not have their own bindings to this and should not be used as methods.

const account = {
  balance: 100,
  deposit: (amount) => { this.balance += amount; }, 
  withdraw: (amount) => { this.balance -= amount; }
}

account.deposit(50);
account.withdraw(20);
console.log(account.balance);

Function Context

Each function has an execution context which is exposed to the function via this keyword. You can use Function Instance methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) call, apply and bind to provide the execution context.

const account = {
  balance: 100,
}

function displayBalance () { 
  console.log(this.balance); 
}

displayBalance();
displayBalance.bind(account)();

The bind() method creates a new function that, when called, has its this keyword set to the provided value.

function greet() {
  console.log(`Welcome to ${this.name}`);
}

const cs280 = { name: "Full-Stack JavaScript" };
const cs226 = { name: "Data Structures" };

const greet280 = greet.bind(cs280);
const greet226 = greet.bind(cs226);

greet280();
greet226();

The calls() method calls a function and sets its this to the provided value. If the function takes in arguments, those arguments can be passed (in order) following the this argument.

const account = {
  balance: 100,
}

function deposit (amount) { 
  this.balance += amount; 
}

function withdraw (amount) { 
  this.balance -= amount; 
}

deposit.call(account, 50);
withdraw.call(account, 20);

console.log(account.balance);

The apply() method calls a function with a given this value, and the function arguments are provided as an array (or an array-like object).

const stats = {
  max: Number.MIN_SAFE_INTEGER,
  min: Number.MAX_SAFE_INTEGER
}

function max(...args) { 
  for(let i = 0; i < args.length; i++) {
    if (this.max < args[i]) {
      this.max = args[i];
    }
  }
}

function min(...args) { 
  for(let i = 0; i < args.length; i++) {
    if (this.min > args[i]) {
      this.min = args[i];
    }
  }
}

const numbers = [5, 6, 2, 3, 7];
max.apply(stats, numbers);
min.call(stats, ...numbers);
console.log(stats);

Arrow functions are not suitable for call, apply and bind methods, which generally rely on establishing a scope.

Exercise

Implement a function, pick, that takes in a JavaScript object and zero or more property names. It must return a copy of the given object that contains only the given property names.

const user = {
  name: "Ali",
  lastname: "Madooei",
  affiliation: "JHU",
  username: "amadooe1"
};

// TODO Implement the pick function




const result = pick(user, "name", "lastname");
console.log(result); // -> {"name":"Ali","lastname":"Madooei"}
Solution
function pick(object, ...keys) {
  let result = {};

  for (let i = 0; i < keys.length; i++) {
    result[keys[i]] = object[keys[i]];
  }

  return result;
}

Here is another version which uses function expression and it is (technically) a single statement!

const pick = (object, ...keys) => Object.keys(object)
    .filter(key => keys.includes(key))
    .reduce((result, key) => ({...result, [key]:object[key]}), {});

Brick Breaker

We are going to develop a Game called Brick Breaker based on a tutorial on MDN website. Unlike the original tutorial, we will take an Object-Oriented approach to the design and implementation of the game.

You will find a demo of the original tutorial here. Feel free to play the game and inspect the source code.

This module serves as an exposition to OOP in JavaScript. It also introduces you to the HTML Canvas element which is the backbone of Web-based (JavaScript based) Animations and Games.

We will not be able to finish this module in one lecture. We'll cover the initial steps and the remaining ones will be left to you as an exercise.

A Logistical Issue!

In this tutorial, we will link our JavaScript classes using the syntax of ES6 modules. Linking to ES6 modules in the browser, while you open the we app locally, is blocked by CORS policy. There are several ways to get around this issue. The one which I recommend, for now, is running your web app in a "local host".

If you are using VSCode, please install live server plugin and open your index.html file using the live server.

Almost every editor and IDE has a similar plugin, and in some cases like JetBrain's Webstorm IDE, the feature is built into the application

Step 1

Clone the starter code

git clone -b starter https://github.com/cs280fall20/brick-breaker.git

The starter code contains a minimal scaffolding to get us started.

The entry point for this tutorial is to animate an object (make it move around) in an HTML page. There are several different ways to achieve this. In its simplest form, we can create an HTML element and set its position with absolute values. Then, we can use JavaScript (or even just CSS) to update the position over the time. Therefore, effectively, the element will move around the HTML page and appears as being animated.

A better approach is to use HTML Canvas. You will find the following line in the index.html file (part of the starter package).

<canvas id="myCanvas" width="480" height="320"></canvas>

A <canvas> is an HTML element which can be used to draw graphics via scripting (usually JavaScript). It was first introduced in WebKit by Apple for the OS X Dashboard, <canvas> has since been implemented in browsers. Today, all major browsers support it.

We will be using the <canvas> at a rudimentary level. There is a lot that can be done with it. You will find a detailed tutorial and documentation for Canvas API on MDN wed docs.

Step 2

Let's draw on the canvas! Copy the following code to script.js and load the index.html file in your favorite browser (Chrome is recommended) to see the outcome.

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

ctx.beginPath();
ctx.rect(20, 40, 50, 50); // x, y, width, height
ctx.fillStyle = "#FF0000";
ctx.fill();
ctx.closePath();

The fist two lines gets hold of the <canvas> element and creates the ctx variable to store the 2D rendering context — the actual tool we can use to paint on the Canvas.

The instructions that followed draw a rectangle on canvas with the specified coordinates and fill color.

The canvas is a coordinate space where origin is the top left corner.

Step 3

Let's make animation! Add the following snippet to script.js file:

function draw() {
  // drawing code
}
setInterval(draw, 10);

The setInterval is a function built into JavaScript. The code above calls the draw() function every 10 milliseconds (forever, or until we stop it).

Aside: requestAnimationFrame

An alternative approach to running the draw method is using the window.requestAnimationFrame. This methods tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. Here is how it will look like:

function draw() {
  // drawing code

  window.requestAnimationFrame(draw);
}

draw();

The requestAnimationFrame is a more common approach to work with canvas for animations and games. To learn more about it, please refer to MDN web docs tutorial on Animation Basics.

Add the following to the draw function:

ctx.beginPath();
ctx.rect(canvas.width / 2, canvas.height - 30, 10, 10);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();

You won't notice the rectangles being repainted constantly at the moment, as they're not moving. We can make them move by changing their coordinates.

Let's refactor the code and introduce the following variables outside of the draw function:

let x = canvas.width / 2;
let y = canvas.height - 30;
const dx = 2;
const dy = -2;

Update the command to draw a rectangle inside the draw function:

ctx.rect(x, y, 10, 10);

Finally, update the values of x and y coordinates inside the draw function:

x += dx;
y += dy;

Save your code again and try it in your browser. This works ok, although it appears that the moving object is leaving a trail behind it:

The apparent trail is there because we're painting a new rectangle on every frame without removing the previous one. Put the following statement as the first line inside the draw` method to take care of this issue.

ctx.clearRect(0, 0, canvas.width, canvas.height);

To slow down the movement and better observe it, feel free to update the time interval (set it to 100 milliseconds for instance). Notice the moving object eventually exits the canvas! We will take care of this issue later.

Step 4

In Computer Graphics, Animation, and Game Development, there are two kinds of objects: blocks and sprites.

Sprite is a computer graphic which may be moved on-screen and otherwise manipulated as a single entity.

A block on the other hand does not move!

We will create an abstraction for each of these.

  • Create a folder model in the root of your project.
  • Create a Block.js and a Sprite.js file in the model folder.

We will start with the Block.js file. Add the following code to it:

class Block {
  constructor(x, y, width, height, color) {
    this.x = x;
    this.y = y;
    this.height = height;
    this.width = width;
    this.color = color;
  }

  draw(ctx) {
    ctx.beginPath();
    ctx.rect(this.x, this.y, this.width, this.height);
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.closePath();
  }
}

Block is a class 🎉 Notice the syntax:

  • We use the class keyword to declare it.
  • There is a special function called constructor which, not surprisingly, is the class constructor!
  • The attributes of the class are initialized in the constructor using the this keyword. Unlike Java/C++, you don't declare fields. The syntax here is similar to Python's class definition. (Python classes are constructed through a special method called __init__() which uses the self parameter-instead of this- to reference to the current instance of the class)
  • Methods are declared with the method declaration syntax (which was explored when we were talking about functions).
  • There are no commas between method definitions.

Step 5

Let's use our Block class to create objects.

Open the script.js file and add the following import statement at the very top of the file.

import Block from "./model/Block.js";

For this import statement to work, we need provisions:

  1. Update the Block.js file by appending the following export statement to the end of it.
    export default Block;
    
  2. The script tag in index.html must include a type attribute as follows. This was already done for you in the starter code.
    <script type="module" src="script.js"></script>
    

In script.js file, remove the following code:

ctx.beginPath();
ctx.rect(20, 40, 50, 50); // x, y, width, height
ctx.fillStyle = "#FF0000";
ctx.fill();
ctx.closePath();

and instead place this statement

const redBlock = new Block(20, 40, 50, 50, "#FF0000");

Notice the use of new keyword to create an instance of the Block class. This must be familiar to you coming from Java/C++.

Inside the draw function, after "clearing the canvas", add the following statement:

redBlock.draw(ctx);

Save your code again and try it in your browser.

Step 6

Let's implement the Sprite class:

import Block from "./Block.js";

class Sprite extends Block {
  constructor(x, y, width, height, color) {
    super(x, y, width, height, color);
  }

  move(dx, dy) {
    this.x += dx;
    this.y += dy;
  }
}

export default Sprite;

Notice the use of extends keyword to use Inheritance. Also take note of the use of the keyword super to invoke the constructor of the parent class.

The constructor of Sprite is not needed; if we eliminate it, the constructor of the parent class is inherited. This is different from e.g. Java where you must explicitly declare (non-default) constructors.

Step 7

Let's use our Sprite class to create objects.

Open the script.js file and add the following import statement at the very top of the file.

import Sprite from "./model/Sprite.js";

In script.js file, remove the following code:

let x = canvas.width / 2;
let y = canvas.height - 30;
const dx = 2;
const dy = -2;

and instead place this statement

const blueSprite = new Sprite(
  canvas.width / 2,
  canvas.height - 30,
  10,
  10,
  "#0095DD"
);

Inside the draw function, remove the following statement:

ctx.beginPath();
ctx.rect(x, y, 10, 10);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
x += dx;
y += dy;

and instead place this statements:

blueSprite.draw(ctx);
blueSprite.move(2, -2);

Save your code again and try it in your browser.

Step 8

Let's make a Ball object that bounces off the edges of canvas!

Create a new file model/Ball.js with the following content:

import Sprite from "./Sprite.js";

class Ball extends Sprite {
  constructor(x, y, width, height, color) {
    super(x, y, width, height, color);
  }
  
  bounce(canvasWidth, canvasHeight) {

  }
}

export default Ball;

Update the bouce method:

bounce(canvasWidth, canvasHeight) {
  if (this.x < 0) {
    // bounce off the left edge
  } else if (this.x > canvasWidth) {
    // bounce off the right edge
  }

  if (this.y < 0) {
    // bounce off the top edge
  } else if (this.y > canvasHeight) {
    // bounce off the bottom edge
  }
}

To bounce off the edges of the canvas, we need to change the speed vector of the object.

bounce(canvasWidth, canvasHeight) {
  if (this.x < 0) {
    // bounce off the left edge
    this.dx *= -1; // switch direction
  } else if (this.x > canvasWidth) {
    // bounce off the right edge
    this.dx *= -1;
  }

  if (this.y < 0) {
    // bounce off the top edge
    this.dy *= -1; // switch direction
  } else if (this.y > canvasHeight) {
    // bounce off the bottom edge
    this.dy *= -1;
  }
}

We need to store the speed of the ball and allow it to be changed. (I'm using the term speed in a loose sense.)

class Ball extends Sprite {
  constructor(x, y, width, height, color) {
    super(x, y, width, height, color);
    this.dx = 0;
    this.dy = 0;
  }

  updateSpeed(dx, dy) {
    this.dx = dx;
    this.dy = dy;
  }
  
  move(canvasWidth, canvasHeight) { /* overriding the move method */
    super.move(this.dx, this.dy);
    this.bounce(canvasWidth, canvasHeight);
  }

  // bounce method not shown to save space!
}

Open script.js and get rid off redBlock and blueSprite! Then, add a ball object:

import Ball from "./model/Ball.js";

const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

const ball = new Ball(canvas.width / 2, canvas.height - 30, 10, 10, "#0095DD");

ball.updateSpeed(2, -2);

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ball.draw(ctx);
  ball.move(canvas.width, canvas.height);
}

setInterval(draw, 10);

Save your code again and try it in your browser.

Step 9

Let's refactor tune Ball.bounce:

bounce(canvasWidth, canvasHeight) {
  if (this.x < 0 || this.x > canvasWidth) {
    // bounce off the left/right edges
    this.dx *= -1; // switch direction
  } 

  if (this.y < 0 || this.y > canvasHeight) {
    // bounce off the top/bottom edge
    this.dy *= -1; // switch direction
  } 
}

Notice when the ball hits each edge it sinks into it slightly before changing direction. This is because we're calculating the collision point of the edge and the top left corner of the ball (Sprite). So, let's refactor!

bounce(canvasWidth, canvasHeight) {
  if (this.x < 0 || this.x + this.width > canvasWidth) {
    // bounce off the left/right edges
    this.dx *= -1; // switch direction
  } 

  if (this.y < 0 || this.y + this.height > canvasHeight) {
    // bounce off the top/bottom edge
    this.dy *= -1; // switch direction
  } 
}

Save your code again and try it in your browser.

Step 10

Let's build a paddle so we can hit the ball with! First, however, we'll make it so we can move it around with the arrow- keys on the keyboard.

Create a file model/Paddle.js which the following content:

import Sprite from "./Sprite.js";

class Paddle extends Sprite {
  constructor(x, y, width, height, color) {
    super(x, y, width, height, color);
    this.dx = 0;
    document.addEventListener("keydown", this.keyDownHandler.bind(this));
    document.addEventListener("keyup", this.keyUpHandler.bind(this));
  }

  keyDownHandler(e) {
    if (e.key === "Right" || e.key === "ArrowRight") {
      this.dx = 7;
    } else if (e.key === "Left" || e.key === "ArrowLeft") {
      this.dx = -7;
    }
  }

  keyUpHandler(e) {
    if (e.key === "Right" || e.key === "ArrowRight") {
      this.dx = 0;
    } else if (e.key === "Left" || e.key === "ArrowLeft") {
      this.dx = 0;
    }
  }

  move() { /* overriding the move method */
    super.move(this.dx, 0);
  }
}

export default Paddle;

Note how I used event listeners to move the paddle around when a user presses the left/right arrow keys.

Update the script.js:

  • Import Paddle.js
    import Paddle from "./model/Paddle.js";
    
  • Create a Paddle
    const paddle = new Paddle(
      (canvas.width - 10) / 2,
      canvas.height - 10,
      75,
      10,
      "#0095DD"
    );
    
  • Draw and move it in the draw method:
    paddle.draw(ctx);
    paddle.move();
    

Save your code again and try it in your browser.

Exercise: remove bind(this) inside addEventListener statements. Save your code again and try it in your browser. What happens and why?

Solution

The methods keyDownHandler and keyUpHandler make use of the this keyword to update the properties of Paddle. However, when they are called by addEventListener, their execution context is not the Paddle object anymore (it is the execution context of addEventListener). We need to explicitly bind their this keyword to the Paddle object when we pass them as argument to addEventListener.

This point is much harder to figure out on your own, had I not told you about it 😜.

Step 11

We must update the Paddle so it does not run off the sides of the canvas.

Add the following method to Paddle.js

handleBoundary(canvasWidth) {
  if (this.x < 0) {
    this.x = 0;
  } else if (this.x + this.width > canvasWidth) {
    this.x = canvasWidth - this.width;
  }
}

Update Paddle.move

move(canvasWidth) {
  super.move(this.dx, 0);
  this.handleBoundary(canvasWidth);
}

Update script.js where it calls paddle.move()

paddle.move(canvas.width);

Save your code again and try it in your browser.

Step 12

Let's implement a kind of collision detection between the ball and the paddle, so it can bounce off it and get back into the play area.

Collision detection is a very common task for game development. We will likely need this between other objects in the game. So, let's implement it at a higher level of abstraction. Add the following method to the Block.js.

// assume other has {x, y, width, height}
intersects(other) {
  let tw = this.width;
  let th = this.height;
  let rw = other.width;
  let rh = other.height;
  if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) {
    return false;
  }
  let tx = this.x;
  let ty = this.y;
  let rx = other.x;
  let ry = other.y;
  rw += rx;
  rh += ry;
  tw += tx;
  th += ty;
  //      overflow || intersect
  return (
    (rw < rx || rw > tx) &&
    (rh < ry || rh > ty) &&
    (tw < tx || tw > rx) &&
    (th < ty || th > ry)
  );
}

The method above detects if two rectangles intersect. This is a common strategy to collision detection (there are other strategies, see e.g. Collision Detection in Javascript or Collision Detection and Physics).

Now add the following method to Ball.js

colides(paddle) {
  if (this.intersects(paddle)) {
    this.dy *= -1; // switch direction
  }
}

Finally, update the script.js file by adding this line to the draw method:

ball.colides(paddle);

Save your code again and try it in your browser. You may not notice the ball is bouncing off the paddle as it also bounces off the bottom edge of the canvas. We are going to change that behavior in the next step though.

Step 13

Let's try to implement "game over" in our game.

We will adjust the behavior of the ball so it bounces off the three sides of the canvas but not the bottom edge. When the ball hits the bottom edge, we will end the game!

Open the Ball.js file and add this declaration before the class declaration:

const gameOver = new CustomEvent("gameover");

Update the Ball.bounce method to dispatch the gameOver event when the ball hits the bottom side of the canvas:

bounce(canvasWidth, canvasHeight) {
  if (this.x < 0 || this.x + this.width > canvasWidth) {
    // bounce off the left/right edges
    this.dx *= -1; // switch direction
  }

  if (this.y < 0) {
    // bounce off the top edge
    this.dy *= -1; // switch direction
  } else if (this.y + this.height > canvasHeight) {
    // dispatch Game Over signal!
    document.dispatchEvent(gameOver);
  }
}

Open script.js and add an event listener for the custom "game over" event we have created earlier!

document.addEventListener("gameover", (e) => {
  window.alert("Game Over!");
});

We can also stop the animation by clearing the setInterval function. For that, you need to store the interval ID retuned by the setInterval function first:

const interval = setInterval(draw, 10);

Then update the event listener for gameover:

document.addEventListener("gameover", (_e) => {
  clearInterval(interval);
  window.alert("Game Over!");
});

Save your code again and try it in your browser.

Step 14

The final piece for this game is to add several layers of bricks. When the ball hits the bricks, it breaks them. The game is about breaking all the bricks without letting the ball drop on the ground (hit the bottom of the canvas)!

Create a new file model/Brick.js with the following content:

import Block from "./Block.js";

class Brick extends Block {
  constructor(x, y, width, height, color) {
    super(x, y, width, height, color);
    this.visible = true;
  }

  draw(ctx) {
    if (this.visible) {
      super.draw(ctx);
    }
  }

  colides(ball) {
    if (this.visible && this.intersects(ball)) {
      this.visible = false;
      ball.colides(this); // causes the ball to bounce off
    }
  }
}

export default Brick;

Note how I have used a boolean flag (this.visible) to make a brick disappear when it is collided with a ball.

In the next step, we will add several bricks to the canvas.

Step 15

Add the following to the script.js somewhere before the draw function.

const brickRowCount = 3;
const brickColumnCount = 5;
const bricks = [];
const brickWidth = 75;
const brickHeight = 20;
const brickPadding = 10;
const brickOffsetTop = 30;
const brickOffsetLeft = 30;

for (let c = 0; c < brickColumnCount; c++) {
  for (let r = 0; r < brickRowCount; r++) {
    let brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
    let brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
    bricks.push(new Brick(brickX, brickY, brickWidth, brickHeight, "#0095DD"));
  }
}

Don't forget to import the Brick class:

import Brick from "./model/Brick.js";

Finally, add the following to the draw function:

bricks.forEach((brick) => {
  brick.draw(ctx);
  brick.colides(ball);
});

Save your code again and try it in your browser.

Step 16

We are technically done with this game. Let's count score and display it.

Open script.js and declare a score variable outside of the draw function:

let score = 0;

Add the following three lines somewhere inside the draw function to display the score:

ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText("Score: " + score, 8, 20);

Finally, we must increment the score when a brick is hit. Update the Brick.colide method to return a boolean!

colides(ball) {
  if (this.visible && this.intersects(ball)) {
    this.visible = false;
    ball.colides(this); // causes the ball to bounce off
    return true;
  }
  return false;
}

Now update the draw method where the brick.collide is called:

bricks.forEach((brick) => {
  brick.draw(ctx);
  score += brick.colides(ball);
});

Save your code again and try it in your browser.

Step 17

You can add some final touches to this game. For instance, you can randomize the initial position of the ball and its movement direction. You can keep score and when all bricks are broken, show a message indicating user won the game. You can add difficulty levels by e.g. arranging bricks in a more sparse fashion, etc. I will leave that to you!

Here is the complete application: GitHub.

Classes

Classes are the primary construct which we organize our code in Object-Oriented Programming (OOP) languages. In this module we will overview how classes work in JavaScript.

JavaScript introduced the class keyword in 2015 with the release of ES6. Class syntax unifies constructor function and prototype methods.

We will focus on the modern JavaScript and ignore its traditional ways of mimicking OOP. An interested reader may refer to the following resources for more information:

Class Syntax

We use the class construct to define a type and create encapsulation.

class Note {
  constructor(content) {
    this.text = content;
  }

  print() {
    console.log(this.text);
  }
}
  • A class is declared using the class keyword.
  • By convention, the class name is capitalized.
  • A class has a special constructor method that is named constructor.
  • Think of the constructor the same way you thought of them in Java/C++. That is, use them to initialize the state (attributes) of your objects.
  • Unlike Java/C++, in JavaScript, a class can only have one constructor.
  • Methods can be declared using a syntax that resembles methods in Java/C++.
  • Inside a class declaration, there are no commas between method definitions.
  • Aside: Classes are not hoisted.

A class encapsulates state (data) and behavior (operations). In the example of Page, the data is a string of text stored in a this.text member property. The behavior is print(), a method that dumps the text to the console.

Instantiate Classes

Here is the Note class again:

class Note {
  constructor(content) {
    this.text = content;
  }

  print() {
    console.log(this.text);
  }
}

const myNote = new Note("This is my first note!");

console.log(typeof Note);
console.log(typeof myNote);
console.log(myNote instanceof Note);
console.log(myNote);

Notice the typeof Note specifies that Note is a function! JavaScript's class is a syntactic sugar; under the hood it is a well crafted function that generates objects for us. To create objects that you can use in the program, a class must be instantiated (with the new keyword) one or more times.

Accessing properties, Invoking behaviors

Here is the Note class again:

class Note {
  constructor(content) {
    this.text = content;
  }

  print() {
    console.log(this.text);
  }
}

const myNote = new Note("This is my first note!");

console.log(myNote.text); // myNote["text"] works too!

myNote.print();

You must use the (familiar) dot notation to access a property or invoke a method on an object instantiated off a class.

The this keyword

You must use the keyword this inside a class to define/access properties (except for defining methods).

class Publication {
  setTitle(title) {
    this.title = title;
  }

  setAuthor(author) {
    this.author = author;
  }

  getTitle() {
    return this.title;
  }

  getAuthor() {
    return this.author;
  }

  print() {
    console.log(this.getTitle() + " by " + this.getAuthor());
  }
}

const publication = new Publication();
publication.setTitle("CS280 Notes");
publication.setAuthor("Ali Madooei");
publication.print();

Unlike Java/C++, you don't declare fields. And, you are not bounded to declare the attributes inside the constructor. However, for improved readability, I suggest that you always initialize your class attribute inside the constructor.

The JavaScript class syntax is more similar to php/python's class definition. (Python classes, for instance, are constructed through a special method called __init__() which uses the self parameter-instead of this- to reference to the current instance of the class)

The this keyword, again!

Be careful when you use this in nested functions!

class MyClass {
  constructor() {
    this.value = 10;
  }

  myMethod1() {
    return function () {
      console.log(this);
    };
  }

  myMethod2() {
    return () => {
      console.log(this);
    };
  }
}

const obj = new MyClass();
obj.myMethod1()();
obj.myMethod2()();

Information Hiding

Let's reiterate that, at the time of writing, JavaScript class syntax does not allow for declaring fields. It is, however, being proposed to do it using a syntax like this:

class Account {
  balance = 0;

  deposit(amount) { 
    this.balance += amount 
  }
}
Transpilers

There are applications (called transpiler), like Babel that allow you write your source code using proposed sysntaxes (such as the one above). You can then transpile your source code to the standard JavaScript. When we use React to build front-end application, it comes packed in with one of these transpilers.

There is no notion of visibility modifier in JavaScript class syntax. You cannot make a property (attribute/method) private. There are, however, ways to mimic this behavior which typically involves clever use of scopes such as closures.

class Account {
  constructor(balance){
    this.getBalance = function() {
      return balance
    }

    this.deposit = function (amount) { 
      balance += amount 
    }
  }
}

const checking = new Account(100);
checking.deposit(20);

console.log(checking.getBalance());
console.log(checking.balance);

There is also a proposal to declare private fields in (future) JavaScript as follows:

class BankAccount {
  #balance = 0;

  deposit(amount) { 
    this.#balance += amount 
  }
}

You may find some programmers employ a convention similar to that in Python which "signals" a property is private by prefixing its name with an underscore.

class BankAccount {
  constructor(balance){
    this._balance = balance;
  }

  deposit(amount) { 
    this._balance += amount 
  }
}

This doesn't protect the property in any way; it merely signals to the outside: "You don't need to know about this property."

Setters & Getters

JavaScript has a special syntax for making getter methods

class Person {
  constructor(first, last) { 
    this.first = first; 
    this.last = last;
  }

  get fullName() { 
    return `${this.first} ${this.last}`;
  }
}

const teacher = new Person('Ali', 'Madooei');

console.log(teacher.fullName); 
  • A getter method is a method with no parameters, declared with keyword get.
  • It can also be used in object literal.
  • Call getters without parentheses!
  • Think of a getter as a dynamically computed property.

Likewise, there is a special syntax for setter methods.

class Person {
  get fullName() { 
    return `${this.first} ${this.last}`;
  }

  set fullName(value) {
    const parts = value.split(" ");
    this.first = parts[0];
    this.last = parts[1];
  }
}

const teacher = new Person();
teacher.fullname = "Ali Madooei";

console.log(teacher.fullname);
  • A setter method is a method with one parameter, declared with keyword set.
  • It can also be used in object literal.
  • Setters are invoked through assignment to property.

Getters/setters look like fields, act like methods.

I don't see a great value in having a special setter and getter methods. In particular since properties can not be made private, it seems redundant to use setters/getters.

Static Fields and Methods

It is possible to create static fields and methods in JavaScript classes.

class Math {
  static PI = 3.14;

  static max(...args) { 
    let max = Number.MIN_SAFE_INTEGER;
    args.forEach( arg => max = max < arg ? arg : max );
    return max;
  }
}

console.log(Math.PI);
console.log(Math.max(3, 0, 5));

Static fields/method in JavaScript, like in Java/C++, are class members and not instance members. So, you access/invoke them using the class name (not the instantiated object).

A class is technically a function! A function is technically an object, so a class is an object!! You can add value/function properties outside of class definition. Those will be added as static members.

class Person {
  constructor(name) {
    this.name = name;
  }
}

Person.genus = "homosapien";

const person = new Person("Ali");

console.log(person);
console.log(Person.genus)

Inheritance

JavaScript now supports inheritance with a syntax that is very similar to that in Java/C++.

class Student {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

class GradStudent extends Student {
  constructor(name, email, advisor) {
    super(name, email);
    this.advisor = advisor;
  }
}

const john = new Student ("John Doe", "john@email.com");
const jane = new GradStudent ("Jane Doe", "jane@email.com", "Prof. Smith");

console.log(john instanceof Student);
console.log(jane instanceof Student);
console.log(john instanceof GradStudent);
console.log(jane instanceof GradStudent);

console.log(john);
console.log(jane);
  • Notice the use of extends keyword to declare GradStudent is a subclass of Student.
  • Also note the use of the keyword super to invoke the constructor of the parent class.
  • The super(..) delegates to the parent class’s constructor for its initialization work.
  • Unlike in Java/C++, constructors are also inherited in JavaScript. You don't need to explicitly define one if all it does is a super(..) call to the parent constructor.

Inheritance is a powerful tool for creating type hierarchies and reusing code (sharing between parent and subclasses).

Polymorphism

You can override an inherited method which opens the door to polymorphism.

class CourseAssistant {
  getBaseSalary() {
    return 500.0; //dollars
  }

  getHourlyPayRate() {
    return 15.0; // dollars
  }
}

class ExperiencedCourseAssistant extends CourseAssistant {
  /* overrides */
  getHourlyPayRate() {
    return 1.1 * super.getHourlyPayRate();
  }
}

function calcPayment(courseAssistant, hoursWorked) {
  let wages =
    courseAssistant.getBaseSalary() +
    hoursWorked * courseAssistant.getHourlyPayRate(); /* dynamic dispatch */
  console.log(wages);
}

const tom = new CourseAssistant();
const mona = new ExperiencedCourseAssistant();

calcPayment(tom, 10);
calcPayment(mona, 10);

In the example above, getHourlyPayRate() is dispatched based on the actual "type" of the courseAssistant argument. It will decide on dispatching the overloaded getHourlyPayRate() during runtime which is a (dynamic) polymorphic behavior.

Method overriding in JavaScript simply means that you can have methods with the same name in parent and subclasses.

Aside: JavaScript does not method overloading since the number of arguments to a method can be fewer/more than declared parameters.

Class Expression

You can use class declaration in an expression!

const Student = class Person {
  constructor(name) {
    this.name = name;
  }
}

const tom = new Student("Tom");
console.log(typeof Student);
console.log(tom instanceof Student);
console.log(tom);

You will not be able to use Person as a class (constructor function). You must use Student, instead. In fact, in a class expression, the class name can be omitted to create anonymous class.

const Student = class {
  constructor(name) {
    this.name = name;
  }
}

const tom = new Student("Tom");
console.log(typeof Student);
console.log(tom instanceof Student);
console.log(tom);

Class Search using the SIS API

In this module, we will build a simplified version of John's Hopkins Class Search web application which makes use of the SIS API.

In order to follow along, you need to:

  1. Go to https://sis.jhu.edu/api and register your email to obtain an API Access Key. Scroll to the bottom of the page to find the registration form.
  2. Download and install Postman.
  3. Install Chrome's Allow-Control-Allow-Origin plugin.

You can access the completed application at https://github.com/cs280fall20/course-search-sis-api.

Step 1

We'll start by creating an empty HTML project; create index.html, style.css, and script.js files.

Add the following to the index.html file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>SIS API Search</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <script src="script.js"></script>
  </body>
</html>

We'll add minimal styling to style.css:

body {
  margin: 2em;
  text-align: center;
  line-height: 1.5;
  font-size: 1.1em;
}

Step 2

Let's update the index.html with elements to allow a user:

  • select a school
  • select a term
  • enter a course name
  • search courses offered by the school for the selected term that match the provided course name.
<label for="schools">Choose a school:</label>
<select name="schools" id="schools">
  <option value="all">All</option>
</select>
<br />

<label for="terms">Choose a term:</label>
<select name="terms" id="terms">
  <option value="all">All</option>
</select>
<br />

<label for="query">Course name:</label>
<textarea id="query" name="query" rows="1" cols="20"></textarea>
<br />

<button id="searchBtn">Search</button>
<br />
<br />

<div class="result"></div>

Your web page should look like this now:

We will use the SIS API to populate the options for "school" and "term". We will use the SIS API again to search for a course, given a user's selections/entry for "course name".

Step 3

Before working with the SIS API, let's put in place the mechanics of working with SIS data.

It is often the case that software APIs document their data objects among other things so you can plan on consuming their data before connecting to the API.

Add the following objects to script.js

// sample schools
schools = [
  {
    Name: "School of Medicine",
  },
  {
    Name: "Whiting School of Engineering",
  },
];

// sample terms
terms = [
  {
    Name: "Fall 2020",
  },
  {
    Name: "Summer 2020",
  },
  {
    Name: "Spring 2020",
  },
];

// sample courses
// (SIS returns provides several other properties for a course)
courses = [
  {
    OfferingName: "EN.601.226",
    SectionName: "01",
    Title: "Data Structures",
    Instructors: "A. Madooei",
  },
  {
    OfferingName: "EN.601.226",
    SectionName: "02",
    Title: "Data Structures",
    Instructors: "A. Madooei",
  },
  {
    OfferingName: "EN.601.280",
    SectionName: "01",
    Title: "Full-Stack JavaScript",
    Instructors: "A. Madooei",
  },
  {
    OfferingName: "EN.601.280",
    SectionName: "02",
    Title: "Full-Stack JavaScript",
    Instructors: "A. Madooei",
  },
];

The sample data more or less resembles the data returned by the SIS API. I know that because I worked with the API but the data attributes for a course object for instance is (somewhat) documented here https://sis.jhu.edu/api too.

Step 4

Let's populate the corresponding drop-down selectors with "schools" and "terms" data.

Add the following to script.js

function populateSelector(selectElmId, data) {
  const select = document.getElementById(selectElmId);
  select.innerHTML = "";
  let item = null;
  for (let i = 0; i < data.length; i++) {
    item = document.createElement("option");
    item.value = data[i]["Name"];
    item.innerText = data[i]["Name"];
    select.appendChild(item);
  }
}

populateSelector("schools", schools);
populateSelector("terms", terms);

The populateSelector function gets a corresponding selector element (given using it id attribute) and populates its options based on the given data object. Note how for each "option" element, I've set both the value attribute and their inner text to the data Name property.

Save your files and check the webpage in your favorite browser. The drop down elements corresponding to "Choose a school" and "Choose a term" must now have the schools and terms data.

Step 5

Let's put in place the process for conducting a search.

Add the following to script.js

function search() {
  const query = document.getElementById("query").value.trim();
  const school = document.getElementById("schools").value;
  const term = document.getElementById("terms").value;

  console.log(`search for ${query} in the ${school} during ${term}`);
}

document.getElementById("searchBtn").addEventListener("click", search);

The search function gets the data it needs (namely the selected "school", "term", and search query) and as of now it simply prints them out to the console. We will later update this function to send a request to SIS API and return its response.

Notice the search function is called when the search button is clicked.

Step 6

Let's put in place a process to display search results. For now, since the search function is not really performing a search, we will simply display the entire courses data object.

Add the following function to script.js

function showSearchResults(data) {
  const resultDiv = document.querySelector(".result");
  resultDiv.innerHTML = "";
  const list = document.createElement("ul");
  for (let i = 0; i < data.length; i++) {
    const item = document.createElement("li");
    item.innerText = `${data[i]["OfferingName"]} (${data[i]["SectionName"]}) ${data[i]["Title"]}`;
    list.append(item);
  }
  resultDiv.append(list);
}

Let's have the search method call showSearchResults:

function search() {
  const query = document.getElementById("query").value.trim();
  const school = document.getElementById("schools").value;
  const term = document.getElementById("terms").value;

  console.log(`search for ${query} in the ${school} during ${term}`);
+ showSearchResults(courses);
 }

As you run the application and perform a search, the UI should look like this:

Step 7

We've got all the bits and pieces in place with the exception of the main ingredient: connecting to and using the SIS API. There is, additionally, an issue with the presentation of the app: it looks like the Internet in its early years!

It will not be difficult to make the app visually more appealing. I, however, want to take this as an opportunity to tell you about CSS libraries. There are several libraries out there which you can use, for free, by including a few fils to your project. These libraries will make it easy for you to design slick and professional looking applications.

Here are some of the popular CSS libraries:

I will use a library called bootswatch which is based off bootstrap. There are several ready made themes; you can select one that you like and download the CSS file for it.

I downloaded the CSS file for the Litera theme: bootstrap.css and added the file to the project directory.

You must now update the link to the CSS file from inside the index.html:

-    <link rel="stylesheet" href="style.css" />
+    <link rel="stylesheet" href="bootstrap.css" />

To have the styling attributes affect the html elements, we must assign class attributes corresponding to the CSS selectors to our elements. One approach to figure out what class attributes can be used is to inspect the "preview" page for the Litera theme. For example, I learned by inspecting, that I must add the following class attribute to the search button to make it look slick!

-    <button id="searchBtn">Search</button>
+    <button class="btn btn-primary" id="searchBtn">Search</button>

Step 8

After careful inspection of the "preview" page for the Litera theme, I've updated the index.html file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>SIS API Search</title>
    <link rel="stylesheet" href="bootstrap.css" />
  </head>
  <body>
    <div class="container">
      <h2>Search JHU SIS API</h2>
      <br />

      <label for="schools">Choose a school:</label>
      <select class="form-control" name="schools" id="schools">
        <option value="all">All</option>
      </select>
      <br />

      <label for="terms">Choose a term:</label>
      <select class="form-control" name="terms" id="terms">
        <option value="all">All</option>
      </select>
      <br />

      <label for="query">Course name or id:</label>
      <textarea
        class="form-control"
        id="query"
        name="query"
        rows="1"
        cols="50"
      ></textarea>
      <br />

      <button class="btn btn-primary" id="searchBtn">Search</button>
      <br />
      <br />

      <div class="result"></div>
    </div>
    <script src="script.js"></script>
  </body>
</html>

Notice how the entire content in wrapped inside a <div class="container"></div> element.

Moreover, when we show the search results, we must add the css selectors as class attributes to the generated html elements, so update the showSearchResults:

function showSearchResults(data) {
  const resultDiv = document.querySelector(".result");
  resultDiv.innerHTML = "";
  const list = document.createElement("ul");
+ list.className = "list-group";
  for (let i = 0; i < data.length; i++) {
    const item = document.createElement("li");
+   item.className = "list-group-item";
    item.innerText = `${data[i]["OfferingName"]} (${data[i]["SectionName"]}) ${data[i]["Title"]}`;
    list.append(item);
  }
  resultDiv.append(list);
}

Here is what the outcome should look like:

API

It's time to work with the SIS API but first, we must talk about what we mean by "API".

An application programming interface (API) generally refers to a software intermediary that allows two applications to talk to each other.

We are using the term API in the context of a client-server software architecture.

Client-Server Application

A client-server is a popular software design architecture which, at an abstract level, breaks down a software into two parts: client-side and server-side.

  • The client-side (or simply, client) is the application that runs on the end-user computer; it provides a user-interface (UI) that handles what the application feels and looks like and how it interacts with end-user. It may employ and consume resources on the user's machine (computing device) such as temporary and local storage, etc.

  • The server-side (or simply, server) is the application that receives requests from the clients, and contains the logic to send the appropriate data back to the client. Instead of user-interface, the server usually has an application programming interface (API). Moreover, the server often includes a database, which will persistently store all of the data for the application.

As long as your software application adheres to the client-server architecture (i.e. a client can send and receive data to an API on a server) you are free to build whatever user interface you want on whatever platform you want. This is advantageous as modern software application are expected to be available across multiple platforms and provide a consistent experience across devices.

Let’s make all of this a bit more concrete, by following an example of the main steps that happen when you access your grades on JHU's Student Information System (SIS).

SIS

Student Information System (SIS) is Johns Hopkins University's web-based system of record for student records and registration, billing, and course data.

We will use SIS as an example of a client-server application.

An example of Client-Server Application

  1. You visit sis.jhu.edu using an internet browser like Chrome on any device that provides internet browsing.

  2. Following your visit, a web-page will be displayed in your browser. This web-page is the user-interface of SIS and it constitutes the "client" side of the SIS application. The client application allows you to interact with the SIS server (which, for the intent of this example, may be a physical computer at one of JHU's campuses).

  3. On the client-side, you enter your username and password to log into SIS. At this stage, the client application (web-page where you put your username and password) will send (an authentication) request to the (SIS) server.

  4. Your (authentication) request travels across the internet to the SIS server. The server, which is actively listening for requests from all users, receives your request and triggers a response.

  5. The response process on the server makes a database query to check your login credentials. This database may contain other information about you such as your grades. The database query is executed, and the database sends the requested data back to the server.

  6. The server receives the data that it needs from the database, and it is now ready to construct and send its response back to the client (you). In this case, the response would be the privilege to access SIS (assuming your login credential were accredited).

  7. The response travels across the internet, back to your computer. Your browser receives the response and uses that information to create and render the view that you ultimately see after successfully logging in.

HTTP

Communication between Client and Server: As long as each side knows what format of messages to send to the other, they can communicate with one another. The messages send in between are generally categorized into request and response messages. And, the most common protocol for communication is HTTP.

What is HTTP?

HTTP stands for Hypertext Transfer Protocol and is used to structure requests and responses over the Internet.

There are 4 basic HTTP verbs (operation) we use in requests to interact with server:

  • GET — retrieve a specific resource (by id) or a collection of resources
  • POST — create a new resource
  • PUT — update a specific resource (by id)
  • DELETE — remove a specific resource by id

A HTTP request, in addition to an HTTP verb, typically consists of:

  • a header, which allows the client to pass along information about the request
  • a path to a resource
  • an optional message body containing data

What is API Endpoint?

HTTP requires data to be transferred from one point to another over the network. An API endpoint is the point of entry in a communication channel when client and server interacting.

SIS API

Part of SIS is made available as a public API.

The public API is primarily a self-service course search API. It returns, in JSON Format, a list of courses available through the Public Course Search https://sis.jhu.edu/classes.

The SIS API provides several endpoints and it can be used to filter courses based on school name, academic term, section number, etc. It can further be given a custom query using a set of query string parameters to search for courses.

The API documentation is available at https://sis.jhu.edu/api.

We will be using the following endpoints:

  • To get a list of all available schools
    https://sis.jhu.edu/api/classes/codes/schools?key=apikeyvalue
    
  • To get a list of available academic terms
    https://sis.jhu.edu/api/classes/codes/terms?key=apikeyvalue
    
  • Advanced Search
    https://sis.jhu.edu/api/classes?key=apikeyvalue&School=schoolvalue&Term=termvalue&CourseTitle=titlevalue`,
    

The apikeyvalue is an access key which you will obtain after registeration.

Access API Key

Only registered users are permitted to access Course Search API. The registration form is available at https://sis.jhu.edu/api. To register you must provide a valid email address.

Once you have registered, you will receive an email with the API Key. The Key is a string of characters like this one: (The one here is a fake one!)

pJnMRxKD1fFECXFRX1FDXykpLww0Lazk

SIS API & CORS Policy

The browser blocks our access to the SIS API due to CORS policy.

This is an issue on SIS API's side. They must update their responses to include Access-Control-Allow-Origin: * in their HTTP header to allow requests from all domains.

To get around this issue, we must tell the browser to not pick on it!

Install Chrome's Allow-Control-Allow-Origin plugin

Once installed, click it in your browser to activate the extension. Make sure the icon's label goes from "off" to "on".

Make sure to turn it off after you are done with this tutorial.

Resources:

Postman

Postman is an API Development Environment (ADE). It was originally design to facilitate performing HTTP requests. Since then it has matured into an industry standard for designing, documenting, testing, and interacting with APIs.

Download and install Postman.

We will demo the use of Postman in class but we cannot cover much due to our limited time. Please visit the Postman Learning Center, in particular, the section about sending API requests, for more information and guidelines.

Another good tutorial to get you up and running with Postman is guru99's postman-tutorial.

Fetch

The Fetch API provides a JavaScript interface for making HTTP request. In its simplest form, its syntax looks like this

fetch('/some/api/endpoint/')
  .then(response => response.json())
  .then(data => console.log(data))

Calling fetch() returns a promise. We can then wait for the promise to resolve. Once resolved, the response will be available with the then() method of the promise.

Using fetch() is an example of retrieving resources asynchronously across the network.

We will explore JavaScript's asynchronous behaviour in the next module.

Resources:

Step 9

Let's fetch the data about "schools" and "terms" from SIS API.

Update script.js to include the following:

const key = "USE_YOUR_KEY"; // trrible practice!
// should never save API key directly in source code
// in particular when it can potentily be seen by users

const requestOptions = {
  method: "GET",
  redirect: "follow",
};

fetch(
  `https://sis.jhu.edu/api/classes/codes/schools?key=${key}`,
  requestOptions
)
  .then((response) => response.json())
  .then((data) => populateSelector("schools", data))
  .catch((error) => console.log("error", error));

fetch(`https://sis.jhu.edu/api/classes/codes/terms?key=${key}`, requestOptions)
  .then((response) => response.json())
  .then((data) => populateSelector("terms", data))
  .catch((error) => console.log("error", error));

Make note of the use of then clauses.

Step 10

The final piece is to add send a search request to the SIS API and display the response.

Update the search function:

-  console.log(`search for ${query} in the ${school} during ${term}`);
-  showSearchResults(courses);
+  fetch(
+    `https://sis.jhu.edu/api/classes?key=${key}&School=${school}&Term=${term}&CourseTitle=${query}`,
+    requestOptions
+  )
+  .then((response) => response.json())
+  .then((data) => showSearchResults(data))
+  .catch((error) => console.log("error", error));

Save your code again and try it in your browser. Make sure the "Allow-Control-Allow-Origin" plugin is "on".

You can access the complete application at https://github.com/cs280fall20/course-search-sis-api.

Asynchronous Programming

In JavaScript programs, whenever you write code that involves operations like making a network request or querying a database, you are dealing with asynchronous code. You need to understand how asynchronous code behaves. And more importantly, you need to write asynchronous code in a clean and maintainable way. And that is what you are going to learn in this module.

Some Things Take Time!

Let's us visit the SIS API one more time.

Use Postman to get all the courses offered by the "Whiting School of Engineering". You should be using the following endpoint.

https://sis.jhu.edu/api/classes/Whiting School of Engineering?key=apikeyvalue

The above request will take at least several seconds to return with a response containing, at the time of writing, more than 47000 courses!

This preposterously large number of courses is the total of all sections of all courses offered by WSE from Spring 2009 up to now.

It is poor design that such a large amount of data is dumped on the user without any pagination. This poor design decision is however useful for us, as we bring this message home: access to resources over the internet could be visibly slow.

Not All Tasks Created Equal!

All programs are seen as composed of control structures, ordered statements or subroutines, executed in sequence.

function task(id) {
  console.log("Task " + id);
}

task(1);
task(2);
task(3);
task(4);

What will happen when one task (subroutine) takes a long time to complete?

function task(id) {
  console.log("Task " + id);
}

function longtask(id) {
  console.log("Task " + id + " started!");
  for(let i = 0; i < 3e9; i++) {
    // do something!
  }
  console.log("Task " + id + " finished!");
}

task(1);
longtask(2);
task(3);
task(4);

The long (resource intensive) task appears to block the ones that follow!

What are the implications of this for event-driven applications such as web apps?

Well, as long as the "long task" is running, all other things are blocked! The web app will appear unresponsive or frozen!

Example

For example, YouTube loads the video content as you view it. This is a resource intensive process, in particular over a slow network. If it were to block other tasks/processes until it has finished, you would not be able to do any of the following while the video content is loading:

  • Like the video
  • Leave a comment or even scroll through the comments
  • Maximize/minimize
  • Change the volume
  • Use any of the controls (play, pause, etc)
  • ...

But this is not how YouTube works!

So what is the secrete? How does it perform a resource-intensive task such as streaming a video while the web page remains perfectly responsive?!

Asynchronous Programming

Asynchronous programming is a technique that allows you to execute a resource-intensive task in a non-blocking way.

In the context of the earlier example, while a "long task" is running, the "control" is returned to the application (e.g. the browser), so it can continue to handle user input and perform other tasks.

JavaScript supports async programming! In particular, operations like making a network request or querying a database can be (and almost always are) done asynchronously.

Here is our earlier example where I have used the built-in function setTimeout to simulate a time consuming task that runs asynchronously.

function task(id) {
  console.log("Task " + id);
}

function longtask(id) {
  console.log("Task " + id + " started!");
  setTimeout(() => console.log("Task " + id + " finished!"), 5000);
}

task(1);
longtask(2);
task(3);
task(4);

The setTimeout is an example of an asynchronous or non-blocking function.

A helpful way to think about setTimeout is that it schedules a task to be performed in the future. It, however, does not wait until the task is executed. Therefore, setTimeout does not block the execution of other functions. Task-3, for instance, does not need to wait for 5 seconds to get its chance to be executed.

I recommend viewing the short YouTube video Asynchronous Vs Synchronous Programming by Web Dev Simplified.

Understanding Async Operations

Restaurant Metaphor

A blocking synchronous restaurant is one where the waiter will wait until your food is ready and served before helping another customer.

A non-blocking asynchronous restaurant is one where the waiter comes to your table, takes your order and give it to the kitchen. The waiter then moves on to server other tables while the chief is preparing your meal. Once your meal is ready, the waiter will get a callback to bring your food to your table.

House chores Metaphor

In a blocking synchronous system, you wash the dishes, when you are done, you take the garbage out.

In a non-blocking asynchronous system, you put the dishes in the dish washer and start it. While the dishes are being washed, you clean the kitchen and take the garbage out. Once the dishes are done, the dishwasher will beep to call on you so you can put the clean dishes in the cupboards.

Single vs Multi Threads

Some programming languages allow concurrent programming (parallel computing). This is a technique where two or more processes start at the same time, run in an interleaved fashion through context switching and complete in an overlapping time period by managing access to shared resources e.g. on a single core of CPU. This feature is called "Multithreading"; Java was design from very early on to provide it. C++ has added it in recent versions.

In our restaurant metaphor,

  • a single threaded program is like having a single chief/cook. All kitchen tasks are done by one person, one after another.
  • a multithreaded program is when the chief hires several cooks so while one task is being done (e.g. one meal is being cooked) other cooks can take on other tasks at the same time.

JavaScript is single threaded.

Event Loop

So how does JavaScript behave asynchronously?

The JavaScript engine relies on the hosting environment, such as the browser, to tell it what to execute next. Any async operation is taken out of the call stack and executed by the hosting environment. (This could be done in a concurrent fashion). Once the async job is completed, it will be placed in a callback queue to return to the call stack. This cycle is monitored by a process called the event loop.

The Event Loop monitors the Call Stack and the Callback Queue. If the Call Stack is empty, it will take the first event from the queue and will push it to the Call Stack, which effectively runs it.

It is beyond the scope of this course to explore this process in details. Here is a demo of our earlier async code that showcases how these pieces work together. The demo is based off a visualization and a great article by Alexander Zlatkov (read the article here).

I also recommend watching Philip Robert's talk at JSConf EU, What the heck is the event loop anyway? on YouTube.

Patterns for Dealing with Async Code

We are going to use setTimeout to simulate asynchronous call to a database and async network requests. Consider the following scenario:

  • We look up a user in our database to collect its bank account number.
  • We send a HTTP GET request to a bank, providing the account number, to receive a list of loans associated with the given account.
  • After the list of loans were received, we will send a second request to get a complete transaction history (e.g. payments, etc) associated with the most recent loan.

Don't get hung up on the irrelevant details such as what if there are no loans associated with the account, etc.

Let's start by (simulation of) getting a user object from a database.

function getUser(id) {
  console.log("Reading a user from a database...");
  setTimeout(() => {
    console.log("Received user data...");
    return { "ID": id, "Account number": "58721094531267" }
  }, 2000);
}

console.log("listening for events");
getUser(1);
console.log("still listening for events!");

Let's try to store and display the user returned from the database:

function getUser(id) {
  console.log("Reading a user from a database...");
  setTimeout(() => {
    console.log("Received user data...");
    return { "ID": id, "Account number": "58721094531267" }
  }, 2000);
}

console.log("listening for events");
const user = getUser(1);
console.log(`user: ${user}`);
console.log("still listening for events!");

The user object is undefined because the return statement in getUser is executed (at least) two seconds after it was called. So what is returned from getUser will not be available at the time of calling it.

So how can we access the user returned from getUser function? There are three patterns to deal with asynchronous code:

  • Callbacks
  • Promises
  • Async/await

We will look at each in the following sections.

Callback

Callback is a function that is called when the result of an asynchronous function is ready.

function getUser(id, callback) {
  console.log("Reading a user from a database...");
  setTimeout(() => {
    console.log("Received user data...");
    callback({ "ID": id, "Account number": "58721094531267" });
  }, 2000);
}

console.log("listening for events");
getUser(1, (user) => console.log(user));
console.log("still listening for events!");

Here is an exercise for you: Add an asynchronous function, getLoans, that simulates sending a HTTP GET request to a bank, providing the account number, to receive a list of loans associated with the given account. Use the callback pattern to display the loans.

function getUser(id, callback) {
  console.log("Reading a user from a database...");
  setTimeout(() => {
    console.log("Received user data...");
    callback({ "ID": id, "Account number": "58721094531267" });
  }, 2000);
}

function getLoans(account) {
  // TODO update this to an asynchronous function
  return ["loan 1", "loan 2", "loan 3"];
}

console.log("listening for events");
getUser(1, (user) => { 
  console.log(user);

  // TODO call getLoans and display the returned values

});
console.log("still listening for events!");
Solution
function getUser(id, callback) {
  console.log("Reading a user from a database...");
  setTimeout(() => {
    console.log("Received user data...");
    callback({ "ID": id, "Account number": "58721094531267" });
  }, 2000);
}

function getLoans(account, callback) {
  console.log("Sending HTTP GET request to a bank...");
  setTimeout(() => {
    console.log("Received loan data...");
    callback(["loan 1", "loan 2", "loan 3"]);
  }, 2000);
}

console.log("listening for events");
getUser(1, (user) => { 
  console.log(user);
  getLoans(user["Account number"], (loans) => {
    console.log(loans);
  })
});
console.log("still listening for events!");

Callback Hell!

Here is the structure of our application which simulates getting transactions for the most recent loan associated with the account number of our user.

// functions "getUser", "getLoans", and "getTransactions" not shown

console.log("listening for events");
getUser(1, (user) => { 
  getLoans(user["Account number"], (loans) => {
    getTransactions(loans["Most recent"], (transactions) => {
      console.log(transactions);
    });
  });
});
console.log("still listening for events!");

Notice, if the function getUser, getLoans, and getTransactions were synchronous, the program would look like this:

// functions "getUser", "getLoans", and "getTransactions" not shown

console.log("listening for events");
const user = getUser(1);
const loans = getLoans(user["Account number"]);    
const transactions = getTransactions(loans["Most recent"]);
console.log(transactions);
console.log("still listening for events!");

The second snippet corresponding to synchronous code is arguably more readable and easier to understand.

In the asynchronous version, because of the callbacks, we have a deeply nested structure. In practice, this can get much deeper. This is referred to as callback hell (or some resouces call it "Christmas tree problem").

method1(arg1, (arg2) => { 
  method2(arg2, (arg3) => {
    method3(arg3, (arg4) => { 
      method4(arg4, (arg5) => { 
        method5(arg5, (arg6) => {
          method6(arg6, (arg7) => {
            // ...
            // CALLBACK HELL
            // ... 
          }); 
        });   
      });   
    });
  });
});

Promises

Promise was added to JavaScript in ES6. It was meant to address some of the issues with asynchronous coding, including the callback hell.

A "Promise" is a JavaScript object that holds the eventual result of an asynchronous operation.

A "promise" object has a "state" and its state at any point in time is one of the following:

  • Pending: initial state, neither fulfilled nor rejected.
  • Fulfilled: meaning that the operation was completed successfully.
  • Rejected: meaning that the operation failed.

When you create a Promise to kick off some asynchronous operation, it is at the pending state. Once the asynchronous operation is completed, the Promise is fulfilled. If for any reason the asynchronous operations fails (for example, the network connection is disrupted in the middle of a HTTP request), then the Promise is rejected.

This is the general pattern for creating a Promise object:

const myPromise = new Promise((resolve, reject) => {
  let result = [];
  let success = false;
  // do some async work!
  // success and result must be updated through this work.

  if (success) {
    resolve(result);
  } else {
    reject(new Error("some message!"));
  }
});

Note the constructor of Promise takes in a function which itself takes two (callback) functions, resolve and reject, as arguments.

Here is how you would consume a promise:

myPromise
  .then((result) => {
    /* do something with result */
  })
  .catch((error) => {
    /* display the error or otherwise handle it */
  });

Notice you have seen this pattern before when we used the fetch API.

I highly recommend watching Fireship's short YouTube video "JavaScript Promise in 100 Seconds".

The YouTube video JavaScript Promises In 10 Minutes from Web Dev Simplified is another excellent resource.

Replacing Callbacks with Promises

Here is the code which exhibits the callback pattern for dialing with async work:

function getUser(id, callback) {
  console.log("Reading a user from a database...");
  setTimeout(() => {
    console.log("Received user data...");
    callback({ "ID": id, "Account number": "58721094531267" });
  }, 2000);
}

console.log("listening for events");
getUser(1, (user) => console.log(user));
console.log("still listening for events!");

We are going to replace the callback pattern with the Promise pattern. As a first step, we must update our async function getUser to return a Promise.

function getUser(id) {
  console.log("Reading a user from a database...");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Received user data...");
      resolve({ ID: id, "Account number": "58721094531267" });
    }, 2000);
  });
}

Note that I've ignored the case where the Promise may be "rejected" to keep the example concise.

Now, as an exercise, update how getUser is employed. (Hint: you must consume a Promise).

function getUser(id) {
  console.log("Reading a user from a database...");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Received user data...");
      resolve({ ID: id, "Account number": "58721094531267" });
    }, 2000);
  });
}

console.log("listening for events");
getUser(1, (user) => console.log(user)); // TODO update me!
console.log("still listening for events!");
Solution
function getUser(id) {
  console.log("Reading a user from a database...");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Received user data...");
      resolve({ ID: id, "Account number": "58721094531267" });
    }, 2000);
  });
}

console.log("listening for events");
getUser(1)
  .then((user) => console.log(user));
console.log("still listening for events!");

Promises Exercise

As a reference, here is the earlier work we had done for getUser:

Callback pattern
function getUser(id, callback) {
  console.log("Reading a user from a database...");
  setTimeout(() => {
    console.log("Received user data...");
    callback({ "ID": id, "Account number": "58721094531267" });
  }, 2000);
}

console.log("listening for events");
getUser(1, (user) => console.log(user));
console.log("still listening for events!");
Promise pattern
function getUser(id) {
  console.log("Reading a user from a database...");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Received user data...");
      resolve({ ID: id, "Account number": "58721094531267" });
    }, 2000);
  });
}

console.log("listening for events");
getUser(1)
  .then((user) => console.log(user));
console.log("still listening for events!");

Exercise: Update the following code to use Promises instead of callbacks.

function getUser(id, callback) {
  console.log("Reading a user from a database...");
  setTimeout(() => {
    console.log("Received user data...");
    callback({ ID: id, "Account number": "58721094531267" });
  }, 2000);
}

function getLoans(account, callback) {
  console.log("Request for loan data...");
  setTimeout(() => {
    console.log("Received loan data...");
    callback({ "Most recent": "loan 3", All: ["loan 1", "loan 2", "loan 3"] });
  }, 2000);
}

function getTransactions(loan, callback) {
  console.log("Request for transactions data...");
  setTimeout(() => {
    console.log("Received transactions data...");
    callback(["tran 3", "tran 2", "tran 1"]);
  }, 2000);
}

console.log("listening for events");
getUser(1, (user) => {
  getLoans(user["Account number"], (loans) => {
    getTransactions(loans["Most recent"], (transactions) => {
      console.log(transactions);
    });
  });
});
console.log("still listening for events!");
Solution
function getUser(id) {
  console.log("Reading a user from a database...");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Received user data...");
      resolve({ ID: id, "Account number": "58721094531267" });
    }, 2000);
  });
}

function getLoans(account) {
  console.log("Request for loan data...");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Received loan data...");
      resolve({ "Most recent": "loan 3", All: ["loan 1", "loan 2", "loan 3"] });
    }, 2000);
  });
}

function getTransactions(loan) {
  console.log("Request for transactions data...");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Received transactions data...");
      resolve(["tran 3", "tran 2", "tran 1"]);
    }, 2000);
  });
}

console.log("listening for events");
getUser(1)
  .then((user) => getLoans(user["Account number"]))
  .then((loans) => getTransactions(loans["Most recent"]))
  .then((transactions) => {
    console.log(transactions);
  });
console.log("still listening for events!");

Chaining Promises

Notice how we had gone from this pattern (which exhibits callback hell)

console.log("listening for events");
getUser(1, (user) => {
  getLoans(user["Account number"], (loans) => {
    getTransactions(loans["Most recent"], (transactions) => {
      console.log(transactions);
    });
  });
});
console.log("still listening for events!");

to this pattern which exhibits promise chaining:

console.log("listening for events");
getUser(1)
  .then((user) => getLoans(user["Account number"]))
  .then((loans) => getTransactions(loans["Most recent"]))
  .then((transactions) => {
    console.log(transactions);
  });
console.log("still listening for events!");

In the first implementation we used callbacks and that is why we ended up with the nested structure (callback hell problem). In the second implementation we used Promises and we got a flat structure.

Since Promises expose then method we can chain them to implement a complex async operation.

As a good practice, whenever you work with Promises you must make sure to catch any errors.

console.log("listening for events");
getUser(1)
  .then((user) => getLoans(user["Account number"]))
  .then((loans) => getTransactions(loans["Most recent"]))
  .then((transactions) => {
    console.log(transactions);
  })
  .catch((error) => {
    console.log(error); // probably need to do more than this!
  });
console.log("still listening for events!");

Async & Await

Here is what we have so far:

console.log("listening for events");
getUser(1)
  .then((user) => getLoans(user["Account number"]))
  .then((loans) => getTransactions(loans["Most recent"]))
  .then((transactions) => {
    console.log(transactions);
  })
  .catch((error) => {
    console.log(error); // probably need to do more than this!
  });
console.log("still listening for events!");

We can make this code even simpler using the new JavaScript feature async/await (which they have borrowed from C#).

Async/Await allows you to write asynchronous code that looks like synchronous code!

Anytime you are calling a function that returns a Promise, you can "await" its results, and then get the results just like calling a synchronous function.

For example, instead of

getUser(1)
  .then((user) => console.log(user))

we can write

const user = await getUser(1);
console.log(user);

So, we can rewrite the code at the top of the page as

console.log("listening for events");
try {
  const user = await getUser(1);
  const loans = await getLoans(user["Account number"]);
  const transactions = await getTransactions(loans["Most recent"]);
  console.log(transactions);
} catch (error) {
  console.log(error);
}
console.log("still listening for events!");

There is one caveat however! You cannot use the await operator in any place that you want. The operator await can only be used inside a function, and that function must be decorated with the async modifier. So we must do something like this:

async function displayTransactions() {
  try {
    const user = await getUser(1);
    const loans = await getLoans(user["Account number"]);
    const transactions = await getTransactions(loans["Most recent"]);
    console.log(transactions);
  } catch (error) {
    console.log(error);
  }
}

console.log("listening for events");
displayTransactions();
console.log("still listening for events!");

Async/Await are syntax sugar! Under the hood, it is converted to a chain of Promises!

Putting it all together
function getUser(id) {
  console.log("Reading a user from a database...");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Received user data...");
      resolve({ ID: id, "Account number": "58721094531267" });
    }, 2000);
  });
}

function getLoans(account) {
  console.log("Request for loan data...");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Received loan data...");
      resolve({ "Most recent": "loan 3", All: ["loan 1", "loan 2", "loan 3"] });
    }, 2000);
  });
}

function getTransactions(loan) {
  console.log("Request for transactions data...");
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Received transactions data...");
      resolve(["tran 3", "tran 2", "tran 1"]);
    }, 2000);
  });
}

async function displayTransactions() {
  try {
    const user = await getUser(1);
    const loans = await getLoans(user["Account number"]);
    const transactions = await getTransactions(loans["Most recent"]);
    console.log(transactions);
  } catch (error) {
    console.log(error);
  }
}

console.log("listening for events");
displayTransactions();
console.log("still listening for events!");

I recommend watching Fireship's short YouTube video "The Async Await Episode I Promised" to further solidify your understanding of this subject.

Modules

Modules are the primary construct which we organize our JavaScript code.

At first glance, a JavaScript module is just a JavaScript file; it arises naturally when splitting large JavaScript programs into several sections each stored in a separate file.

Additionally, modules create scope. The variables declared in a module have "file scope"; they are not accessible outside of the module. A module system provides mechanism to export any value declared in a module and import them in another module when needed. Therefore, modules are another way (in addition to Class and Closure) to create encapsulation.

For a long time, JavaScript modules were implemented via third-party libraries (not built into the language). ECMAScript 2015 (ES6) was the first time that JavaScript provided built-in modules. We will focus on ES6 modules.1

1

Later, we need to learn an older module system, CommonJS, which is used by NodeJS.

IFEE Revisited!

We know that JavaScript functions create scope. We have seen the pattern of Immediately Invoked Function Expression or IFEE to create Object-Oriented like encapsulation:

// script.js file
(function () {

  const number = 2;
  console.log(number);
  
})();

Assume the above code snippet is all there is inside a script.js file which is linked to our index.html,

<script src="script.js"></script>

The IFEE gets executed as soon as the script is loaded to the webpage. The value of number (that is "2") will be printed to the browser console. The variable number however is not accessible from outside of the IFEE.

On the other hand, if we were to remove the IFEE pattern,

// script.js file
const number = 2;
console.log(number);

The number variable will be placed in the global scope.

An ES6 Module is just a file!

ES6 Modules can be used to create scope. All you need to do is to tell the browser to consider script.js as a "module".

<script type="module" src="script.js"></script>

Notice the use of type="module" attribute in the <script> element.

Modules work only via HTTP(s), not in local files If you try to open a web-page locally, via file:// protocol, you'll find that scripts attributed as "module" are blocked by CORS policy. Use a local web-server such as VS Code Live Server Extension to test modules.

As it can be seen in the image below, you will not be able to access the number variable.

Notice the content of script.js is still simply the following two lines

// script.js file
const number = 2;
console.log(number);

With ES6 modules, each file has its own scope. To make values (objects, functions, classes, etc) available to the outside world, you must first export the value and then import the value where needed in other modules (files).

Exporting

To export a value, append its declaration with the export keyword:

// account.js file
let balance = 0;
export const INTEREST_RATE = 0.2;

export function deposit(amount) {
  balance += amount;
}

export function widthraw(amount) {
  balance -= amount;
}

export function getBalance() {
  return balance;
}

You can export values one by one (as it is done above) or you can export all in a single statement at the end of the module:

// account.js file
let balance = 0;
const INTEREST_RATE = 0.2;

function deposit(amount) {
  balance += amount;
}

function widthraw(amount) {
  balance -= amount;
}

function getBalance() {
  return balance;
}

export { INTEREST_RATE, deposit, widthraw, getBalance };

You can also give an alias to an exported value with the as keyword:

export { INTEREST_RATE as interest, deposit, widthraw, getBalance };

Moreover, you can break a long export statement into several export statements (not a very common practice):

export { INTEREST_RATE as interest };
export { deposit, widthraw, getBalance };

Importing

You need an import statement to import the values exported from a module.

// script.js file
import { deposit, widthraw, getBalance } from "./account.js";

console.log(getBalance());

deposit(100);
widthraw(20);

console.log(getBalance());
  • The import statement must be placed at the top of your script/module file.
  • The import statement begins with the import keyword.
  • Imported values are comma separated inside a pair of curly brackets.
  • The from keyword is used to supply the location of the module relative to the current file.

As it can be seen in the image below, the value returned by getBalance shows the balance variable (in account.js file) has been updated.

Notice, there is no requirement that we import everything a module exports. In the snippet above, I have not imported interest (alias for the INTEREST_RATE constant). In the snippet below, I only import the interest:

// script.js file
import { interest } from "./account.js";

console.log(interest);

Here is the output to the browser console:

Note that it is not possible to import a value from a module that was not exported:

// script.js file
import { balance } from "./account.js";

console.log(balance);

Similar to exports, you can use aliases when importing:

// script.js file
import { deposit, widthraw, getBalance as balance } from "./account.js";

console.log(balance());

deposit(100);
widthraw(20);

console.log(balance());

Although not common, it is possible to break a long import statement into several import statements:

import { deposit, widthraw } from "./account.js";
import { getBalance as balance, interest } from "./account.js";

A common pattern is to import everything that's exported by a module into one object as shown in the example below:

// script.js file
import * as Account from "./account.js";

console.log(Account.getBalance());

Account.deposit(100);
Account.widthraw(20);

console.log(Account.getBalance());

Notice (1) there is no {} in the import statement above. (2) The imported values are accessed with the dot notation (e.g.Account.getBalance()).

A module does not create a type!

Modules are the primary means to employ "Separation of Concerns" design principle. They, however, do not create a (data) "type". In our earlier example, there is only ever one instance of Account. You cannot create different types of accounts each having their own "balance".

// script.js file
import * as Account from "./account.js";

console.log(Account.getBalance());

Account.deposit(100);
Account.widthraw(20);

console.log(Account.getBalance());

To use Account as a type, you need JavaScript classes:

// account.js file
const INTEREST_RATE = 0.2;

class Account {
  constructor(balance) {
    this.balance = balance;
  }

  deposit(amount) {
    this.balance += amount;
  }

  widthraw(amount) {
    this.balance -= amount;
  }
}

export { INTEREST_RATE as interest, Account };

We can now use Account as a type:

// script.js file
import { Account } from "./account.js";

const checking = new Account(100);
checking.deposit(20);

console.log(checking.balance);

This may seem contrary to our goal of hiding "balance" since JavaScript classes do not provide information hiding (at the time of writing), and the "balance" is not protected (hidden) now.

You need to appreciate the difference between modules and classes:

  • Use classes to create types.
  • Use modules to modularize your JavaScript program.

The module construct and the class construct have the same goal of creating encapsulations which is to group data and behavior together in logical units. The module construct further allows information hiding by creating scope. At the time of writing, information hiding is not achieved by classes. On the other hand, classes create abstractions (types) which you can instantiate or extend.

Default Export/Import

If you want to export a single value (or when one value is the primary element in a module), you could use a default export:

// account.js file
const INTEREST_RATE = 0.2;

class Account {
  constructor(balance) {
    this.balance = balance;
  }

  deposit(amount) {
    this.balance += amount;
  }

  widthraw(amount) {
    this.balance -= amount;
  }
}

export default Account;
export { INTEREST_RATE as interest };

When importing a "default export," you do not use curly brackets.

// script.js file
import Account from "./account.js";

const checking = new Account(100);
checking.deposit(20);

console.log(checking.balance);

It is possible to import both default and non-default exports in one import statement. The syntax is as follows:

// script.js file
import Account, { interest } from "./account.js";

console.log(interest);

const checking = new Account(100);
checking.deposit(20);

console.log(checking.balance);

Before ES6 Modules

For a long time, JavaScript existed without a built-in module construct. At first, this was not a problem, because initially scripts were small and simple, so there was no need. But eventually JavaScript programs became more and more complex, so the community invented a variety of ways to organize code into modules. To name some (for historical reasons):

  • AMD – one of the most ancient module systems, initially implemented by the library require.js.
  • CommonJS – the module system used by NodeJS.
  • UMD – meant to be a universal system, compatible with AMD and CommonJS.

JavaScript community is adopting ES6 modules but the legacy systems are still in use. In particular, the CommonJS module system is still the primary way Node applications are written. We shall learn about it when we learn about NodeJS.

Node

NodeJS (or simply Node) is an open source cross platform runtime environment for executing JavaScript code outside of a browser.

We use Node in particular for server-side programming to build APIs.

Use this link to download and install Node on your operating system.

Before Node

Before Node, we only used JavaScript to build applications that run inside of a browser. Every browser has a JavaScript Engine that takes your JavaScript code and turns it into an executable form

A JavaScript engine, at its core, consists of an interpreter and a runtime environment.

  • JavaScript was (and still is considered) as an interpreted language. This means the JavaScript engine in a browser reads over the JavaScript code, interprets each line, and runs it.
  • Modern browsers, however, use a technology known as Just-In-Time (JIT) compilation, which compiles JavaScript to executable bytecode just as it is about to run. The byte-codes are then executed using a JavaScript Runtime Environment.
  • The runtime environment inside a browser provides global objects such as window and document that enable JavaScript to interface with the browser and the webpage it is part of.

In 2009, Ryan Dahl took Google's v8 engine and embedded it in a C++ program and called that program Node.

Node uses Google's V8 to convert JavaScript into byte-codes but runs it outside of a browser.

In Node, we don’t have browser environment objects such as window or the document object. Instead, we have other objects that are not available in browsers, such as objects for working with the file system, network, operating system, etc.

If you are interested to learn more about the JavaScript Engine and how your code is executed, I recommend the following readings:

Run a script file with Node

Create a folder and place the following index.js file in it

// index.js file
console.log("Hello Node!");

Open the terminal at the folder containing the index.js. In the terminal, run the following command:

node index.js

This must produce the following output:

Hello Node!

Since node can only execute .js files, adding .js extension to the file path is optional! So, you can also use the command node index in the terminal to run the index.js file.

If we provide a directory path instead of a file path to Node, it will try to find and run an index.js file inside that directory. So, in our case, we can also run the index.js file by simply typing node . in the terminal (notice the proceeding dot that indicates "current folder").

If you just enter node in the terminal then you will run Node's repl module which provides a Read-Eval-Print-Loop (REPL) implementation.

Node Module System

Every file in a Node application is considered a module.

Values defined are scoped in their file. If you want to use a value, you must explicitly export it from its file and then import it into another file.

This is the same as in ES6 modules. The only difference is the syntax of exports and imports.

Nodes default module system is based on CommonJS.1

// acount.js file
const INTEREST_RATE = 0.2;

class Account {
  constructor(balance) {
    this.balance = balance;
  }

  deposit(amount) {
    this.balance += amount;
  }

  widthraw(amount) {
    this.balance -= amount;
  }
}

module.exports.interest = INTEREST_RATE;
module.exports.Account = Account;

Notice the use of module.exports instead of the export keyword in ES6 modules.

The module.exports is a JavaScript object. Any value which you want to export can be added as a property to the exports object.

To load a module, we use the require function (instead of ES6 import statements).

// index.js file
const { Account } = require("./account.js");

const checking = new Account(100);
checking.deposit(20);

console.log(checking.balance);
  • The require function takes the location of the module relative to the current file.
  • It returns the module.exports object from the module file. In the code snippet above, I have used Destructuring assignment to destruct the exports object.
  • The require statement can be placed anywhere in a file but it is typically put at the top. (Consider this a good practice and follow it!)
1

You can also use the ES6 module system with Node. Please refer to Node's document on ECMAScript 2015 (ES6) and beyond.

Module Wrapper Function

Let's see how the CommonJS module system works. First, try the following:

// index.js file
console.log(module);

Run the index.js file using the node runtime environment. It must print an output similar to the one below: (The "paths" will be different on your computer.)

Module {
  id: '.',
  path: '/Users/alimadooei/Desktop/CS280/01-FA-20/staging/code/app',
  exports: {},
  parent: null,
  filename: '/Users/alimadooei/Desktop/CS280/01-FA-20/staging/code/app/index.js',
  loaded: false,
  children: [],
  paths: [
    '/Users/alimadooei/Desktop/CS280/01-FA-20/staging/code/app/node_modules',
    '/Users/alimadooei/Desktop/CS280/01-FA-20/staging/code/node_modules',
    '/Users/alimadooei/Desktop/CS280/01-FA-20/staging/node_modules',
    '/Users/alimadooei/Desktop/CS280/01-FA-20/node_modules',
    '/Users/alimadooei/Desktop/CS280/node_modules',
    '/Users/alimadooei/Desktop/node_modules',
    '/Users/alimadooei/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ]
}
  • Notice module itself is an object.
  • It is present in every file!
  • It has several properties, including exports which itself is an empty object.
  • When we want to export a value, we add that value as a property to the exports object.
    • If you have a single value to export, you can overwrite the exports object to simply store only that value.

But where does the module object come from? The Node application wraps every file in an IFEE that looks like this:

(function (exports, require, modules, __filename, __dirname) {
  // your file content goes here!
})();

This is essentially the mechanics of CommonJS modules.

To further experiment with this secrete wrapper IFEE, try the following:

// index.js file
console.log(__filename);
console.log(__dirname);

Here is the output on my computer (will be different on yours):

/Users/alimadooei/Desktop/CS280/01-FA-20/staging/code/app/index.js
/Users/alimadooei/Desktop/CS280/01-FA-20/staging/code/app

Path Module

Node comes with a bunch of useful modules. You will find information about its modules on Node's online documentation. We will look at a few of these.

Let's play around with the Path module first.

// index.js file
const path = require("path");

const pathObj = path.parse(__filename);

console.log(pathObj);

Notice the argument to require is not a file path. It simply is the name of the module. This is the pattern we use to load Node's built-in modules.

Run index.js and you will get an output similar to this one:

{
  root: '/',
  dir: '/Users/alimadooei/Desktop/CS280/01-FA-20/staging/code/app',
  base: 'index.js',
  ext: '.js',
  name: 'index'
}

OS Module

Let's experiment with the OS module.

// index.js file
const os = require("os");

console.log(`Total memory ${os.totalmem()}`);
console.log(`Free memory ${os.freemem()}`);

Here is the output when I run the index.js file on my computer:

Total memory 17179869184
Free memory 544534528

FS Module

Let's experiment with the File system (FS) module.

// index.js file
const fs = require("fs");

const files = fs.readdirSync("./");

console.log(files);

The readdirSync will synchronously read the files stored in the given directory (the path to the directory is given as an argument to readdirSync function).

On my computer, running index.js results in:

[ 'account.js', 'index.js' ]

There is an asynchronous version of this function which you can (and should) use:

// index.js file
const fs = require("fs");

fs.readdir("./", (err, files) => {
  console.log(files);
});

Notice we have used the callback pattern to deal with the async operation. The readdir function was written before JavaScript had Promise object. The function does not return a Promise so we cannot use the Promise pattern (nor async/await) here.

HTTP Module

One of the most powerful modules in Node is the HTTP module. It can be used for building networking applications. For example, we can build an HTTP server that listens to HTTP requests on a given port.

// index.js file
const http = require("http");

const PORT = 5000;
const server = http.createServer(routes);

function routes(request, response) {
  if (request.url === "/") {
    response.write("Hello world!");
    response.end(); // Don't forget this!
  }
}

server.listen(PORT);

console.log(`The server is listening on port ${PORT}`);

As you run index.js, you must see the following message printed in the terminal:

The server is listening on port 5000

Note the application is running! To stop it, you must halt the process by pressing Ctrl + C.

While the server application is running, open your browser and head over to http://localhost:5000/. You must see the "Hello World" message!

A note on PORT

In computer networking, a port is a communication endpoint.

  • Port numbers start from 0.
  • The numbers 0 to 1024 are reserved for privileged services (used by the operating system, etc.).
  • For local development, you can safely use any number \(\ge 3000\).

It is beyond the scope of this course to explore what a port is and how it operates. An interested reader is referred to the Wikipedia entry "Port (computer networking)".

A very basic API

Let's update our HTTP server application as follows:

// index.js file
const http = require("http");

// sample schools
schools = [
  {
    Name: "School of Medicine",
  },
  {
    Name: "Whiting School of Engineering",
  },
];

// sample terms
terms = [
  {
    Name: "Fall 2020",
  },
  {
    Name: "Summer 2020",
  },
  {
    Name: "Spring 2020",
  },
];

// sample courses
courses = [
  {
    OfferingName: "EN.601.226",
    SectionName: "01",
    Title: "Data Structures",
    Instructors: "A. Madooei",
  },
  {
    OfferingName: "EN.601.226",
    SectionName: "02",
    Title: "Data Structures",
    Instructors: "A. Madooei",
  },
  {
    OfferingName: "EN.601.280",
    SectionName: "01",
    Title: "Full-Stack JavaScript",
    Instructors: "A. Madooei",
  },
  {
    OfferingName: "EN.601.280",
    SectionName: "02",
    Title: "Full-Stack JavaScript",
    Instructors: "A. Madooei",
  },
];


const PORT = 5000;
const server = http.createServer(routes);

function routes(request, response) {
  if (request.url === "/") {
    response.write("Welcome to Courses API!");
  } else if (request.url === "/api/schools") {
    response.write(JSON.stringify(schools));
  } else if (request.url === "/api/terms" ) {
    response.write(JSON.stringify(terms));
  } else if (request.url === "/api/courses" ) {
    response.write(JSON.stringify(courses));
  }
  response.end();
}

server.listen(PORT);

console.log(`Server is listening on port ${PORT}`);

Run the application and then head over to your browser and try the following endpoints:

Try these endpoints in the Postman application too (as HTTP Get requests).

The example above showcases the essence of how API servers are built using NodeJS.

We will explore this further in future modules.

Express

Express is a web framework for Node. It allows to build web applications and APIs.

We are going to work with Express for several lectures.

The source code for the completed app (which we will build in this module) can be found at this link.

NPM

There is a lively ecosystem around JavaScript in general and Node in particular. The community embraced open-source projects. A plethora of Node libraries and applications are published and many more are added every day. These open-source projects, which are called Node packages, are published through Node Package Manager or NPM.

NPM is first and foremost an online repository of software (Node in particular) projects. At the time of writing, NPM is the world's largest software registry, with about a million records.

NPM also provides a command-line interface (CLI) that assists with package installation and dependency management. The NPM CLI is installed with Node. To ensure you have it, open the terminal and type npm --version which on my computer, at the time of writing, outputs 6.14.7.

Node packages are like building blocks that you can put together to build a software application.

You can search for packages on http://npmjs.org/. For example, the Express framework can be found at https://www.npmjs.com/package/express.

Once you have a package you want to install, it can be installed with a single command-line command.

If you want to learn more about NPM, read "About npm" on http://npmjs.org/.

Step 1: Adding Express as a Dependency

Create a new folder. I will call mine express-app. Open this folder in the terminal and run the following command:

npm init -y

The command will create a package.json file inside the express-app folder:

{
  "name": "express-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

By convention, every Node application (package) has a package.json file at its root folder. The file holds metadata relevant to the project. It is also used, by NPM, for managing the project's dependencies, scripts, version and a whole lot more. We will explore this further in future.

A Node package a folder containing a program described by a package.json file.

Next, run the following command in the terminal:

npm install express

The npm install command installs a package, and any packages that it depends on.

After installing Express, it can be used in our Node application. This means we can, for instance, create a file index.js, and import Express as a module:

// index.js file
const express = require("express");

// use express in your application!

Notice three changes as a result of installing Express:

  1. The following property was added to package.json

    "dependencies": {
      "express": "^4.17.1"
    }
    

    This simply states that the Express package version 4.17.1 or higher is a "dependency" of our application.

  2. A folder node_modules was added to express-app. This folder has a lot of content in it! Essentially, the Express package itself is built out of several other packages which themselves are made of other packages. All of these packages are "installed" (downloaded to node_modules folder) so that Express (and thus your app) can work as expected.

    Make sure to always exclude node_modules folder from your Git repository. Add node_modules/* to your .gitignore file.

  3. A file, package-lock.json is added to the root of your project. As stated earlier, the Express framework itself has many other dependencies. All the packages needed to make Express work are also added as a dependency of your project with their exact versions to the package-lock.json file.

    The package-lock.json is automatically generated and modified. You should never directly edit it.

    Make sure to include package-lock.json in your Git repository. For any subsequent installs of your project, you need this file to get the exact dependencies which your project is build on.

Install Your Node Application

When you pull down a repository of your Node application, you need to install all the dependencies before running your app. This is also the case for all the starter/completed codes we provide you with.

Let's simulate the process for installing a Node application.

Delete the node_modules folder. Then open the terminal and type the following command:

npm install

This will look up the dependencies in package.json file (and package-lock.json) and install them all.

You must now have node_modules folder rappeared in the root of your application (inside the express-appfolder).

Step 2: Hello Express

Here is a minimal Express application:

// index.js file
const express = require("express");
const app = express();
const port = 5000;

app.get("/", (req, res) => {
  res.send("Hello Express!");
});

app.listen(port, () => {
  console.log(`Express app listening at http://localhost:${port}`);
});

Go to terminal and run node index.js. You will see the following message printed in the terminal:

Express app listening at http://localhost:5000

While the Express app is running, open your browser and go to http://localhost:5000/. You will see the "Hello Express" message!

Recall: to stop the app, you must halt the process by pressing Ctrl + C.

The application we've built here with Express is identical to the one we had built earlier with Node's HTTP Module. In fact, Express is built on top of Node's HTTP module but it adds a ton of additional functionality to it.

Express provides a clean interface to build HTTP server applications. It provides many utilities, takes care of nuances under the hood, and allows for many other packages to be added as middleware to further the functionality of Express and your server application.

Dissecting the code

Let's take a closer look at our Express app:

const express = require("express");
const app = express();
const port = 5000;

app.get("/", (req, res) => {
  res.send("Hello Express!");
});

app.listen(port, () => {
  console.log(`Express app listening at http://localhost:${port}`);
});

We will explore it line by line:

  • The require("express") returns a function which we capture in the express variable.
  • This function returns an object which we capture in the app variable by invoking express().
  • The app object has methods for handling HTTP requests among other things.
  • The app.get() handles a HTTP Get request.
    • The first argument is a path, a string representing part of a URL that follows the domain name (see "Anatomy of a URL" for more information).
    • The second argument is a callback function. We'll talk about it shortly.
  • The app.listen() binds and listens for connections on the specified port. This method is identical to Node's http.Server.listen(). It optionally takes a second argument, a callback function, which I've used to simply print a message to the terminal.

Step 3: A very basic API

Let's recreate the simple API we've created earlier with the HTTP module but this time we will be using Express:

const express = require("express");
const app = express();
const port = 5000;

// sample schools
schools = [
  {
    Name: "School of Medicine",
  },
  {
    Name: "Whiting School of Engineering",
  },
];

// sample terms
terms = [
  {
    Name: "Fall 2020",
  },
  {
    Name: "Summer 2020",
  },
  {
    Name: "Spring 2020",
  },
];

// sample courses
courses = [
  {
    OfferingName: "EN.601.226",
    SectionName: "01",
    Title: "Data Structures",
    Instructors: "A. Madooei",
  },
  {
    OfferingName: "EN.601.226",
    SectionName: "02",
    Title: "Data Structures",
    Instructors: "A. Madooei",
  },
  {
    OfferingName: "EN.601.280",
    SectionName: "01",
    Title: "Full-Stack JavaScript",
    Instructors: "A. Madooei",
  },
  {
    OfferingName: "EN.601.280",
    SectionName: "02",
    Title: "Full-Stack JavaScript",
    Instructors: "A. Madooei",
  },
];

app.get("/", (req, res) => {
  res.send("Hello Express!");
});

app.get("/api/schools", (req, res) => {
  res.json(schools);
});

app.get("/api/terms", (req, res) => {
  res.json(terms);
});

app.get("/api/courses", (req, res) => {
  res.json(courses);
});

app.listen(port, () => {
  console.log(`Express app listening at http://localhost:${port}`);
});

Run the application and then head over to your browser and try the following endpoints:

Step 4: Routing

Routing refers to responding to client requests received over various endpoints (URLs).

In an Express app, routing generally looks like this:

app.METHOD(PATH, HANDLER)

Where:

  • app is an instance of express.
  • METHOD is an HTTP request method, in lowercase. For example, app.get() to handle GET requests and app.post to handle POST requests. Express supports methods that correspond to all HTTP verbs.
  • PATH is a "path" on the server. It typically appears after the domain name in a URL. For example, in https://sis.jhu.edu/api/classes/codes/schools the path is /api/classes/codes/schools.
  • HANDLER is the function executed when the route is "matched".

Let's focus on an example of routing:

app.get("/api/schools", (req, res) => {
  res.json(schools);
});

The Express app is listening on http://localhost:5000/. When a client goes to the URL http://localhost:5000/api/schools, the path /api/schools is matched with the above route and the following "handler" function is executed:

(req, res) => {
  res.json(schools);
}
  • The req is an object that represents the HTTP request. The Express app will build this object (based on the incoming requests) with properties for the request query string, parameters, body, HTTP headers, and so on.

  • The res is an object that represents the HTTP response. The Express app provides many properties so you can appropriately respond to an HTTP request. For example, res.json(schools) sends back the schools as a JSON object.

Note that we have not yet made any use of the req object. However, in earlier lectures, when we interacted with the SIS API, we've seen how for instance the API Key was passed as a query parameter along with the API endpoint. Similarly, if a client were to make the following Get request to our Express app:

http://localhost:5000/api/schools?key=pJnMRxKD1fFECXFRX1FDXykpLww0Lazk

We could get the (API) key and print it to the terminal as follows:

app.get("/api/schools", (req, res) => {
  console.log(req.query.key);
  res.json(schools);
});

Step 5: Web Application

We have deployed simple web apps to GitHub Pages. When a client visits the URL of our deployed app, they send an HTTP Get request to the GitHub server. The GitHub server in response sends the index.html file (and the script.js, style.css and any other files linked to index.html) to the client. The client's browser then renders the index.html and the client will see the app.

We can build our own server to serve clients with files and data pertaining to a web application.

Add the following index.html file to the express-app folder.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Express App</title>
</head>
<body>
  <h1>Hello Express!</h1>
</body>
</html>

Update the index.js file so when a client visits http://localhost:5000, they would receive the index.html file.

app.get("/", (req, res) => {
  res.sendFile(path.resolve(__dirname, "index.html"));
});

Note: to use the Path module you must import it

const path = require("path");

Run the Express app and then visit http://localhost:5000.

Step 6: Login

Let's add a login form to our Web application. To keep things simple, we only ask for username!

Update index.html by adding the following form element to it:

<form action="/dashboard">
  <label for="uname">username:</label>
  <input type="text" id="username" name="uname"><br><br>
  <input type="submit" value="Login">
</form>

Notice the action attribute of the form.

Save index.html and refresh http://localhost:5000/.

Go ahead an key in a username and then hit on the login button. What does happen?

The browser is directed to the http://localhost:5000/dashboard?uname=ali. There is no route in our Express app to serve this endpoint however.

Add the following to index.js

app.get("/dashboard", (req, res) => {
  res.send(`Welcome ${req.query.uname}`);
});

Reload the Express app. Go to http://localhost:5000/. Enter a username and press login. You will be redirected to the "dashboard" endpoint where a welcome message awaits you!

Step 7: The Dashboard!

Let's make an HTML page for the dashboard. Create dashboard.html with the following content:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Dashboard</title>
</head>
<body>
  <h1>Welcome USERNAME!</h1>
</body>
</html>

Update the route handler to serve this file:

app.get("/dashboard", (req, res) => {
  console.log(`Welcome ${req.query.uname}`);
  res.sendFile(path.resolve(__dirname, "dashboard.html"));
});

Rerun your Express app. Visit http://localhost:5000/. Enter a username and press login. You will be redirected to the "dashboard" endpoint!

The only issue now is to find a way to dynamically update the USERNAME (in dashboard.html) based on the request query parameter which we receive (req.query.uname).

Step 8: Template Engine

In order to dynamically update the dashboard.html we need a template engine. A template engine enables you to use static template files in your application.

At runtime, the template engine replaces variables in a template file with actual values, and transforms the template into an HTML file sent to the client.

There are many template engines out there! We will be using Nunjucks. It is specially designed to play well with JavaScript and that is the main reason we prefer it over alternatives.

Stop your Express app and add Nunjucks as a dependency to your application (i.e. install it using NPM):

npm install nunjucks

Let's add and configure Nunjucks in index.js. At the top of the file, add the following line:

const nunjucks = require("nunjucks");

Add the following after declaration of the app variable:

nunjucks.configure("views", {
  autoescape: true,
  express: app,
});

Create a new folder, views, inside the express-app folder. Create dashboard.njk file with the following content, inside the views folder:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Dashboard</title>
</head>
<body>
  <h1>Welcome {{ uname }}!</h1>
</body>
</html>

Notice this file is identical to dashboard.html except for the {{ uname }} in <h1> element. The {{}} is a Nunjucks syntax for looking up a variable's value from the template context. The template context must be provided in your route handler:

// index.js file
app.get("/dashboard", (req, res) => {
  let data = {
    uname: req.query.uname,
  };
  res.render("dashboard.njk", data);
});

Rerun your Express app. Visit http://localhost:5000/. Enter a username and press login. You will be redirected to the "dashboard" endpoint!

There is a lot more we can do with template engines. Essentially, we can build a full-fledged web application using Express, template engines, and a database. In future modules, we will further explore these possibilities.

Step 9: Serving Static Files

Let's add minimal styling to our application! Create the following style.css file and place it inside express-app/assets:

body {
  margin: 2em;
  text-align: center;
  line-height: 1.5;
  font-size: 1.1em;
}

Put a link to style.css in index.html and dashboard.njk:

<link rel="stylesheet" href="style.css">

Notice the href attribute does not include the folder assets in the CSS file path. We will have the Express to serve the content of assets folder as a static files.

Static files are files such as images, CSS files, and even JavaScript files which are shared among your HTML pages and do not change during runtime as a result of user interaction (hence, "static").

To serve static files, place the following statement somewhere after declaration of the app variable:

app.use(express.static("assets"));

Rerun your Express app. Visit http://localhost:5000/. You must see the look of the application changed!

Continuous Delivery with Heroku

Continuous Delivery (or Continuous Deployment, or CD) is a software engineering approach in which software functionalities are delivered frequently through automated deployments.

Heroku, a platform as a service (PaaS), is one of the most popular choices for deploying and running software applications. Heroku is primed for continuous delivery.1

Heroku, in a nutshell, is a cloud application platform that lets you deploy your server online. By taking care of most things related to deployment, it makes it easy to get your application up and running.

Before we get started, you must create a free Heroku account (sign up).

You may also install the Heroku CLI, although, we are going to use the Integration with GitHub tool for automatic deployement.

1

Read more about Continuous Delivery on Heroku at https://www.heroku.com/continuous-delivery.

Step 1

Login to your Heroku account and create a (free) Heroku app.

Give your app a name. Most names are already be taken! You must come up with a unique one.

Once the app is created, connect it to your GitHub Account.

You must now select the repository that holds the source code of your app.

App is connected now:

Enable Automatic Deploys.

From this point on, every time you push to main, it will be deployed to the Heroku.

Notice that GitHub has changed the default branch name from master to main about 11 days ago! (Read their announcement here). Heroku had updated their default branch for deployment (from master to main) before GitHub doing this. If you are wondering why, read this article: Regarding Git and Branch Naming.

Step 2

We must make two changes to our repository for Heroku to successfully deploy our application from it.

First, we must add a Procfile to the root of the repository with the following content

web: node ./index.js

A Procfile specifies the commands that are executed by the Heroku app on startup. Read more about it here.

Next, we must update the index.js as follows. Change this line

const port = 5000;

to the following:

const port = process.env.PORT || 5000;

In many environments (e.g. Heroku), and as a convention, you can set the environment variable PORT to tell your web server what port to listen on.

So process.env.PORT || 5000 means: use whatever is in the environment variable PORT as the port number, or 5000 if there's nothing there.

We need this change because Heroku will automatically assign a port to your application. It then stores the port number as a environment variable.

Finally, commit and push your changes. As the new changes get to your GitHub repository, Heroku will build and deploy the application. Give it a minute or two and then visit your your app.

Ours is deployed at https://express-app-1234.herokuapp.com/.

Deploy from Terminal

You don't need a GitHub repository to deploy an application to Heroku. There may be cases where you want to deploy a local development without creating a GitHub repository. This section teaches you how to use Heroku CLI for that.

First, download and install Heroku CLI by following this guideline.

Next, you must link your Heroku CLI to your Heroku account. This is done by loggin to your account through the command-line interface. Please follow the instructions here.

We are going to create a very simple Express app locally before deploying it to Heroku. Create a folder heroku-cli-demo. Open the terminal and change the directory to heroku-cli-demo folder. Run npm init -y to initiate a Node project. Follow this by running npm install express.

Heroku does not need GitHub but it does need Git! So, before going any further, create a .gitignore file inside heroku-cli-demo with the following content:

.DS_Store
.vscode/*
.idea/*
node_modules/*

Next, head over to the terminal and run the following commands:

git init
git add .
git commit -m "Initial Commit"

Create the index.js file inside heroku-cli-demo with the following content:

const express = require("express");
const app = express();
const port = process.env.PORT || 5000;

app.get("/", (req, res) => {
  res.send("Hello Heroku!")
});

app.listen(port, () => {
  console.log(`Express app listening at port: ${port}`);
});

Notice the statement const port = process.env.PORT || 5000; that allows us to use this script both locally and on Heroku by dynamically changing the port number.

Create the Procfile file inside heroku-cli-demo with the following content:

web: node ./index.js

Go to the terminal and run the following commands:

git add .
git commit -m "App is ready for deployment"

We can now deploy the Express app to Heroku with two simple commands!

First, create a Heroku App using CLI. In the terminal, run the following command:

heroku create

Here is the output on my computer

Creating app... done, ⬢ sleepy-ocean-40199
https://sleepy-ocean-40199.herokuapp.com/ | https://git.heroku.com/sleepy-ocean-40199.git

Notice an app has been created with a randomly generated name and it is now available at https://sleepy-ocean-40199.herokuapp.com/. The URL will be different for you.

Second, deploy the local repository to Heroku App. In the terminal, run the following command:

git push heroku master

This command pushes your code to the Heroku app and triggers a build followed by deployment. Here is the output on my computer

Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 12 threads
Compressing objects: 100% (7/7), done.
Writing objects: 100% (9/9), 5.22 KiB | 5.22 MiB/s, done.
Total 9 (delta 1), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote: 
remote: -----> Node.js app detected
remote:        
remote: -----> Creating runtime environment
remote:        
remote:        NPM_CONFIG_LOGLEVEL=error
remote:        NODE_ENV=production
remote:        NODE_MODULES_CACHE=true
remote:        NODE_VERBOSE=false
remote:        
remote: -----> Installing binaries
remote:        engines.node (package.json):  unspecified
remote:        engines.npm (package.json):   unspecified (use default)
remote:        
remote:        Resolving node version 12.x...
remote:        Downloading and installing node 12.19.0...
remote:        Using default npm version: 6.14.8
remote:        
remote: -----> Installing dependencies
remote:        Installing node modules
remote:        added 50 packages in 1.131s
remote:        
remote: -----> Build
remote:        
remote: -----> Caching build
remote:        - node_modules
remote:        
remote: -----> Pruning devDependencies
remote:        audited 50 packages in 0.79s
remote:        found 0 vulnerabilities
remote:        
remote:        
remote: -----> Build succeeded!
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote: 
remote: -----> Compressing...
remote:        Done: 22.9M
remote: -----> Launching...
remote:        Released v3
remote:        https://sleepy-ocean-40199.herokuapp.com/ deployed to Heroku
remote: 
remote: Verifying deploy... done.
To https://git.heroku.com/sleepy-ocean-40199.git
* [new branch]      master -> master

My app is now up and running at https://sleepy-ocean-40199.herokuapp.com/.

From this point on, every time I make a change to the local repository, I must commit those changes and then run the command git push heroku master to trigger a deployment on Heroku.

Note: you can rename the app from the randomly generated name to something else. The instruction are provided here.

YouNote API

We are going to build the API for a note taking application called YouNote.

In our API, users will be able to create notes, as well as read, update, and delete the notes they’ve created.

The source code for the completed app (which we will build in this module) can be found at this link.

Step 1

Create a folder younote-api. Open the terminal and change directory to your project folder. Run the command npm init -y. Then, install Express: npm install express.

Create a index.js file with the following content.

const express = require("express");
const app = express();
const port = 4567;


app.listen(port, () => {
  console.log(`Server is listening on http://localhost:${port}`);
});

You can run your Express app now (node index.js in the terminal).

Step 2

Create a folder model inside younote-api. Create the file Note.js inside the model folder, with the following content:

class Note {
  constructor(content, author) {
    this.content = content;
    this.author = author;
  }
}

module.exports = Note;

You can now go to index.js and import Note to make a sample object:

const Note = require("./model/Note.js");

const note = new Note("Sample Note", "Ali");
console.log(note);

Rerun the Express app and make sure the sample note is printed to the terminal.

Expected output
Note { content: 'Sample Note', author: 'Ali' }

Step 3

We will eventually store our notes in a database but for now let's simply keep them in an array.

Add the following to index.js:

const notes = [];
notes.push(new Note("Sample 1", "Author 1"));
notes.push(new Note("Sample 2", "Author 2"));
notes.push(new Note("Sample 3", "Author 3"));
console.log(notes);

Rerun the Express application again and ensure the samples are printed to the terminal.

Expected output
[
  Note { content: 'Sample 1', author: 'Author 1' },
  Note { content: 'Sample 2', author: 'Author 2' },
  Note { content: 'Sample 3', author: 'Author 3' }
]

It is going to be annoying if, during development, every time we change something in the code, we would need to stop the server and run it again.

Stop the application and install nodemon:

npm install -g nodemon

Nodemon is a utility that will monitor for any changes in your source and automatically restart your server.

I've decided to install it globally (the flag -g means install globally) since we will very likely use Nodemon for other projects.

After installation, instead of using node command, use nodemon to run your application:

nodemon index.js

Make a small change to your code and save the file. Notice the server is restarted automatically!

Step 4

As stated earlier, we will eventually use a database to add persistence to this application. For now, we will add a Data Access Object for Note.

In a nutshell, a Data Access Object is an object that provides an abstraction over some type of database or other persistence mechanism.

Create the NoteDao.js file inside the model folder with the following content:

class NoteDao {
  constructor() {
    this.notes = [];
  }
}

module.exports = NoteDao;

We shall provide methods for common database operations.

CRUD stands for create, read, update, and delete. It refers to the common tasks you want to carry out on database.

NoteDao is going to provide CRUD operation for notes.

Let's start with "create"; add the following method to NoteDao:

create(content, author) {
  this.notes.push(new Note(content, author));
}

To use Note, you must import it in NoteDao.js:

const Note = require("./Note.js");

Most databases will assign a unique ID to an object once it is created. We can simulate that here:

create(content, author) {
  const note = new Note(content, author);
  note._id = this.nextID();
  this.notes.push(note);
  return note;
}

where we declare nextID in the constructor

constructor() {
  this.notes = [];
  this.nextID = uniqueID();
}

Here is the implementation of uniqueID()

const uniqueID = function() {
  let id = 0;
  return function() {
    return id++;
  }
}

Notice the use of closure! Place uniqueID outside of the NoteDao class definition.

Step 5

Add the following method to NoteDao class:

readAll() {
  return this.notes;
}

The readAll method is like a getter method that returns all notes.

Let's update the index.js where we created sample notes previously to instead use NoteDao as follows:

const notes = new NoteDao();
notes.create("Sample 1", "Author 1");
notes.create("Sample 2", "Author 2");
notes.create("Sample 3", "Author 2");
notes.create("Sample 4", "Author 1");
console.log(notes.readAll());

As you save the changes to your file, you must see the sample notes printed to the terminal.

Expected output
[
  Note { content: 'Sample 1', author: 'Author 1', _id: 0 },
  Note { content: 'Sample 2', author: 'Author 2', _id: 1 },
  Note { content: 'Sample 3', author: 'Author 2', _id: 2 },
  Note { content: 'Sample 4', author: 'Author 1', _id: 3 }
]

It is common to search a database for a specific record/object using its unique identifier. We can add the following read method for that:

read(id) {
  return this.notes.find((note) => note._id === id);
}

It is also common to search for database records given one or more attributes. We can allow clients to search for notes given their author or content attributes.

Let's update the readAll method to have an optional parameter author:

readAll(author = "") {
  if (author !== "") {
    return this.notes.filter((note) => note.author === author);
  }
  return this.notes;
}

To test our application, update the index.js where we created sample notes previously to read (and return) notes given an id or their author:

const notes = new NoteDao();
notes.create("Sample 1", "Author 1");
notes.create("Sample 2", "Author 2");
notes.create("Sample 3", "Author 2");
notes.create("Sample 4", "Author 1");
console.log(notes.read(2));
console.log(notes.readAll("Author 1"));
console.log(notes.readAll());

As you save the changes to your file, you must see the sample notes printed to the terminal.

Expected output
Note { content: 'Sample 3', author: 'Author 2', _id: 2 }
[
  Note { content: 'Sample 1', author: 'Author 1', _id: 0 },
  Note { content: 'Sample 4', author: 'Author 1', _id: 3 }
]
[
  Note { content: 'Sample 1', author: 'Author 1', _id: 0 },
  Note { content: 'Sample 2', author: 'Author 2', _id: 1 },
  Note { content: 'Sample 3', author: 'Author 2', _id: 2 },
  Note { content: 'Sample 4', author: 'Author 1', _id: 3 }
]

Step 6

Let's implement a method to "update" a note given its ID:

update(id, content, author) {
  const index = this.notes.findIndex((note) => note._id === id);
  if (index !== -1) {
    this.notes[index].content = content;
    this.notes[index].author = author;
    return this.notes[index];
  }
  return null;
}

Let's also provide an operation to "delete" a note given its ID:

delete(id) {
  const index = this.notes.findIndex((note) => note._id === id);
  if (index !== -1) {
    const note = this.notes[index];
    this.notes.splice(index, 1);
    return note;
  }
  return null;
}

To test our application, update the index.js where we created sample notes previously to update/delete notes given their id:

const notes = new NoteDao();
notes.create("Sample 1", "Author 1");
notes.create("Sample 2", "Author 2");
notes.create("Sample 3", "Author 2");
notes.create("Sample 4", "Author 1");
console.log(notes.read(2));
console.log(notes.readAll("Author 1"));
console.log(notes.readAll());
console.log(notes.update(0, "Sample 0", "Author 3"));
console.log(notes.delete(1));
console.log(notes.readAll());

As you save the changes to your file, you must see the sample notes printed to the terminal.

Expected output
Note { content: 'Sample 3', author: 'Author 2', _id: 2 }
[
  Note { content: 'Sample 1', author: 'Author 1', _id: 0 },
  Note { content: 'Sample 4', author: 'Author 1', _id: 3 }
]
[
  Note { content: 'Sample 1', author: 'Author 1', _id: 0 },
  Note { content: 'Sample 2', author: 'Author 2', _id: 1 },
  Note { content: 'Sample 3', author: 'Author 2', _id: 2 },
  Note { content: 'Sample 4', author: 'Author 1', _id: 3 }
]
Note { content: 'Sample 0', author: 'Author 3', _id: 0 }
Note { content: 'Sample 2', author: 'Author 2', _id: 1 }
[
  Note { content: 'Sample 0', author: 'Author 3', _id: 0 },
  Note { content: 'Sample 3', author: 'Author 2', _id: 2 },
  Note { content: 'Sample 4', author: 'Author 1', _id: 3 }
]

Now that we know the NoteDao works fine, delete all the console.log message in index.js and leave it as creating four sample notes.

Step 7

We are ready to build our API.

Recall the following HTTP verbs (operations):

  • POST: to create a resource
  • PUT: to update it
  • GET: to read it
  • DELETE: to delete it

We will start with HTTP GET requests.

Let's document our HTTP requests as we implement them:

Read Notes
HTTP MethodGET
API Endpoint/api/notes
Request Path Parameter
Request Query Parameter
Request Body
Response BodyJSON array of notes
Response Status200

What is Response Status?

The response status is a "code" (number) returned to the client that signals the success/failure of their request. Here are the common status codes and their meaning:

StatusMeaning
200 (OK)This is the standard response for successful HTTP requests.
201 (CREATED)This is the standard response for an HTTP request that resulted in an item being successfully created.
204 (NO CONTENT)This is the standard response for successful HTTP requests, where nothing is being returned in the response body.
400 (BAD REQUEST)The request cannot be processed because of bad request syntax, excessive size, or another client error.
403 (FORBIDDEN)The client does not have permission to access this resource.
404 (NOT FOUND)The resource could not be found at this time. It is possible it was deleted, or does not exist yet.
500 (INTERNAL SERVER ERROR)The generic answer for an unexpected failure if there is no more specific information available.

Add this route to index.js

app.get("/api/notes", (req, res) => {
  res.json(notes.readAll());
});

As you save the code, nodemon reruns the Express application. Open your browser and head over to http://localhost:4567/api/notes. You must receive a JSON array containing our 4 sample notes.

You can also hit this endpoint on Postman to test it.

Step 8

Here is another GET request.

Read Notes (filter by author)
HTTP MethodGET
API Endpoint/api/notes
Request Path Parameter
Request Query Parameterauthor
Request Body
Response BodyJSON array of notes
Response Status200

For this one, we expect a client to use the same endpoint as before but optionally provide an author name as a query parameter. We can update the route we already have (/api/notes) to account for this scenario:

app.get("/api/notes", (req, res) => {
  const author = req.query.author;
  res.json(notes.readAll(author));
});

Save the code then head over to your browser and visit http://localhost:4567/api/notes?author=Author 2. You must receive a JSON array containing two sample notes, written by "Author 2".

Test this endpoint in Postman too.

Step 9

Let's create a route to receive a specific note given its ID.

Read Note
HTTP MethodGET
API Endpoint/api/notes/:noteId
Request Path ParameternoteId
Request Query Parameter
Request Body
Response BodyJSON object (note)
Response Status200

Notice the API endpoint; to retrieve an item from a collection, it is common to use an endpoint /api/collection/:id where id is the ID of the item you are searching for.

Add the following route to index.js:

app.get("/api/notes/:id", (req, res) => {
  const id = Number.parseInt(req.params.id);
  res.json(notes.read(id));
});

Notice how the path contains :id and how I have used req.params object to get the path parameter (as opposed to using req.query for getting query parameters).

If you want to identify a resource, you should use "path parameter". But if you want to sort or filter items, then you should use query parameter. Source: When Should You Use Path Variable and Query Parameter?

Save the code then head over to your browser and visit http://localhost:4567/api/notes/2. You must receive a JSON object containing the note with ID of 2.

Test this endpoint in Postman too.

Step 10

Let's include a route to create a note.

Create Note
HTTP MethodPOST
API Endpoint/api/notes
Request Path Parameter
Request Query Parameter
Request BodyJSON object (note attributes)
Response BodyJSON object (created note)
Response Status201

Notes:

  • We use a POST method to create a note
  • The path (endpoint) is similar to the GET request we had earlier for receiving all notes. You can think about it as we are going to post to the collection of notes here (whereas before we were going to read the collection of notes).
  • We need to provide the attributes (content & author) for creating the note. This attribute will be provided in the "body" (payload) of the request (we will see soon what that means!)
  • Once the note is created, we will return the newly created resource. This is just a common pattern in the design of APIs.
  • We send a status code of 201 to signal the resource creation succeeded.

Add the following route to index.js:

app.post("/api/notes", (req, res) => {
  const content = req.body.content;
  const author = req.body.author;
  const note = notes.create(content, author);
  res.status(201).json(note);
});

Notice how I have set the status code.

Also, notice how I have used req.body object to get the attributes of the note. The client is expected to provide these attributes as a JSON data in the request body. To allow Express parse the request body, we must add the following line somewhere in index.js (after app is declared):

app.use(express.json());

You will not be able to test a POST request in your browser (where you typically write URLs is for GET requests). You must test this endpoint in Postman.

Step 11

We need to provide input validation when designing our API. In particular when we create, update, or delete resources.

We can perform input validation inside our route handler or delegate that to NoteDao. I've gone with the latter; update NoteDao.create method:

create(content, author) {
  if (!content || !author) {
    throw new Error("Invalid attributes");
  }
  const note = new Note(content, author);
  note._id = this.nextID();
  this.notes.push(note);
  return note;
}

If either content or author are falsy values (null, undefined, or empty string), and error will be thrown.

Update the route handler in index.js to include error handling:

app.post("/api/notes", (req, res) => {
  const content = req.body.content;
  const author = req.body.author;

  try {
    const note = notes.create(content, author);
    res.status(201).json(note);
  } catch (error) {
    res.status(400).send(error.message);
  }
});

Notice I set the status code to 400 before sending the error message.

In Postman, test the input validation by sending a request to api/notes endpoint with insufficient or invalid attributes.

Step 12

Let's include a route to delete a note.

Delete Note
HTTP MethodDELETE
API Endpoint/api/notes/:noteId
Request Path ParameternoteId
Request Query Parameter
Request Body
Response BodyJSON object (note)
Response Status200

Notice the path (endpoint) is similar to the GET request we had earlier for retrieving a note given its ID. By convention, we return the deleted note (with status code 200).

Add the following route to index.js:

app.delete("/api/notes/:id", (req, res) => {
  const id = Number.parseInt(req.params.id);
  const note = notes.delete(id);
  if (note) {
    res.json(note);
  } else {
    res.status(404).send("Resource not found!");
  }
});

Notice that if the given ID does not exist in the notes collection, the NoteDao returns null. I've accordingly respond with a 404 status to signal "resource was not found".

In Postman, test this endpoint by attempting to delete a note that exist.

Also, test by requesting to delete a note that does not exist.

Step 13

Let's include a route to update a note.

Update Note
HTTP MethodPUT
API Endpoint/api/notes/:noteId
Request Path ParameternoteId
Request Query Parameter
Request BodyJSON object (note attributes)
Response BodyJSON object (updated note)
Response Status200

Similar to how we updated NoteDao.create to provide input validation, we must update NoteDao.update method:

update(id, content, author) {
  if (!content || !author) {
    throw new Error("Invalid attributes");
  }
  
  const index = this.notes.findIndex((note) => note._id === id);
  if (index !== -1) {
    this.notes[index].content = content;
    this.notes[index].author = author;
    return this.notes[index];
  }
  return null;
}

Add the following route to index.js:

app.put("/api/notes/:id", (req, res) => {
  const id = Number.parseInt(req.params.id);
  const content = req.body.content;
  const author = req.body.author;

  try {
    const note = notes.update(id, content, author);
    if (note) {
      res.json(note);
    } else {
      res.status(404).send("Resource not found!");
    }
  } catch (error) {
    res.status(400).send(error.message);
  }
});

Notice I set two error status code: (400) for invalid request and (404) for invalid ID number (resource not found).

In Postman, test this endpoint by attempting to update a note that exist.

Also, test by requesting to update a note that does not exist.

Finally, test the endpoint with insufficient or invalid attributes.

Step 14

We are almost done with the YouNote API. It needs some polishing (refactoring the code, incorporating some best practices, etc) which we will do in the next module.

Until then, I recommend reading the following short reads:

Refactoring YouNote API

In his classic book, Refactoring, Martin Fowler defines refactoring as

The process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure.

Refactoring is the art of making design changes to keep the software fit for its purpose and ready for more changes. In this module, we will refactor and polish our YouNote API.

The source code for the completed app (which we will build in this module) can be found at this link.

Step 1

It is always good practice to write tests, but perhaps essential before refactoring our code. Having tests enables us to continuously test the code as we refactor it, to ensure the changes we make don't break the expected behavior.

For testing APIs, we can use Postman. In its simplest form, we can save the API requests, group them in "collections" and run them at a later time. This arrangement would serve similar to having a suite of tests.

You can do a lot more with Postman, such as Automated Testing and API Monitoring. Please refer to their documentation for more details.

For this lecture, ensure you have all the following API requests "saved" in a "collection". To create a collection, simply click on + New Collection and give it a name.

To save an API (HTTP) Request in Postman, simply press Command + S (or Ctrl + S if on Windows), give it a name and select which collection it should be saved to.

Here are the API requests we have made in the previous module, as we have developed YouNote API:

Get all notes

Get notes by a given author

Get a note given its ID

Create a note

Delete a note given its ID

Delete note (invalid ID)

Update note

Update note (invalid ID)

Update note (invalid attributes)

A nice feature in Postman is that you can "run" all the requests saved in a collection (which is similar to running all your "tests"), and you can share your collection with others, in various forms! I have, for instance, exported the collection into a JSON file and you can download it from here and import it to your Postman application.

To import the collection, click on the big Import button on the top left side of the Postman application.

Step 2

The index.js file for the YouNote API contains several routing methods related to CRUD operations for notes. It is perceivable that we will have other resources/entities, such as users, in this application. It will likely be the case that we will need several other routing methods for CRUD operations for other resources.

Express has a Router object that enables you to move routing logic into a separate file. We will use the express router to refactor routing code.

Create a folder routes inside the younote-api folder. The files in this folder will contain all of our routes and we will export them using the express router.

Create a file notes.js inside routes folder. Copy and paste the content of index.js into notes.js. We will now make the following updates:

  1. Remove the following statements

    - const port = 4567;
    
    - app.use(express.json());
    
    - app.listen(port, () => {
    -    console.log(`Server is listening on http://localhost:${port}`);
    - });
    
  2. Update the path to NoteDao

    - const NoteDao = require("./model/NoteDao.js");
    + const NoteDao = require("../model/NoteDao.js");
    
  3. Make the following update

    - const app = express();
    + const app = express.Router();
    

    The Router() returns an an object with similar HTTP methods (.get, .post, .put, .delete, etc) to the app object we have previously been using.

  4. Rename app variable to router (you must apply this consistently to all instances of app in note.js file). This renaming is not a "must"; it is a good name choice though.

  5. Add the following statement to the end of note.js file

    module.exports = router;
    
  6. Update index.js to simply contain the following:

    const express = require("express");
    const noteRoutes = require("./routes/notes.js");
    const app = express();
    const port = process.env.PORT || 4567;
    
    app.use(express.json());
    app.use(noteRoutes);
    
    app.listen(port, () => {
      console.log(`Server is listening on http://localhost:${port}`);
    });
    

    The routes related to "notes" are now encapsulated within noteRoutes object and the express app is told to "use" it. Notice that I've also updated the declaration of port with somewhat forward thinking for when we deploy this application on Heroku.

Run the API server and run the API requests in Postman to ensure this refactoring has not broken our API's behavior.

Step 3

Phillip Hauer has a great blog post API Design Best Practices. We have already employed some and we will employ two more here (and a few other practice in later modules).

One of the recommendations is "Wrap the actual data in a data field". Let's refactor routes/note.js to incorporate this practice:

Find every instance of res.json() and update it to wrap its argument in a data object as in the example below:

- res.json(note);
+ res.json({ data: note });

Once done, save the file and rerun the API requests in Postman. Here is an example for retrieving a note given its ID.

Similarly, we are going to wrap error messages in an errors object. In the blog post, this recommendation is made under "Provide Useful Error Messages". Here is an example:

- res.status(404).send("Resource not found!");
+ res.status(404).json({
+  errors: [
+    {
+      status: 404,
+      detail: "Resource not found!",
+    },
+  ],
+ });

You must update all instances where the response entailed an "error" to adhere to the pattern above.

Once done, save the file and rerun the API requests in Postman. Here is an example for attempting to delete a note given an invalid ID.

Step 4

The routes/note.js file contains the following statement which generates sample data. This sample data generation is not a responsibility of a route handler. We need to refactor and extract this segment into its own file (module).

Create a folder called data and add a file notes.js to it, with he following content:

function addSampleNotes(notes) {
  notes.create("Sample 1", "Author 1");
  notes.create("Sample 2", "Author 2");
  notes.create("Sample 3", "Author 2");
  notes.create("Sample 4", "Author 1");
}

module.exports = { addSampleNotes };

Update the routes/note.js file:

const express = require("express");
+ const { addSampleNotes } = require("../data/notes.js");
const router = express.Router();

const notes = new NoteDao();
- notes.create("Sample 1", "Author 1");
- notes.create("Sample 2", "Author 2");
- notes.create("Sample 3", "Author 2");
- notes.create("Sample 4", "Author 1");
+ addSampleNotes(notes);

Save the files and run the API requests in Postman to ensure this refactoring has not broken our API's behavior.

Now stop the API server! We are going to install a package that assists us with generating more realistic fake data! Run the following command in the terminal.

npm install faker

The faker package can generate fake users, images, content, etc.

Update data/notes.js:

const faker = require("faker");

const NUM_SAMPLES = 4;

function addSampleNotes(notes) {
  for (let i = 0; i < NUM_SAMPLES; i++) {
    notes.create(faker.lorem.paragraphs(), faker.name.findName());
  }
}

module.exports = { addSampleNotes };

Run the API server and run the API requests in Postman to ensure this refactoring has not broken our API's behavior. You may need to update some of the requests; for instance, the author name "Author 2" is not among the sample data anymore so a request to retrieve notes written by this author will return an empty array object.

Note the drawback of using a package like faker is that the randomness involved in generating fake data makes it difficult to test the API.

We can also use faker's random "unique ID" generator to assign unique IDs to notes; it will be more realistic to what a database does compared to our auto-incrementing ID feature currently in place in NoteDao.

Update the NoteDao file as follows:

  1. import faker

    const faker = require("faker");
    
  2. Use it to assign random unique IDs:

    - note._id = this.nextID();
    + note._id = faker.random.uuid();
    
  3. You don't need the uniqueID function, so delete it!

  4. You don't need this statement in the constructor, so delete it:

    - this.nextID = uniqueID();
    

Open the routes/notes.js file and remove the following casting (since IDs are not integers anymore)

- const id = Number.parseInt(req.params.id);
+ const id = req.params.id;

Save the files and run the API requests in Postman to ensure this refactoring has not broken our API's behavior. You will need to update many of the requests; for instance, every request that contained a "note id" request parameter needs to be updated as we no longer have integer IDs.

Step 5

Recall the issue with CORS (Cross Origin Resource Sharing) when we were working with the SIS API. The browser blocked our access to the SIS API due to CORS policy. We needed to install a plugin to get around this issue. At the time, I've noted this is an issue on SIS API's side. They must update their responses to include Access-Control-Allow-Origin: * in their HTTP header to allow requests from all domains. This will be the case for our YouNote API as well. Since the intention is to build a "public" API, we must allow cross domain requests.

The fix happens to be very simple, as long as we are working with NodeJS & Express. To understand how the fix works, go to Postman, run any of the requests, and notice in the response, there are 6 "header" attributes.

Now, stop the API server and install the Node package cors

npm install cors

Next, update the index.js file by

  1. Importing cors

    const cors = require("cors");
    
  2. Linking it to express

    app.use(cors());
    

That's it! Run the server again and run any of the API requests in Postman. Make note of the response header attributes:

The cors package adds this Access-Control-Allow-Origin: * to the header of every response we send back to the client. This information informs applications such browsers that we allow cross domain requests.

The cors package can take many options to e.g. allow requests from certain domains (but not all). There are many resources online if you are interested to learn more about this. This short video could be a good starting point: Node API - CORS, what is it and how to use it on YouTube.

Step 6

We have allowed cross domain requests to our API. This is needed however it makes our server more vulnerable to various security risks. We can get help from another Node package called helmet to compensate for this. Helmet can protect our API from some well-known web vulnerabilities by setting HTTP headers appropriately.

To use helmet, stop the API server and install the it:

npm install helmet

Next, update the index.js file by

  1. Importing helmet

    const helmet = require("helmet");
    
  2. Linking it to express

    app.use(helmet());
    

That's it! Run the server again and run any of the API requests in Postman. Make note of the response header attributes:

It is beyond the scope of this course to get into the details of what these headers mean and what they do. If you are interested, a good starting point is this short YouTube video Secure ExpressJS Application With Helmet. I also recommend watching this (longer) YouTube video Information Security with HelmetJS with FreeCodeCamp by Dylan Israel.

Step 7

Look at these four lines from index.js file:

app.use(cors());
app.use(helmet());
app.use(express.json());
app.use(noteRoutes);

The pattern of app.use(something) is how Express employs middleware functions.

Middleware functions are functions that have access to the request object (req) and the response object (res), and they can modify application’s request-response cycle.

For example, cors and helmet add "headers" to all our responses before they are actually sent to client. The express.json reads all the requests and if they contain JSON payload, it parses the JSON and makes it available to us through req.body object.

There are many middleware available for Express and you can make your own. The best place to start is "Using middleware" on Express official Guidelines. You can also look at Express middleware under Resources at expressjs.com for a list of popular middleware functions. We have already used a few but let's add another one:

Stop the API server and install morgan

npm install morgan

Next, update the index.js file by

  1. Importing morgan

    const morgan = require("morgan");
    
  2. Linking it to express

    app.use(morgan("dev"));
    

We use morgan to log HTTP requests. As you interact with the API server, you can look at the console (terminal) for the server and see the requests that are received and served by the server. Morgan generated color-coded output based on response status for development use. For example, output will be colored green for success codes, red for server error codes, yellow for client error codes, cyan for redirection codes, etc.

Run the server again and run some of the API requests in Postman. Make note of console as the server is running:

MongoDB

Adding persistence to application means storing data so that it persists beyond the lifetime of the application.

The easiest way to achieve this is perhaps to save data to (plain text) files. For simple applications, it is fine to persist data directly to disk as text files. However, when building larger applications, in particular for use by many users, file-based persistence can cause problems:

  • sharing can cause data loss or lead to security problems
  • having multiple files can result in data redundancy and inconsistency
  • querying files is difficult in particular in case of concurrent access

The solution is to use a database (together with a Database Management System).

A database is a shared collection of related data.

Database Management Systems (DBMS) provide a convenient environment to create, secure and maintain databases.

DBMS provides an API for users to (efficiently) retrieve and store information from/to database.

We will be using a database and DBMS application called MongoDB.

MongoDB is a cross-platform document-oriented database program.

MongoDB uses JSON-like documents with optional schemas. It plays well with JavaScript and Node applications. Its simplicity (compared to traditional relational databases) speeds up application development and reduces the complexity of deployments.

You can run MongoDB locally or in the cloud. When you deploy your application you will need a cloud database. The developers of MongoDB have created an online application called MongoDB Atlas, or simply Atlas, that assists you with handling the complexity of deploying and managing your MongoDB instance on popular cloud service provides such as AWS, Google, and Azure.

We are going to work with MongoDB Atlas to provision a cloud database which we will use on our local computer as well as when our application is deployed.

The source code for the completed demo app (which we will build in this module) can be found at this link.

Step 1

There is the official Get Started with Atlas document that I have found to be unfortunately not very helpful for first time users. I've made an effort here to document the "getting started" process more closely. In particular, there are certain things you need to do once, to get set up, and other things which you will do every time you need a cloud database.

Get started with Atlas

Go to https://www.mongodb.com/cloud/atlas.

Click on "Start free" and create an account.

You will be redirected to the following page. "Skip" this step.

When asked to "Choose a path", select "Shared Clusters" and click on "Create a cluster".

You will be taken to the following view, get yourself out of it by clicking on the "clusters" link (top left as highlighted in the image).

After clicking on "clusters" you will be taken to the following view:

Notice on the top left (above image), the "organization" and "project" are set to default names generated by Atlas. You can change (rename) these or make new ones as you wish. To rename, access their settings (gear/ellipsis icon, as highlighted in the image).

There is no need for you to change "organization" and/or "project" names. That said, I have renamed my organization to "CS280" and my project to "YouNote".

Step 2

Database Access

We now need to create at least one "user" and provide it with permissions to connect to our database. Here, user means the developer (i.e. you) not the end-user of your product. You can create multiple users with different functional role/permissions.

Under the security tab, click on "Database Access".

Then click on "Add New Database User". You will be presented with the following view.

Select the following options:

  • Authentication Method: Password
  • Database user privilege: Atlas admin

I've chosen the username younote-admin and autogenerated a secure password.

Click on "Add User". It will take a moment for the new setting to be put in place. You will eventually see the following view:

Network Access

As an extra layer of security, you can restrict access to your cloud database by selectively allowing/prohibiting certain IP addresses. We are going to allow all IPs! This does not mean that everyone will have access to our database! Only the database "users" will have access (and they need to be authenticated using their password).

Under the security tab, click on "Network Access".

Then click on "Add IP Address". You will see the prompt below:

Click on "ALLOW ACCESS FROM ANYWHERE". Notice once you click on that option, the "Access List Entry" will be set to 0.0.0.0/0.

Confirm your selection. It will take a moment for the new setting to be put in place. You will eventually see the following view:

Step 3

Build a Cluster

Each MongoDB deployment that is managed by Atlas is called a "cluster".

We will create a "starter" cluster which is free. The free tier clusters are enough for small applications.

Under "Data Storage", click on "Clusters" and then click on the green button that says "Build a Cluster".

You will be taken back to this page:

Select "Create Cluster". You will see the following page:

Notice you have the option to choose between AWS, Google Cloud or Azure. All of them have free and paid tiers.

I will keep the following "recommended" options:

  • Cloud Provider: AWS
  • Region: N. Virginia (us-east-1)
  • Cluster Tier: M0 Sandbox (Shared RAM, 512 MB Storage).
    • This is the free forever tier.
  • Additional Settings: MongoDB 4.2, No Backup.

Note the cluster name by default is Cluster0. You can change this now. However, once your cluster is created, you won't be able to change its name. I will change the cluster name to younote-api.

It may take a minute or two for the cluster to be created. Once it is done you should be greeted with the following screen.

We are ready to connect our YouNote API to the MongoDB cluster in the cloud.

Step 4

Before connecting the YouNote API to the MongoDB cluster in the cloud, we will make a demo app to get familiar and practice working with MongoDB from a Node application.

Create a folder mongo-demo. Open the terminal and change directory to mongo-demo. Initiate a Node application:

npm init -y

There is an official MongoDB driver for NodeJS. It provides an API to work with MongoDB from within Node applications. We, however, are going to use another Node package called Mongoose to work with MongoDB.

Mongoose is an object document mapping (ODM) that sits on top of Node's MongoDB driver.

It is not required to use Mongoose with MongoDB but there are advantages in doing so (see e.g. Top 4 reasons to Use Mongoose with MongoDB).

Install mongoose and while at it, install faker as well so we can populate our database with some fake data!

npm install mongoose faker

Create an index.js file:

const mongoose = require("mongoose");

console.log(mongoose);

Save the file and run it

node index.js

Notice the mongoose object printed to the terminal. We will be using some of the functions attached to this object.

Step 5

Let's connect our demo Node app to the MongoDB cluster we have created on Atlas.

Go to MongoDB Atlas website, find your cluster and click on the "Connect" button.

You will be presented with the following prompt:

Select "Connect your application". You will see the following view:

Click on the copy button and past the value in the index.js file:

const URI = `mongodb+srv://younote-admin:<password>@younote-api.cwpws.mongodb.net/<dbname>?retryWrites=true&w=majority`

This is a Uniform Resource Identifier (URI) (similar to URL for accessing websites). The URI will allow you to connect to the MongoDB cluster in the cloud. Notice, within the URI, there is <password> and <dbname>. We need to update these:

const password = "not-shown-here";
const dbname = "younote-db";
const URI = `mongodb+srv://younote-admin:${password}@younote-api.cwpws.mongodb.net/${dbname}?retryWrites=true&w=majority`

You can choose any value for dbname (database name); I've settled on younote-db.

As for the password, it is the password we had Atlas generate for our younote-admin user. To get the password, go to "Database Access" under "Security" and then click on "Edit" button as depicted below:

In the "Edit User" prompt, click on "Edit Password".

You will see the following view; click on the "Copy" button.

Finally, paste the copied password into the index.js file (in the code snippet above, it is where it says not-shown-here).

It is a bad practice to include sensitive information like password inside source code. We will fix this soon!

Step 6

We must remove the password from within the source code. A common practice is to store sensitive information such as passwords as "environment variables". Then, we can have the node application read that environment variable.

Update the index.js file:

- const password = "not-shown-here";
+ const password = process.env.DB_ADMIN_PASSWORD;

Now we must create an environment variable DB_ADMIN_PASSWORD and store the password in that variable. The process of creating environment variables varies in different environments (e.g. Mac vs. Windows). There is a Node package that makes this as easy as defining variables in a text file.

Install the dotenv package:

npm install dotenv

Create a .env file inside mongo-demo folder. Open the file and add the following:

DB_ADMIN_PASSWORD="your-password-goes-here"

Add the following to the top of index.js

require("dotenv").config();

To ensure this works as intended, you can add console.log(password) to index.js and run it.

You must exclude (ignore) the .env file from your git repository.

Step 7

Add the following to index.js

const option = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
};

mongoose
  .connect(URI, option)
  .then(() => {
    console.log("Connected to MongoDB!");
  })
  .catch((err) => {
    console.log(err);
  });

We are using the connect method on mongoose object to establish a connection to our MongoDB cluster in the cloud.

The connect method takes two arguments and returns a Promise. The arguments are the database URI and an option object. You can read about available option parameters, in particular those that are important for tuning Mongoose, here (link opens to mongoose documentation).

Once the Promise returned by the connect method resolves, we print the message "Connected to MongoDB!". If the attempt to connect fails for any reason, the error will be printed to the terminal.

Run the demo application; if you have followed all the steps correctly, it will work and you will see "Connected to MongoDB!" printed in the terminal.

Mongoose emits events when e.g. a connection is established (or failed to establish due to an error). So, another pattern for working with Mongoose is the following that uses the event listeners on connection object:

mongoose.connect(URI, option);

mongoose.connection.on("open", () => {
  console.log("Connected to MongoDB!");
});

mongoose.connection.on("error", (err) => {
  console.log(err);
});

Run the demo application to check that this alternative approach works too. It might be a good idea to deliberately provide mongoose with e.g. wrong password and check that connection fails.

Step 8

We have connected our demo application to our MongoDB cluster in the cloud. Let's store some data! Before doing so, we need to understand how MongoBD organizes data.

A Mongo Database is made up of collections and documents.

A collection represents a single entity in your app. For example in our YouNote app, we have "notes" and we may later add other entities like "users". Each of such entities will be a single collection in our database.

A collection contains documents. A document is an instance of the entity represented by the collection. For example, a collection called notes will have documents each representing a note. Each "note" document would contain the attributes that make up a note (such as "content", "author", etc). A document in MongoDB looks a lot like a JSON object.

If you have never worked with a database, it may be helpful to think about collections as folders and documents as files organized inside the folders.

If you have experience working with databases, it most likely has been with a Relational database. (For those never heard of it, you can think of relational databases like spreadsheets where data is structured in tables and each entry is a row in a table.) MongoDB is not a relational database although you can think of collections as tables and documents as the rows (records) in a table.

For this demo application, we will make a collection of "users". Each user will have a "username" and a "password". As the first step, we will define a schema for "user" documents. According to MongoDB documentation:

A document schema is a JSON object that allows you to define the shape and content of documents embedded in a collection. You can use a schema to require a specific set of fields, configure the content of a field, or to validate changes to a document based on its beginning and ending states.

Add the following to index.js:

const Schema = mongoose.Schema;

const UserSchema = new Schema({
  username: { type: String },
  password: { type: String },
});

We are using the Schema class from Mongoose to create a document schema for users. The code snippet above essentially establishes that a user will have a username and a password and both of these attributes are going to be of type String.

Next, we create a model:

const User = mongoose.model("User", UserSchema);

Notice the first argument to mongoose.model is the singular name for the entity your model is for. Mongoose automatically looks for the plural, lowercased version of your model name as the collection that holds documents of that model. Therefore, the model "User" will result in construction of a users collection in the database.

Models are one of the constructs which Mongoose brings to the table. You can link a document schema to a model, then use the model to construct documents, as shown in the example below:

User.create(
  {
    username: faker.internet.userName(),
    password: faker.internet.password(),
  },
  (err, user) => {
    if (err) console.log(err);
    console.log(user);
  }
);
Aside: Faker package

We use the faker package to generate fake (but realistic) username and password! Don't forget to import the faker package

const faker = require("faker");

The create method attached to a Mongoose model allows you to create documents of that model. It takes two arguments: an object representing the data to be saved in the database, and a callback function. The create method invokes the callback function by passing two arguments to it: an error (which will be null if there was no error) and the document created (which will be null if there was an error).

Save the index.js file and run it. If all goes well, you will get the user document (which looks like a JSON) printed to the terminal.

Once the data is printed to the terminal, head over to MongoDB Atlas website. Find your cluster and click on the "Collections" button.

You must be able to find the user we have just created!

Step 9

The create method attached to a Mongoose model is, as you expect, an async operation. We embraced the callback pattern to deal with it. We can try the alternative patterns:

Promise Pattern: rewrite the User.create(...) as follows.

User.create({
  username: faker.internet.userName(),
  password: faker.internet.password(),
})
  .then((user) => {
    console.log(user);
  })
  .catch((err) => {
    console.log(err);
  });

Save index.js and run the application. A new user document must be printed to the terminal. Go to Atlas website and click on "Refresh" button to see the new user added to the database.

Async/Await Pattern: rewrite the User.create(...) as follows.

async function createUser(username, password) {
  try {
    const user = await User.create({
      username,
      password,
    });
    console.log(user);
  } catch (err) {
    console.log(err);
  }
}

createUser(faker.internet.userName(), faker.internet.password());

Save index.js and run the application. A new user document must be printed to the terminal. Go to Atlas website and click on "Refresh" button to see the new user added to the database.

The data is randomly created so your users will have different username and password compared to what you see in the image here.

Step 10

A Mongoose Model is a class. You can use it to construct objects using the new operator:

const user = new User({
  username: faker.internet.userName(),
  password: faker.internet.password(),
});

console.log(user);

You can then "save" the object in the database (which is connected to mongoose):

user.save((err, user) => {
  if (err) console.log(err);
  console.log(user);
});

So the Mongoose Model can be used to create objects of certain type in your application, as well as to connect to the database and handle the persistence mechanism.

Aside: I take issue with the design of Mongoose; it seems to violate the "Single Responsibility Principle" as the "data model" is fused with "data access object". At any rate, this is how Mongoose models work. In fact, there are several other methods attached to the "model" that allows you to perform various database queries.

For example, to retrieve all the "users" you can use the find method:

User.find()
  .then((users) => console.log(users))
  .catch((err) => console.log(err));

Notice find is invoked through User the model we created for constructing user documents (not the object user).

To find a specific user given their ID, you can use findById:

User.findById("5f9a1d156e848f06c458f5f4")
  .then((user) => console.log(user))
  .catch((err) => console.log(err));

The 5f9a1d156e848f06c458f5f4 is an ID of one of the users in my database. You must use an ID for one of your user documents.

Step 11

To delete a user given their ID, you can use findByIdAndDelete:

User.findByIdAndDelete("5f9a1d156e848f06c458f5f4")
  .then((user) => console.log(user))
  .catch((err) => console.log(err));

Before you run the above example, you must provide another "option" parameter to the connect method:

const option = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
+  useFindAndModify: false
};

Once you removed the user, try to find it using findById method; it must return null to you, indicating there is no user with the given ID.

Step 12

In my database, there is currently a user with the following information

{
  _id: "5f9a157610bf5e86746c8eb9",
  username: "Matteo27",
  password: "yj71YRo4SHookR4",
  __v: 0
}

To update a user given their ID, you can use findByIdAndUpdate:

User.findByIdAndUpdate("5f9a157610bf5e86746c8eb9", {
  username: "username-updated",
})
  .then((user) => console.log(user))
  .catch((err) => console.log(err));

When I run the query above, it returns the same user as before! This is a little confusing! It happens because findByIdAndUpdate returns the "original" (before update) user document. To change this default behavior, you must pass an option parameter {new: true}:

User.findByIdAndUpdate(
  "5f9a157610bf5e86746c8eb9",
  {
    username: "username-updated-again",
  },
  { new: true }
)
  .then((user) => console.log(user))
  .catch((err) => console.log(err));

Now you will get the "updated" document:

{
  _id: "5f9a157610bf5e86746c8eb9",
  username: "username-updated-again",
  password: "yj71YRo4SHookR4",
  __v: 0
}

We have now covered all the CRUD operations we need for the YouNote API. There are several other query methods which you can learn about here.

YouNote API Persisted

In this module we will connect our YouNote API to the MongoDB cluster we have created in the previous module.

The source code for the completed app (which we will build in this module) can be found at this link.

Step 1

We are starting where we left off with refactoring YouNote API.

Install mongoose:

npm install mongoose

As the first step, we will update our model for "note"; open model.Note.js file which currently contains the following:

class Note {
  constructor(content, author) {
    this.content = content;
    this.author = author;
  }
}

module.exports = Note;

Update the source code:

const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const NoteSchema = new Schema({
  content: { type: String, required: true },
  author: { type: String, required: true },
});

const Note = mongoose.model("Note", NoteSchema);

module.exports = Note;

As it can be seen, we are making a schema for notes document and link that schema to a mongoose.model called Note. Note the attribute required: true in the NoteSchema; this attribute will help us with input validation.

Step 2

Open model/NoteDao.js file and reduce it to a skeleton data access object for notes:

const Note = require("./Note");

class NoteDao {
  constructor() {
  }

  create(content, author) {
    return null;
  }

  readAll(author = "") {
    return null;
  }

  read(id) {
    return null;
  }

  update(id, content, author) {
    return null;
  }

  delete(id) {
    return null;
  }
}

module.exports = NoteDao;

We will implement the above CURD operations one by one.

Step 3

Let's implement NoteDao.create method:

async create(content, author) {
  const note = await Note.create({ content, author });
  return note;
}

Notice I'm using the Async/Await pattern to deal with asynchronous Note.create operation:

  • The NoteDao.create method signature is modified by placing an async keyword in front of it.
  • The Note.create method is called and the call is preceded with await operator.

Notice I have not included the input validation which we had before. The Note.create method takes care of it (well, actually, Mongoose is taking care of it since when we defined the schema for notes, we specified "content" and "author" as "required" attributes).

If anything goes wrong with creating (and saving) the note, the Note.create will throw an error, and that error will be propagated in our application. So any program that calls NoteDao.create can wrap it inside a try-catch block to deal with potential errors.

Also note, the argument to Note.create is an "object" not the parameters "content" and "author".

Step 4

Let's implement the "read" operations:

async readAll(author = "") {
  const filter = author ? { author } : {};
  const notes = await Note.find(filter);
  return notes;
}

async read(id) {
  const note = await Note.findById(id);
  return note;
}

Notice the same Async/Await pattern is applied here.

The Note.find method takes an optional parameter, a filter, which can be used to search for notes that match the given attribute values. So, for example if we want to search for all notes written by "Bob" we can do this:

const notes = await Note.find({ author: "Bob" });

If we want to receive all notes, we can call Note.find with no argument

const notes = await Note.find();

Or with an empty filter object

const notes = await Note.find({});

If there are no "notes" in the database, or there is no match for the filter we have provided, the Note.find method returns an empty array. On the other hand, if there is no match for the ID we have provided to Note.findById, it will return null. These behaviors are consistent with how we have designed and implement our NoteDao "read" operations.

Step 5

Here is the NoteDao.delete operation:

async delete(id) {
  const note = await Note.findByIdAndDelete(id);
  return note;
}

The Note.findByIdAndDelete will delete and return the deleted note if id exist in the database. If the id does not exist, it will return null.

Here is the NoteDao.update operation:

async update(id, content, author) {
  const note = await Note.findByIdAndUpdate(
    id,
    { content, author },
    { new: true, runValidators: true }
  );
  return note;
}

Notice three parameters are provided to Note.findByIdAndUpdate:

  • id: the ID of a note in your database to be updated. If id does not match an existing note, the findByIdAndUpdate will return null.
  • An object containing the new attributes (and their values) which are to replace the existing attribute values of the note to be updated.
  • An object of parameters:
    • new: true changes the default behavior of findByIdAndUpdate to return the updated note (instead of the original note).
    • runValidators: true changes the default behavior of findByIdAndUpdate to force running validators on new attributes. If validation fails, the findByIdAndUpdate operation throws an error.

We are no done with operations of NoteDao. Next we will update the "notes" Router object.

Step 6

When I was writing the notes, I skipped over step 6 and went to step 7! To keep the notes consistent with the commits on the repository for this module, I've left this empty step here!!

Let's use it as a break!

Step 7

Open routes/notes.js file and strip it down to placeholder for route handlers:

const NoteDao = require("../model/NoteDao.js");
const express = require("express");
const { addSampleNotes } = require("../data/notes.js");
const router = express.Router();

const notes = new NoteDao();
addSampleNotes(notes);

router.get("/api/notes", (req, res) => {

});

router.get("/api/notes/:id", (req, res) => {

});

router.post("/api/notes", (req, res) => {

});

router.delete("/api/notes/:id", (req, res) => {

});

router.put("/api/notes/:id", (req, res) => {

});

module.exports = router;

We will implement the route handlers one by one.

Step 8

Here is the handler for the HTTP GET request through /api/notes endpoint:

router.get("/api/notes", (req, res) => {
  const author = req.query.author;
  notes
    .readAll(author)
    .then((notes) => res.json({ data: notes }))
    .catch((err) => errorHandler(res, 500, err));
});

Notice we are using the Promise pattern for dealing with async operations of notes object (instance of NoteDao).

Also note the use of the helper function errorHandler which is implemented as follows:

function errorHandler(res, status, error) {
  res.status(status).json({
    errors: [
      {
        status: status,
        detail: error.message || error,
      },
    ],
  });
}

The status code 500 means "Internal Server Error"; it indicates that our server encountered an unexpected condition that prevented it from fulfilling the request. This is the only plausible explanation where the simple GET request to read all notes has failed! It must be an issue with our database or the server connecting to the database, etc. (The fault is on our side!)

Similarly, we can implement the handler for the HTTP GET request through /api/notes/:courseId endpoint:

router.get("/api/notes/:id", (req, res) => {
  const id = req.params.id;
  notes
    .read(id)
    .then((note) => res.json({ data: note }))
    .catch((err) => errorHandler(res, 500, err));
});

Step 9

Here is the handler for the HTTP POST request through /api/notes endpoint:

router.post("/api/notes", (req, res) => {
  const content = req.body.content;
  const author = req.body.author;
  notes
    .create(content, author)
    .then((note) => res.status(201).json({ data: note }))
    .catch((err) => errorHandler(res, 400, err));
});

Notice if any error arises in the process of creating/storing a new note, it will be captured by the "catch" block.

A better implementation for this handler would be one that looks at the err object and distinguishes between issues related to e.g. database connection (which must be reported as "Internal Server Error" with status code of 500) vs. issues related to client's request (which is reported as 400 error: bad request).

Step 10

Here is the handler for the HTTP DELETE request through /api/notes/:courseId endpoint:

router.delete("/api/notes/:id", (req, res) => {
  const id = req.params.id;
  notes
    .delete(id)
    .then((note) =>
      note
        ? res.json({ data: note })
        : errorHandler(res, 404, "Resource not found")
    )
    .catch((err) => {
      errorHandler(res, 400, err);
    });
});

Notice, we account for the case where a given ID does not exist in our database:

  • The notes.delete operation will return null
  • We capture this by responding with a 404 error:
    note
      ? res.json({ data: note })
      : errorHandler(res, 404, "Resource not found")
    

Similarly, here is the handler for the HTTP PUT request through /api/notes/:courseId endpoint:

router.put("/api/notes/:id", (req, res) => {
  const id = req.params.id;
  const content = req.body.content;
  const author = req.body.author;

  notes
    .update(id, content, author)
    .then((note) =>
      note
        ? res.json({ data: note })
        : errorHandler(res, 404, "Resource not found")
    )
    .catch((err) => errorHandler(res, 400, err));
});

We have now implemented all the route handlers.

Step 11

Open data/notes.js, the module that generates sample data for us:

const faker = require("faker");

const NUM_SAMPLES = 4;

function addSampleNotes(notes) {
  for (let i = 0; i < NUM_SAMPLES; i++) {
    notes.create(faker.lorem.paragraphs(), faker.name.findName());
  }
}

module.exports = { addSampleNotes };

We must update this to account for the fact that notes.create is an async operation now. Moreover, we don't want to make sample data every time we run the application. It made sense before since we did not persisted the data but now we have a database. So instead, we will check if and generate sample data only if the database is empty.

const faker = require("faker");

const NUM_SAMPLES = 4;

async function addSampleNotes(notes) {
  const data = await notes.readAll();

  if (data.length === 0) {
    for (let i = 0; i < NUM_SAMPLES; i++) {
      await notes.create(faker.lorem.paragraphs(), faker.name.findName());
    }
  }
}

module.exports = { addSampleNotes };

Note that our sample data generation strategy is not a great one! If a client deletes all their notes (intentionally) and then we reset the server, they will open their application finding four sample notes which they didn't want to have.

Sample data is useful during development but you should not use it when the application goes to production.

Step 12

We are going to have mongoose connect to our MongoDB cluster.

First, we install the dotenv package:

npm install dotenv

Then create a .env file in the root of your application with the following content:

DB_ADMIN_PASSWORD="your-password-goes-here"

Open the .gitignore file and add the following to it, to ensure the .env file is not stored in your git repository:

.env

Create a new file data/db.js with the following content:

require("dotenv").config();
const mongoose = require("mongoose");

const password = process.env.DB_ADMIN_PASSWORD;
const dbname = "younote-db";
const URI = `mongodb+srv://younote-admin:${password}@younote-api.cwpws.mongodb.net/${dbname}?retryWrites=true&w=majority`;
const option = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useFindAndModify: false,
};

function connect() {
  mongoose.connect(URI, option);

  mongoose.connection.on("error", (err) => {
    console.log(err);
  });

  mongoose.connection.on("open", () => {
    console.log("Connected to MongoDB!");
  });
}

module.exports = { connect };

Step 13

Open index.js and add the following statements to the top of it:

const db = require("./data/db.js");

Next, add the following statement anywhere within the source file:

db.connect();

Save the file and run the API server!

Open your cluster on MongoDB Atlas. You must have a "notes" collection with 4 sample note documents:

Open Postman and try the API by retrieving all the notes:

Using Postman, try other API endpoints and make sure the updates are persisted in the database.

YouNote App

Here is the starter code that contains a very simple frontend for the YouNote application.

We will have the frontend app to communicated with the backend (YouNote API server) to retrieve notes for a given user (author).

Moreover, we will deploy the backend to Heroku to gain experience working with the backend server running locally and in the cloud.

Step 1

Open the terminal and change the directory to younote-app-starter folders.

Install the dependencies:

npm install

Run the application:

nodemon .

Open the browser and visit http://localhost:7000/.

Enter a username (any random one will do) and click on "Login":

We have these two views in this simple application.

Step 2

Let's explore the views of this application.

Open the views folder and notice there are three files in there:

  • base.njk
  • index.njk
  • dashboard.njk

We will explore these one by one!

base.njk

This template file contains the boilerplate HTML which is the basis of every HTML file that our app will render.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>YouNote</title>
    {% block css %}
    <!-- Google Fonts -->
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
    <!-- CSS Reset -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css">
    <!-- Milligram CSS -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css">
    <!-- Our Style -->
    <link rel="stylesheet" href="style.css">
    {% endblock %}
  </head>
  <body>
    {% block body %}
    {% endblock %}
    {% block script %}
    {% endblock %}
  </body>
</html>

Notice the use of Nunjucks Blocks.

A block defines a section on the template and identifies it with a name.

Blocks are used by Template Inheritance.

Further notice the stylesheets imported here; we are using a minimalist CSS framework called Milligram.

index.njk

This template file contains the homepage view which will be rendered when a user visits the YouNote web app. The template basically contains a form with a field for user to add their username.

{% extends "base.njk" %}

{% block body %}
<div class="container">
  <div class="row">
    <div class="column column-50">
      <h1>YouNote</h1>
      <form action="/dashboard">
        <label for="uname">username:</label>
        <input type="text" id="username" name="uname"><br><br>
        <input type="submit" value="Login">
      </form>
    </div>
  </div>
</div>
{% endblock %}

Notice the use of Nunjucks extends to specify template inheritance. The specified template is used as a base template.

The block body overrides the content of the block body declared in the base.njk template.

dashboard.njk

The "dashboard" view is rendered when the user logs in. The dashboard contains

  • A welcome message.
  • A text area to write notes.
  • A section where user's notes are displayed.
{% extends "base.njk" %}

{% block body %}
<div class="container">
  <div class="row">
    <div class="column column-50">
      <h1>Welcome {{ nickname }}!</h1>
      <form action="/note">
        <textarea name="content">Write your note here.</textarea>
        <input type="submit" value="Save!">
      </form>
      <h2>Your Notes</h2>
      {% for note in notes %}
        <div class="note">{{ note.content }}</div>
      {% else %}
        <p>You don't have any notes yet!</p>
      {% endfor %}
    </div>
  </div>
</div>
{% endblock %}

Notice the use of Nunjucks for loop that allows iterating over arrays and dictionaries. It appears the template expects us to inject an array notes which contains the user's notes.

Step 3

Let's take a closer look to the index.js.

In addition to the basic statements to get a NodeJS/Express app up and running (plus static files, and Nunjucks views), we have the following route handlers:

app.get("/", (_req, res) => {
  res.render("index.njk", null);
});

app.get("/dashboard", async (req, res) => {
  const username = req.query.uname;
  const data = {
    nickname: username,
    notes: []
  };
  res.render("dashboard.njk", data);
});

Notice the data passed to the dashboard view contains an empty notes array. We will update this to retrieve the notes for a given author (user) from the YouNote API.

Step 4

Open your terminal to the root folder of the YouNote API. (Use the lates version where we have connected the API to a MongoDB cluster in the cloud.)

Run the YouNote API on a port different than that of the YouNote App. If you've been following along, I have the API running on port 4567 and frontend app running on port 7000.

We are going to use a Node package called axios which provides an interface for fetching resources though HTTP requests.

Axios is similar to JavaScript's fetch in functionality, however, it provides a more powerful and flexible feature set. (See e.g. Axios vs. Fetch.)

Stop the frontend app and install axios:

npm install axios

Next, import axios in index.js

const axios = require("axios");

Now update the route handler for "dashboard" view:

app.get("/dashboard", async (req, res) => {
  const username = req.query.uname;
  const data = {
    nickname: username,
  };

  try {
    let response = await axios.get(`http://localhost:4567/api/notes?author=${username}`);
    data.notes = response.data.data;
  } catch (error) {
    console.log(error);
    data.notes = [];
  }

  res.render("dashboard.njk", data);
});

Save the file and run your frontend application. Select a user (author) which you have some notes stored in your younote-db cloud database. The notes must be displayed when you visit the dashboard for that user.

Here is a good tutorial on Axios: How to make HTTP requests like a pro with Axios.

Step 5

We will leave it to you to communicate to the API to save "create" notes, edit/delete notes, etc.

Instead of enhancing the frontend, let's deploy the backed to Heroku. This is especially interesting as we need to create "environment variables" on Heroku.

Go on Heroku and create an application and link it to the GitHub repository of YouNote API.

Stop the backend (API) application. Add a Procfile:

web: node ./index.js

Save, commit, and push the changes.

Go to your Heroku app settings and click on "Reveal Config Vars".

Config vars are the environment variables on Heroku.

Add the environment variable (with its value) that we have used to store the password for our MongoDB user.

Then restart your Heroku app:

Step 6

Open the index.js file in younote-app-starter folder.

Update the endpoint URL which we have provided to axios.get function:

- let response = await axios.get(`http://localhost:4567/api/notes?author=${username}`);
+ let response = await axios.get(`${API_URL}/api/notes?author=${username}`);

Create a variable API_URL and give it the URL of your Heroku app:

const API_URL = "https://cs280-younote-api.herokuapp.com";

Restart the frontend!

Your frontend app is now connected to the deployed YouNote API.

This repository contains the updated YouNote App.

Users App

In this module (and the next one) we learn how to implement a custom user authentication system that controls users access to web resources using NodeJS, ExpressJS and MongoDB. The system lets users sign up, log in, and log out, limiting access to password-protected resources.

Step 1

Download (clone) the starter code here.

Open the terminal and change directory to users-app-starter. Install the dependencies:

npm install

Run the application

nodemon .

Open your browser and visit http://localhost:5001.

Add a dummy username and password, and click on "Login" button. You will be redirected to the /dashboard:

Click on "Logout" button. You will be redirected to the homepage. Click on the registration link.

You will be redirected to the registration form.

Add a dummy username and password and click on "Register". You will be redirected to the homepage to login.

Step 2

Let's explore the code organization of the starter app:

.
├── README.md
├── assets
│   └── style.css
├── data
│   └── db.js
├── index.js
├── model
│   └── User.js
├── package-lock.json
├── package.json
└── views
    ├── alert.njk
    ├── base.njk
    ├── dashboard.njk
    ├── index.njk
    └── register.njk

4 directories, 12 files

This is a NodeJS/Express application which uses Nunjucks as template engine. There are three primary views:

  • index.njk
  • dashboard.njk
  • register.njk

Feel free to explore these in the views folder.

The application also includes a model and a data folder.

  • The model folder includes User.js which exposes a mongoose.model for representing a "user" in our application as well as performing CRUD operations on users.
  • The data folder contains db.js which encapsulates the logic to connect to our MongoDB instance.

Step 3

Let's connect the application to our MongoDB cluster (the one we have created for the YouNote API).

Open the data/db.js file and notice the URI is read from environment variable:

mongoose.connect(process.env.DB_URI, option);

Go to MongoDB Atlas website and get the connection URI for your cluster. For my MongoDB cloud cluster, the URI is:

mongodb+srv://younote-admin:<password>@younote-api.cwpws.mongodb.net/<dbname>?retryWrites=true&w=majority
  • Replace <password> with your database admin password. For me, this is the password for the user younote-admin.
  • Replace the <dbname> with the desired database name. I will continue to use younote-db as eventually I would like to bring the user registration/authentication into the YouNote App and its API.

Create a .env file in the users-app-starter folder with the following content:

DB_URI="YOUR-URI-GOES-HERE"

Open index.js file and import the db module:

const db = require("./data/db.js");

Connect to the database (place the following statement somewhere in index.js before using the app variable).

db.connect();

Save the index.js file and you should see your app is connected to the database:

[nodemon] restarting due to changes...
[nodemon] starting `node .`
Users' App is running at http://localhost:5001
Connected to MongoDB!

Notice the option variable in data/db.js which includes the following parameter:

useCreateIndex: true,

This parameter is included because we want the username attributes of each user to be unique, as it is indicated in mode/User.js:

const UserSchema = new Schema({
  username: { type: String, required: true, unique: true },
  password: { type: String, required: true },
});

Step 4

Let's explore the routes and the logic to handle client requests.

Open index.js. Notice the following routes:

app.get("/", (req, res) => {
  // Homepage- Render login form  
});

app.post("/login", (req, res) => {
  // check credential then redirect to dashboard
});

app.get("/dashboard", (req, res) => {
  // Render dashboard 
});

app.post("/logout", (req, res) => {
  // Log user out then redirect to homepage
});

app.get("/register", (req, res) => {
  // Render registeration form
});

app.post("/register", (req, res) => {
  // Register the user then redirect to homepage
});

I've replaced the implementation of each route handler with a comment to briefly describe its job.

We will focus on the user registration process first.

Step 5

Open views/registeration.njk and notice the user registration form:

<form action="/register" method="POST">
  <input type="text" name="username" placeholder="username" class="field">
  <input type="password" name="password" placeholder="password" class="field">
  <input type="submit" value="Register" class="btn">
</form>

In particular notice the action and method attributes for form element. As a client provides their desired username and password, and clicks on "Register" button, the "form" will send a POST HTTP request along with the "username" and "password" data.

Open the index.js and focus on the logic for handling POST request through /register endpoint:

app.post("/register", (req, res) => {
  const username = req.body.username;
  const password = req.body.password;
  console.log(colors.cyan("Register:", { username, password }));
  // TODO register the user!

  // redirect to the login page
  res.redirect("/");
});

Notice we receive the "username and "password" from req.body. This is not possible by default unless you use the following middleware (as it is done in index.js):

app.use(express.urlencoded({ extended: false }));

The express.urlencoded() function is a built-in middleware in Express. We use it in order to parse the body parameter of request that was sent from a "HTML form" on the client application. The extended: false prevents parsing "nested object" (considered a good practice).

Notice I also use the colors package that allows us to get colors in your NodeJS console. This is not needed; I've just added it here for fun!

console.log(colors.cyan("Register:", { username, password }));

Step 6

Let's register a user!

Update route handler for POST /register in index.js:

app.post("/register", async (req, res) => {
  const username = req.body.username;
  const password = req.body.password;
  console.log(colors.cyan("Register:", { username, password }));
  try {
    // TODO register the user!
    const user = await users.create({username, password});
    console.log(user);
    // redirect to the login page
    res.redirect("/");
  } catch (err) {
    console.log(err);
    res.json(err);
  }
});

Notice we simply print the error to console and return it to the client. This is not a good strategy to deal with errors! We will leave it for now however to focus on the registration process and good practices.

Also notice, the handler function is decorated with async keyword

app.post("/register", async (req, res) => {
  // notice the async keyword!
});

Finally, note you must include the following import statement in index.js

const users = require("./model/User.js");

Save index.js and go to http://localhost:5001/register to register a user:

You must the user object printed to the terminal (once it is created)

{
  _id: '5faab2d28853463591a63b4c',
  username: 'madooei',
  password: '1234',
  __v: 0
}

And check your MongoDB database to ensure the data is saved there

Step 7

Let's check user credentials as they login.

Update route handler for POST /login in index.js:

app.post("/login", async (req, res) => {
  const username = req.body.username;
  const password = req.body.password;
  console.log(colors.cyan("Login using", { username, password }));

  try {
    // check credential
    const user = await users.findOne({ username, password });
    if (!user) throw Error("No user found with this username!");
    console.log(user);
    // redirect to dashboard
    res.redirect(`/dashboard?username=${user.username}`);
  } catch (err) {
    console.log(err);
    // redirect to homepage
    res.redirect("/");
  }
});

Notice if we don't have a user registered in our database with the provided credentials we redirect to the login page. This could be confusing for clients when e.g. they misspell their username or input an incorrect password, there must be an error message shown to them.

Save index.js and try to login using a valid (and then invalid) credentials.

Step 8

Let's try something! Open index.js and update the route handler for GET /:

app.get("/", (req, res) => {
  res.render("index.njk", { message: "This is a message!" });
});

Notice I'm passing an object containing a message property to the index.njk template file.

Save index.js and visit http://localhost:5001/:

Notice the message appears at the top of the page and it is dismissible.

How did this happen? Well, open index.njk and notice the body block starts with

{{ super() }}

This statement means include whatever is already defined in the body block inside the base template (base.njk). So open base.njk and notice the content of the body block:

{% block body %}
  {% if message %}
    {% include "alert.njk" %}
  {% endif %}
{% endblock %}

The statement above indicate, if there is a message variable passed to the template, then include the content of alert.njk in that place. So, open alert.njk:

<div class="alert">
  <div class="closebtn" onclick="this.parentElement.style.display='none';">&times;</div>
  {{ message }}
</div>

The alert.njk basically contains the dismissible message element that was displayed when you visited the app's homepage.

Step 9

We can use this dismissible message element to inform user about potential error messages or other informations.

For example, update the catch block in the route handler for POST /login in index.js:

catch (err) {
  console.log(err);
  // redirect to homepage
  res.redirect("/?message=Incorrect username or password");
}

Also, update the route handler for GET /:

app.get("/", (req, res) => {
  const message = req.query.message;
  res.render("index.njk", { message });
});

Save index.js and visit http://localhost:5001/. Enter a username/password that does not exist in the database. You must get the following:

Step 10

Notice the URL for homepage will be appended with the message query parameter:

http://localhost:5001/?message=Incorrect username or password

This is not the most elegant way of achieving the desired behavior. Imagine we have more detailed error messages (and perhaps other properties to pass along the message). The query parameter is not the place to send these messages.

We will now introduce an alternative approach to achieve the same results. This alternative strategy uses web cookies!

Web cookies (or simply cookies) are simple text files that a website can store on your browser.

Stop the application and install the cookie-parser package:

npm install cookie-parser

Open index.js, import the cookie-parser module:

const cookieParser = require("cookie-parser");

Then, add this module as a middleware to your express app:

app.use(cookieParser());

Step 11

Let's update the catch block in the route handler for POST /login in index.js:

} catch (err) {
  console.log(err);
  // redirect to homepage
  res.cookie("message", "Incorrect username or password").redirect("/");
}

Also, update the route handler for GET /:

app.get("/", (req, res) => {
  const message = req.cookies.message;
  res.render("index.njk", { message });
});

Notice the message is read from the req.cookies and not req.query.

Save index.js and visit http://localhost:5001/. Enter a username/password that does not exist in the database. You must get the "Incorrect username or password" message. But notice the URL is simply http://localhost:5001/. So where does this data come from?

Open the developer tools, go to "Application" tab and look under "Cookies".

Step 12

Let's update the app to use the dismissible messages in other views!

Let's have the logout process to set a message "You have successfully logged out!":

app.post("/logout", (req, res) => {
  console.log(colors.cyan("Log out!"));
  res.cookie("message", "You have successfully logged out!").redirect("/");
});

Also, let's update the registration process to display appropriate message

app.post("/register", async (req, res) => {
  const username = req.body.username;
  const password = req.body.password;
  console.log(colors.cyan("Register:", { username, password }));
  try {
    // TODO register the user!
    const user = await users.create({ username, password });
    console.log(user);
    // redirect to the login page
    res.cookie("message", "You have successfully registered!").redirect("/");
  } catch (err) {
    console.log(err);
    res.cookie("message", "Invalid username or password!").redirect("/register");
  }
});

Also, update the following handler to provide the register and dashboard view with the message when it is available:

app.get("/register", (req, res) => {
  const message = req.cookies.message;
  res.render("register.njk", { message });
});
app.get("/dashboard", (req, res) => {
  const username = req.query.username;
  const message = req.cookies.message;
  res.render("dashboard.njk", { username, message });
});

Save index.js and visit http://localhost:5001/. Try visiting different routes and test the messaging works as intended.

Step 13

There is a potential issue with the way I have used cookies for passing messages between different views. Let's observe the issue first: open the homepage and enter an invalid user credentials.

You will get the "Incorrect username and password" message. Now click on the "registration" link.

Notice there is again the "Incorrect username and password" message. This is due to the simple fact that the cookie with the error message still exist. So potentially, we will see this message in every view until the cookie is deleted or overwritten.

We must update the program so the (flash) message is only available once, for the next request.

That is if we store a (flash) message in a cookie and render a page, the message is read and displayed but if we render the same (or another) page, the message is not present (it is destroyed).

There are a number of strategies to achieve this desired outcome. I will use this opportunity to show you how we can write our own Express middleware. The middleware we write will destroy the flash message after it is read.

Step 14

Add the following to index.js after app.use(cookieParser()); and before any of the route handlers:

app.use((req, res, next) => {
  if (req.cookies.message) {
    res.clearCookie("message");
  }
  next();
});

Recall middleware intercept req and res objects:

  • before the req goes to any of the route handlers, we intercept it and check if it contains a "message" cookie.
  • if there is a "message" cookie, then we call the clearCookie on the response object, so it will be called in addition to whatever else is done to the "response" in any of the route handlers (and other middleware).
  • the next() function is part of the semantic of any middleware; it calls the "next" middleware (if there is any).

Save index.js and try to recreate the issue we observed in the previous section.

Step 15

Notice when we redirect to dashboard, we pass the username as query parameter:

res.redirect(`/dashboard?username=${user.username}`);

We can now change this to instead use a cookie to store username.

res.cookie("username", user.username).redirect(`/dashboard`);

Also, update update the route handler for dashboard so that you read the username from set cookie:

- const username = req.query.username;
+ const username = req.cookies.username;

Save index.js. Go to the application and login. Make sure the dashboard view works as intended.

Step 16

The dashboard is a "protected resource". Only a user must be able to access this page. At the moment, we can directly navigate to dashboard: http://localhost:5001/dashboard.

Now that we have a "username" cookie, we can use as an indication to which a user is logged in or not. We can therefore protect the dashboard view:

app.get("/dashboard", (req, res) => {
  const username = req.cookies.username;
  const message = req.cookies.message;
  if (username) {
    res.render("dashboard.njk", { username, message });
  } else {
    res.cookie("message", "Please login first!").redirect("/");
  }
});

Save the index.js file. In developer tools, manually delete the "username" cookie: (right click on it and select delete)

Then refresh the dashboard view; you must be redirected to the homepage!

Step 17

We can apply the same pattern to the homepage! If a user is already logged in, then take them to their dashboard and skip the log in form.

app.get("/", (req, res) => {
  const username = req.cookies.username;
  const message = req.cookies.message;
  if (username) {
    res.redirect("/dashboard");
  } else {
    res.render("index.njk", { message });
  }
});

Save the index.js file; visit the homepage and login with a valid credentials. Then, try to visit the homepage again! You must be redirected to dashboard.

Now we can also implement the logout process; we simply remove (clear) the username cookie:

app.post("/logout", (req, res) => {
  console.log(colors.cyan("Log out!"));
  res
    .clearCookie("username")
    .cookie("message", "You have successfully logged out!")
    .redirect("/");
});

Save the index.js file. Play around with the application; log in and out, try to visit dashboard when you are already logged in or after you have logged out. The app is coming together 😃.

Step 18

Notice that you can edit the value of a cookie in developer tools:

You can even create a cookie, username, and give it a value. That would mean tricking our app to think a user has already logged in.

One strategy to mitigate this issue is to "sign" (encrypt) the "username" cookie.

To make signed (encrypted) cookie, you must first provide an encryption key to the cookieParser function:

app.use(cookieParser(process.env.ENCRYPT_KEY));

Update your .env file and add an ENCRYPT_KEY variable

DB_URI="YOUR-URI-GOES-HERE"
ENCRYPT_KEY="SOME_SECRET_PHRASE"

Next, you must provide an option when you create the "username" cookie to indicate you want this cookie to be signed:

- res.cookie("username", user.username).redirect(`/dashboard`);

+ res
+  .cookie("username", user.username, { signed: true })
+  .redirect(`/dashboard`);

Finally, you must change every instance of reading the "username" cookie, to read it as a signed cookie:

- const username = req.cookies.username;
+ const username = req.signedCookies.username;

Save the index.js file. Log in using a valid credential and then investigate the cookie generated for the user. For example, I've logged in using the user name madooei and this is the content of username cookie on my browser:

s%3Amadooei.M5UVfPbyTAEE65zH1I401zdu4Kty6EQkkoncj1J1qyk

Notice the madooei is still part of the cookie. If I manually change the cookie however, (for example, change the madooei part to ali) and refresh the dashboard, it will recognize that I have tampered with the cookie and take me to the login page.

A better, more secure, strategy to encode your cookies is using Json Web Token (JWT). We will not cover this here but you can read more about it following these resources:

Step 19

Another useful property of cookies is expiration time. Have you noticed some web services require you to login after certain time has passed since you last logged in? The JHU authentication works this way. Sometimes I need to login several times in a day to access my email through their webmail service (which I find annoying). Other services, like Gmail and YouTube, keep you logged in until you log out.

Update where the username cookie is set:

- res
-  .cookie("username", user.username, { signed: true })
-  .redirect(`/dashboard`);

+ res
+  .cookie("username", user.username, { signed: true, maxAge: 2000 })
+  .redirect(`/dashboard`);

Save index.js. Login using a valid credential. Give it 2 seconds and then refresh! You must be redirected to login page!!

The maxAge value is in milliseconds. I will set it to 2 hours for this application:

res
  .cookie("username", user.username, { signed: true, maxAge: 7200000 })
  .redirect(`/dashboard`);

There is so much you can do with cookies. If you want more, you must store information pertaining to user login and visits on the server side (backend). The technical term often used to contrast this alternative strategy from using (Web) cookies, is using (Web) "sessions".

A session can be defined as a server-side storage of information that is desired to persist throughout the user's interaction with the web site or web application.

Cookies are still used with sessions but typically the cookie for an application contains an identifier for a session.

We will not cover web sessions. If you are interested, here are some helpful resources:

Step 20

Let's fix a huge security risk in our application!

You should never store users' password in a database! You must encode the password and store the encoded version.

We are going to follow the recommendation made in How To Safely Store A Password which essentially recommends using the bcrypt password-hashing function.

Stop the application and install the bcrypt package:

npm install bcrypt

Import the package in index.js:

const bcrypt = require("bcrypt");

Update where you create and save user:

- const user = await users.create({ username, password });
+ const hash = await bcrypt.hash(password, 10);
+ const user = await users.create({ username, password: hash });

Notice the 10 is the value for "salt round" which is the cost factor in BCrypt algorithm. The cost factor controls how much time is needed to calculate a single BCrypt hash. The higher the cost factor, the more hashing rounds are done. Increasing the cost factor by 1 doubles the necessary time. The more time is necessary, the more difficult is brute-forcing.

Save index.js and run the application again. Go to registration page and create a new user account. I created a user with username ali and password 1234 and this is what is now stored in the database:

{
  _id: '5facb6581128743f8c9f7a8c',
  username: 'ali',
  password: '$2b$10$FGpyplYOqntwbNJNvfKy1efZA1LGSTOn3H9gemDVOVlwf3KqWnIle',
  __v: 0
}

When you authenticate user, you need to use the bcrypt package:

- const user = await users.findOne({ username, password });
+ const user = await users.findOne({ username });
if (!user) throw Error("No user found with this username!");
+ const authorized = bcrypt.compare(password, user.password);
+ if (!authorized) throw Error("Invalid password!");

Save index.js and ensure you can login with the credential you have created after adding bcrypt to this application.

Another potential addition to our Users App is a process to validate password complexity to enforce users provide strong passwords. There are node packages that help you with this, for example joi-password-complexity, which I encourage you to explore on your own.

The complete application is here: https://github.com/cs280fall20/users-app.

React

There are many frameworks and libraries that facilitate development of Web application, in particular its front-end component, the user interface, or UI.

Among these, React is perhaps the most popular. It was developed by a group of Facebook engineers, initially at Facebook.

In this module we learn to work with React. As a first step, we will rebuild our good old SleepTime App in React!

React has a great documentation and set of tutorials on its Website. The best place to learn React is there! I also found this article to have a good overview: The Missing Introduction to React.

Here are some other reads you may be interest in:

Step 1

There are several different ways to create a React application. Consult the documentation here.

We are going to use NPM's package runner, NPX, for scaffolding our React app using the create-react-app package.

npx create-react-app sleep-time-react

The create-react-app is a command-line tool that scaffolds a React application.

Using create-react-app, there is no need to install or configure module bundlers like Webpack, or transpilers like Babel. These come preconfigured (and hidden) with create-react-app, so you can jump right into building your app!

Change directory to sleep-time-react. Run the application:

npm run tart

Point your browser to http://localhost:3000/

Step 2

Let's explore the Node project created by create-react-app:

sleep-time-react
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    ├── serviceWorker.js
    └── setupTests.js

We are going to strip down the application to the bare minimum!

Delete everything except for index.html in the public folder. Moreover, update the index.html file to the following minimal content:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>SleepTime App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

The index.html is a template where UI components will be added to the "root" (<div> element with id="root"). If you recall, the second time we were building SleepTime App, we only had <div id="root"></div> inside index.html file and we created the entire app in script.js. (See here to refresh your memory!) A similar approach is taken by React.

Step 3

Delete everything except for App.js and index.js from the src folder. Moreover, update the index.js and App.js as follows:

// index.js
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));
// App.js
function App() {
  return <div>Hello React!</div>;
}

export default App;

Save the files. Notice the application running in the browser:

Step 4

Let's take a look at App.js file:

function App() {
  return <div>Hello React!</div>;
}

export default App;

This looks like a simple function that returns an HTML element. There are several interesting thing about it though!

First, let's notice the HTML element directly used inside JavaScript file!! You cannot normally do this in JavaScript source file. This works because JavaScript files in a React app are "transpiled" before execution. For instance, the code above will be transpiled into the following:

import React from "react";

function App() {
  return React.createElement("div", null, "Hello React!");
}

export default App;

So the <div>Hello React!</div> is not truly a HTML element, it it a React element.

A React element is a JavaScript object describing a DOM node (HTML element) and its properties.

To see this, update the App.js as follows:

import React from "react";

function App() {
  const elm = React.createElement("div", null, "Hello React!");
  console.log(elm);
  return elm;
}

export default App;

Save the file, as the app reloads in the browser, open the Developer Tools and notice the elm printed to the console:

The React element is rendered as an HTML element (DOM node) by the react-dom module. In fact, we can go to the index.js and eliminate the middleman! Simply update the file to the following:

import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(
  React.createElement("div", null, "Hello React!"),
  document.getElementById("root")
);

Save the file, as the app reloads in the browser, open the Developer Tools and look at the HTML elements:

Notice <div>Hello React!</div> is added as a child to <div id="root">.

Aside 1: A React element describes a DOM node (HTML element) but it is a JavaScript object. Some references call this object a Virtual DOM. (Good read: What is the Virtual DOM?)

Aside 2: To create a React element, you can use React's createElement() method. It's syntax is:

React.createElement(/* type */, /* props */, /* content */);

We will, however, use JSX syntax to create React elements (as in <div>Hello React!</div> instead of React.createElement("div", null, "Hello React!")). We will talk about JSX in more details, shortly.

Step 5

Undo the changes we've made to App.js and index.js:

// App.js
function App() {
  return <div>Hello React!</div>;
}

export default App;
// index.js
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));

Notice we import the App in index.js (and, also notice, we use ES6 modules with import/export statements).

The separation between the App.js and index.js is symbolic!

The philosophy of React is to decouple the process of creating an app and rendering it. So you can create the app and then decide to render it on a server, native devices or even VR environment.

Notice how <App /> looks like an HTML element (with angle brackets and all!). This is a React component.

A Component is a building block in React that is ultimately responsible for returning HTML to be rendered onto a page.

Just as you can compose functions or classes to create more complex solutions/models, you can compose simpler components to create more complex ones.

<Page>
  <Sidebar />
  <Article />
</Page>

React builds up pieces of a UI using Components.

Components can be classes or functions. This is a function component:

function App() {
  return <div>Hello React!</div>;
}

export default App;

Here is the same component as a class:

import React from "react";

class App extends React.Component {
  render() {
    return <div>Hello React!</div>
  }
}

export default App;

Notice the class component is more verbose. It has a render method that returns a react element. A class component allows us to declare state and benefit from inheritance. However, if you don't need those perks, go for function components. Prefer the simplicity of functions.

Step 6

Let's update the App.js:

function App() {
  return (
    <div>
      <p>If you go to bed NOW, you should wake up at...</p>
      <button>zzz</button>
      <p>It takes the average human fourteen minutes to fall asleep.</p>
      <p>
        If you head to bed right now, you should try to wake up at one of the
        following times:
      </p>
      <p>11:44 PM or 1:14 AM or 2:44 AM or 4:14 AM or 5:44 AM or 7:14 AM</p>
      <p>A good night's sleep consists of 5-6 complete sleep cycles.</p>
    </div>
  );
}

export default App;

Save the file and reload the app:

Notice how easy it is to mix HTML and JavaScript! This is possible due to JSX.

JSX is a syntax extension to JavaScript that allows us to mix JavaScript with HTML.

Good reads: Introducing JSX on React Docs, and What the heck is JSX and why you should use it to build your React apps by freeCodeCamp.

We will use JSX with React to describe what the UI should look like.

JSX is, in many ways, like a template language (such as Nunjucks), but it is more versatile as it comes with the full power of JavaScript. (Besides, you don't need to learn a new syntax specifically designed for a template language).

Template languages such as Nunjucks are closer to HTML than JavaScript. JSX, on the other hand, is closer to JavaScript than HTML.

There is a lot that can be done with JSX. There are some gotchas and nuances too! We will explore these as we build React applications. For an impatient reader, I recommend reading JSX in-depth on React Docs.

Step 7

A great mindset to have when building React apps is to think in Components.

Components represent the modularity and reusability of React.

Let's break out the SleepTime App into two components:

  • The main app, in App.js (including the logic to compute sleep cycles).
  • The output, in Output.js

Create a file Output.js with this content:

function Output() {
  return (
    <div>
      <p>It takes the average human fourteen minutes to fall asleep.</p>
      <p>
        If you head to bed right now, you should try to wake up at one of the
        following times:
      </p>
      <p>11:44 PM or 1:14 AM or 2:44 AM or 4:14 AM or 5:44 AM or 7:14 AM</p>
      <p>A good night's sleep consists of 5-6 complete sleep cycles.</p>
    </div>
  );
}

export default Output;

Notice that we must wrap the output elements in a single element (the outer div). Each React component returns a single React element.

Now update App.js as follows:

import Output from "./Output.js";

function App() {
  return (
    <div>
      <p>If you go to bed NOW, you should wake up at...</p>
      <button>zzz</button>
      <Output />
    </div>
  );
}

export default App;

Step 8

Let's update the Output.js to dynamically render the sleep cycles:

function Output() {
  const cycles = [
    "11:44 PM",
    "1:14 AM",
    "2:44 AM",
    "4:14 AM",
    "5:44 AM",
    "7:14 AM",
  ];

  return (
    <div>
      <p>It takes the average human fourteen minutes to fall asleep.</p>
      <p>
        If you head to bed right now, you should try to wake up at one of the
        following times:
      </p>
      <ul>
        {cycles.map((cycle) => (
          <li>{cycle}</li>
        ))}
      </ul>

      <p>A good night's sleep consists of 5-6 complete sleep cycles.</p>
    </div>
  );
}

export default Output;

Save the file and see the output:

Notice the cycles are displayed in an unordered list. I've made this change so we can work with a slightly more complex JSX syntax:

<ul>
  {cycles.map((cycle) => (
    <li>{cycle}</li>
  ))}
</ul>

When we mix JavaScript with HTML in JSX, we must put the JavaScript bits in {}.

Open the Developer Tool in the browser and notice a warning printed to the terminal:

The warning says, each child in a list must have a unique key. The key helps React to track changes in the child elements, as the state changes in the app. Here is a simple fix:

<ul>
  {cycles.map((cycle, index) => (
    <li key={index}>{cycle}</li>
  ))}
</ul>

JSX may feel strange and you may be confused when first starting using it. In my experience though, you will quickly get the hang of it, and then it will increase your coding productivity.

Step 9

The Output component does not generate the sleep cycle hours. It must receive it. Let's pass this data from App component to Output component.

Move the cycles from Output.js to App.js:

import Zzz from "./Zzz.js";
import Output from "./Output.js";

function App() {
  const cycles = [
    "11:44 PM",
    "1:14 AM",
    "2:44 AM",
    "4:14 AM",
    "5:44 AM",
    "7:14 AM",
  ];

  return (
    <div>
      <p>If you go to bed NOW, you should wake up at...</p>
      <Zzz />
      <Output cycles={cycles} />
    </div>
  );
}

export default App;

Notice how cycles is passed to Output component:

<Output cycles={cycles} />

In React jargons, cycles is called "props". Just like we use arguments to pass data to functions, we use props to pass data to React Components.

A prop is any input that you pass to a React component.

Just like an HTML attribute, a prop is declared as name and value pair when added to a Component. (Props is short for properties - Good read: What is “Props” and how to use it in React?.)

We now update the Output.js:

function Output(props) {
  return (
    <div>
      <p>It takes the average human fourteen minutes to fall asleep.</p>
      <p>
        If you head to bed right now, you should try to wake up at one of the
        following times:
      </p>
      <ul>
        {props.cycles.map((cycle, index) => (
          <li key={index}>{cycle}</li>
        ))}
      </ul>

      <p>A good night's sleep consists of 5-6 complete sleep cycles.</p>
    </div>
  );
}

export default Output;

The "props" passed to a Component are collected as name/value properties in one object called props. If you have a function component, the props will be passed to the function as an argument:

function Output(props) {
  // to get the "cycles" array use props.cycles
}

Aside: If you use Class component, the props will be passed to its constructor as an argument:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
  }

  // you can use this.props anywhere in this class
  // to get the "cycles" array use this.props.cycles
}

Step 10

Let's refactor the App component to a class component:

import { Component } from "react";
import Output from "./Output.js";

class App extends Component {
  constructor(props) {
    super(props);
    this.cycles = [
      "11:44 PM",
      "1:14 AM",
      "2:44 AM",
      "4:14 AM",
      "5:44 AM",
      "7:14 AM",
    ];
  }

  render() {
    return (
      <div>
        <p>If you go to bed NOW, you should wake up at...</p>
        <button>zzz</button>
        <Output cycles={this.cycles} />
      </div>
    );
  }
}

export default App;

Notice the alternative approach to using React.Component class is to directly import the Component class.

Let's add a method to calculate the cycles:

calcCycles() {
  // get current time
  let now = Date.now(); // in milliseconds
  let minute = 60 * 1000; // milliseconds
  let cycle = now;

  // allow 14 minutes to fall sleep
  cycle += 14 * minute;

  // calculate 6 sleep cycles (each 90 minutes)
  for (let i = 0; i < 6; i++) {
    cycle += 90 * minute;

    // update the sleep cycles
    this.cycles[i] = new Date(cycle).toLocaleTimeString([], {
      hour: "2-digit",
      minute: "2-digit",
    });
  }

  // print cycles for sanity check
  console.log(this.cycles);
}

Now we need to attach this method to the zzz button:

<button onClick={this.calcCycles}>zzz</button>

Save the file and reload the app. Then click on zzz button. Notice you will get an error message!

The problem is related to the execution context of calcCycle. The transpiler used in React does not bind the methods to the object of class. To solve this, you can use the bind method as follows:

<button onClick={this.calcCycles.bind(this)}>zzz</button>

If you have a method that you are going to use in multiple places, then you may want to bind it to the class object once, in the class constructor:

constructor(props) {
  super(props);
  this.cycles = [
    "11:44 PM",
    "1:14 AM",
    "2:44 AM",
    "4:14 AM",
    "5:44 AM",
    "7:14 AM",
  ];
  this.calcCycles = this.calcCycles.bind(this);
}

Another alternative is to use arrow functions for method declaration.

calcCycles = () => {
  // get current time
  let now = Date.now(); // in milliseconds
  let minute = 60 * 1000; // milliseconds
  let cycle = now;

  // allow 14 minutes to fall sleep
  cycle += 14 * minute;

  // calculate 6 sleep cycles (each 90 minutes)
  for (let i = 0; i < 6; i++) {
    cycle += 90 * minute;

    // update the sleep cycles
    this.cycles[i] = new Date(cycle).toLocaleTimeString([], {
      hour: "2-digit",
      minute: "2-digit",
    });
  }

  // print cycles for sanity check
  console.log(this.cycles);
}

Arrow functions do not bind their own this, instead, they inherit one from the parent scope, which is called "lexical scoping".

Once you fixed the issue, save the file, reload the app and then click on zzz again!

Step 11

We can now generate the cycles but we need to reload (re-render) the Output component to show the new cycles.

This is where "states" come in!

We learned that props refer to data passed from parent components to child components.

The props represent "read-only" data that are meant to be immutable.

So if you are a component, should not modify props that you receive!

A component's state, on the other hand, represents mutable data. The "state" ultimately affects what is rendered on the page.

A component's state is managed internally by the component itself and is meant to change over time, commonly due to user input (e.g., clicking on a button on the page).

By having a component manage its own state, any time there are changes made to that state, React will know and automatically make the necessary updates to the page.

This is one of the key benefits of using React to build UI components:

When it comes to re-rendering the page, we just have to think about updating state.

  • We don't have to keep track of exactly which parts of the page changed each time there are updates.
  • We don't need to decide how we will efficiently re-render the page.
  • React compares the previous state and new state, determines what has changed, and makes these decisions for us.
  • This process of determining what has changed in the previous and new states is called Reconciliation.

Update the constructor of App component:

constructor(props) {
  super(props);
  this.state = {
    cycles: [
      "11:44 PM",
      "1:14 AM",
      "2:44 AM",
      "4:14 AM",
      "5:44 AM",
      "7:14 AM",
    ],
  };
}

When you update the state, you must use the function setState (which is inherited from Component class).

We can technically update the state directly, but that way React will not know we have updated it!

The simplest way to use setState is to pass an object to it, and the object will be merged into the current state to form the new state.

this.setState({ cycles: [/* new values */] })

Let's update the calcCycles method to update the state:

calcCycles() {
  // get current time
  let now = Date.now(); // in milliseconds
  let minute = 60 * 1000; // milliseconds
  let cycle = now;
  const cycles = new Array(this.state.cycles.length);

  // allow 14 minutes to fall sleep
  cycle += 14 * minute;

  // calculate 6 sleep cycles (each 90 minutes)
  for (let i = 0; i < 6; i++) {
    cycle += 90 * minute;

    // update the sleep cycles
    cycles[i] = new Date(cycle).toLocaleTimeString([], {
      hour: "2-digit",
      minute: "2-digit",
    });
  }

  // print cycles for sanity check
  console.log(cycles);

  // update state
  this.setState({ cycles: cycles });
};

Aside: Don't forget to update the props passed to Output component:

- <Output cycles={this.cycles} />
+ <Output cycles={this.state.cycles} />

Save the file and reload the app. Click on the zzz button. The cycles must be updated in the page!

Step 12

Notice I could have created the state in Output component as

this.state = {
  cycles: this.props.cycles
}

And then update the state in the Output component.

This is an anti-pattern; when defining a component's initial state, avoid initializing that state with props. Why? because it is error-prone. It also leads to duplication of data, deviating from a dependable "source of truth."

Let's talk a bit about another important feature of React. This has to do with data flow.

Data flow is about managing the state of the app: when data changes in one place it would be reflected every where.

Other popular front-end frameworks like Angular and Ember use Two-way data binding where the data is kept sync throughout the app no matter where it is updated. That means any part of the app could change the data. This solution is hard to keep track of, when app grows large.

React uses an explicit method of passing data down to components:

In React, data only flows in one direction: from parent to child. When data gets updated in the parent, it will pass it again to all the children who use it.

We will talk more about this in future modules. If you are interested in learning more on this, I recommend reading Unidirectional Data Flow in React.

Step 13

Let's hide the output and show it when user clicks on the zzz button.

Add the showOutput variable to the states of App component:

this.state = {
  cycles: [
    "11:44 PM",
    "1:14 AM",
    "2:44 AM",
    "4:14 AM",
    "5:44 AM",
    "7:14 AM",
  ],
  showOutput: false,
};

Pass the showOutput as a prop to Output component:

<Output cycles={this.state.cycles} showOutput={this.state.showOutput} />

Update Output component to show the result based on the showOutput:

function Output(props) {
  return !props.showOutput ? (
    <div></div>
  ) : (
    <div>
      <p>It takes the average human fourteen minutes to fall asleep.</p>
      <p>
        If you head to bed right now, you should try to wake up at one of the
        following times:
      </p>
      <ul>
        {props.cycles.map((cycle, index) => (
          <li key={index}>{cycle}</li>
        ))}
      </ul>

      <p>A good night's sleep consists of 5-6 complete sleep cycles.</p>
    </div>
  );
}

export default Output;

Update App.calcCycle to flip the showOutput flag:

// update state
- this.setState({ cycles: cycles });
+ this.setState({ cycles: cycles, showOutput: true });

Save the files and reload the app. You must see the app without the output component:

And the output must be displayed once you click on zzz button:

Step 14

Let's add some styling to our app!

Create the following files in the src folder:

/* index.css */
body {
  margin: 0;
  background-color: #282c34;
  color: #ffffff;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
/* App.css */
.App {
  text-align: center;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
}

button {
  font-family: 'Source Sans Pro', sans-serif;
  font-weight: 900;
  padding: 1.25rem 2rem;
  font-size: 1rem;
  border-radius: 3.5rem / 100%;
  position: relative;
  min-width: 15rem;
  max-width: 90vw;
  overflow: hidden;
  border: 0;
  cursor: pointer;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  -webkit-transition: all 330ms;
  transition: all 330ms;
}
/* Output.css */
ul {
  list-style-type: none;
}

One of the nice features of React is that you can create styling for each component, separately, and associate the component with the corresponding CSS file.

All you need to do, is to import the css file in the corresponding JavaScript file.

So make the following updates:

// index.js
+ import './index.css';
// App.js
+ import './App.css';
// Output.js
+ import './Output.css';

You must also assign a "class" attribute to the <div> element that wraps the entire app! Open the App.js file and update the return statement of the render method:

return (
- <div>
+ <div className="App">
    <p>If you go to bed NOW, you should wake up at...</p>
    <button onClick={this.calcCycles.bind(this)}>zzz</button>
    <Output cycles={this.state.cycles} showOutput={this.state.showOutput} />
  </div>
);

Notice, in JSX, to assign class attribute, you must use the attribute name of className instead of class because class is a keyword in JavaScript!.

Save the files and reload the app:

And after clicking on the zzz button, you will get:

Step 15

Let's make the app look a little fancier!

Add this file to the src folder.

Update the App.js file; first import the image:

import logo from './moon.png';

Then add it to the React element that is returned from the App component:

render() {
  return (
    <div className="App">
      <img src={logo} className="App-logo" alt="logo" />
      <p>If you go to bed NOW, you should wake up at...</p>
      <button onClick={this.calcCycles.bind(this)}>zzz</button>
      <Output cycles={this.state.cycles} showOutput={this.state.showOutput} />
    </div>
  );
}

Add the following to the App.css file:

.App-logo {
  height: 30vmin;
  pointer-events: none;
}

Save the files and reload the app:

And after clicking on the zzz button, you will get:

The completed application is here.

Zirectory (Part I)

We will be building an application called "Zirectory" (Zoom Directory) which is a collection of Zoom meeting links for JHU courses. Faculty can add their courses' Zoom link to this directory and students can search to find the links, for fast and easy access to their lectures.

In the first part, we will focus on breaking the UI into a component hierarchy and build each component. In the process, we will revisit the basics of React, using JSX, passing data as props, managing state, etc.

The repository for part-1 (completed) is here.

Step 1

You can find the starter code of this application here

  • Clone this repository
  • Install dependencies by npm install
  • Run in development mode by npm run start
  • Open http://localhost:3000 to view it in the browser.

Step 2

This is the wireframe of the UI which we are going to build.

Thinking in React involved breaking the UI into a component hierarchy. I can think of at least three components in the above wireframe:

  • A component that represents the search bar
  • A component that represents each meeting
  • A component that wraps the search bar and meetings; let's call it "list meetings"

We will build these components one at a time!

Step 3

Create the following files with the provided content. Notice that I have already created style sheet for each component to save us time!

Search.js

import "./Search.css";

function Search() {
  return <div>Search</div>;
}

export default Search;
Search.css
.search-meetings {
  width: 100%;
  padding: 20px 20px 20px 60px;
  background-image: url("./icons/search.svg");
  background-repeat: no-repeat;
  background-position: 20px center;
  background-size: 1.2em;
  font-size: 1.2em;
  border: 0;
  outline: none;
}

Meeting.js

import "./Meeting.css";

function Meeting() {
  return <div>Meeting Information</div>;
}

export default Meeting;
Meeting.css
.meeting-list-item {
  padding: 20px;
  background: white;
  display: flex;
}

@media (min-width: 600px) {
  .meeting-list-item {
    margin: 20px;
    border: 1px solid #d5d8df;
    border-radius: 4px;
  }
}

.meeting-details {
  padding-left: 20px;
  border-left: 1px solid #eee;

  flex: 1;
  overflow: hidden;

  display: flex;
  flex-direction: column;
  justify-content: center;
}

.meeting-details p {
  margin: 0;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.meeting-details .title {
  font-size: larger;
}

.meeting-details a {
  color: #003f62;
  font-family: helvetica;
  text-decoration: none;
}

.meeting-details a:hover {
  text-decoration: underline;
}

.meeting-details a:active {
  color: #003f62;
}

.meeting-details a:visited {
  color: #003f62;
}

.meeting-remove {
  padding-left: 20px;
  align-self: center;
  width: 32px;
  height: 32px;
  background-color: transparent;
  background-image: url("./icons/cancel.svg");
  background-size: 100% 100%;
  border: 0;
  color: black;
  font-size: 0;
  vertical-align: middle;
  cursor: pointer;
  outline: none;
}

ListMeetings.js

import "./ListMeetings.css";
import Search from "./Search.js";
import Meeting from "./Meeting.js";

function ListMeetings() {
  return (
    <div className="list-meetings">
      <div className="list-meetings-top">
        <Search />
      </div>
      <ol className="meeting-list">
        <Meeting />
      </ol>
    </div>
  );
}

export default ListMeetings;
ListMeetings.css
.list-meetings {
  padding-top: 60px;
}

.list-meetings-top {
  position: fixed;
  width: 100%;
  top: 0;
  border-bottom: 1px solid #d5d8df;
  display: flex;
}

.meeting-list {
  width: 100%;
  margin: 0;
  padding: 0;
  list-style-type: none;
}

Step 4

Let's update the App component to:

  1. Hold state (meetings data)
  2. Pass the state down to child components (ListMeetings)

Recall, in this course, we use function components for state-less components. If your component has state, refactor it to a class component.

import { Component } from "react";

class App extends Component {
  state = {
    meetings: [
      {
        _id: "5faf5ab7043e87536e31e54e",
        course: "EN.601.226 Data Structure",
        instructor: "Ali Madooei",
        time: "MWF 12:00 - 1:15 PM",
        link: "https://wse.zoom.us/j/91907049828",
      },
      {
        _id: "5faf5ab7043e87536e31e54f",
        course: "EN.601.226 Data Structure",
        instructor: "Ali Madooei",
        time: "MWF 1:30 - 2:45 PM",
        link: "https://wse.zoom.us/j/99066784665",
      },
      {
        _id: "5faf5ab7043e87536e31e550",
        course: "EN.601.280 Full-Stack JavaScript",
        instructor: "Ali Madooei",
        time: "TuTh 12:00 - 1:15 PM",
        link: "https://wse.zoom.us/j/93926139464",
      },
      {
        _id: "5faf5ab7043e87536e31e551",
        course: "EN.601.280 Full-Stack JavaScript",
        instructor: "Ali Madooei",
        time: "TuTh 1:30 - 2:45 PM",
        link: "https://wse.zoom.us/j/91022779135",
      },
    ],
  };

  render() {
    return (
      <div style={{ color: "white" }}>
        {JSON.stringify(this.state.meetings)}
      </div>
    );
  }
}

export default App;

Notice we can store the state directly as a field (no need to use the constructor). Class fields are not yet available in JavaScript but in React you can use them and it will be transpiled to standard JavaScript for you.

Aside: With new versions of React, you can employ Hooks to use state (and other React features) without writing a class. Do not use hooks for your homework!

Step 5

Let's add ListMeetings component to App and pass App's state to it as props.

Fist, import ListMeetings:

import ListMeetings from "./ListMeetings.js";

Next, updated the return statement of the render method:

- return (
-   <div style={{ color: "white" }}>
-     {JSON.stringify(this.state.meetings)}
-   </div>
- );
+ return <ListMeetings meetings={this.state.meetings} />;

Step 6

Let's update the ListMeetings component to iterate over the "meetings" and pass each meeting object to the Meeting component:

  return (
    <div className="list-meetings">
      <div className="list-meetings-top">
        <Search />
      </div>
      <ol className="meeting-list">
        {props.meetings.map((meeting, index) => (
          <Meeting meeting={meeting} key={index} />
        ))}
      </ol>
    </div>
  );
}

Notice the use of {} to use JavaScript in JSX!

Also note the use of the key attribute.

Step 7

Let's update the Meeting component to display the information for each meeting (which it receives as props):

function Meeting(props) {
  const { meeting } = props;
  return (
    <li className="meeting-list-item">
      <div className="meeting-details">
        <p className="title">{meeting.course}</p>
        <p>{meeting.instructor}</p>
        <p>{meeting.time}</p>
        <p>
          <a href={meeting.link}>{meeting.link}</a>
        </p>
      </div>
    </li>
  );
}

Notice the use of destructuring assignment to get meeting and key values out of the props object.

Save the file and revisit the app in your browser.

Step 8

In the wireframe, there is a QR code next to each meeting.

The QR shall contain the Zoom link for that meeting. Let's add that to our application.

We can now take a break from this app, go spend a few weeks reading about QR, trying to implement it, test it, and then come back here an add it to our application! Alternatively, we can do a Google search for "QR React component" (as I did) and find ready to use component such as qrcode.react on NPM.

Stop the application and install qrcode.react:

npm install qrcode.react

Open Meeting.js and import qrcode.react:

import QRCode from "qrcode.react";

Add a QR to the React element returned from the Meeting component:

return (
return (
  <li className="meeting-list-item" key={key}>
+   <div>
+     <QRCode value={meeting.link} />
+   </div>
    <div className="meeting-details">
      <p className="title">{meeting.course}</p>
      <p>{meeting.instructor}</p>
      <p>{meeting.time}</p>
      <p>
        <a href={meeting.link}>{meeting.link}</a>
      </p>
    </div>
  </li>
);

Save the file and run the application. Open http://localhost:3000 to view it in the browser.

Aside: This is why JavaScript is the solution fo rapid software prototyping. It took us a Google search and perhaps 5 minutes of reading to added a QR component to our application!

Step 9

Update the Meeting component to include a "delete" button, next to each meeting:

return (
  <li className="meeting-list-item" key={key}>
    <div>
      <QRCode value={meeting.link} />
    </div>
    <div className="meeting-details">
      <p className="title">{meeting.course}</p>
      <p>{meeting.instructor}</p>
      <p>{meeting.time}</p>
      <p>
        <a href={meeting.link}>{meeting.link}</a>
      </p>
    </div>
+   <button className="meeting-remove"></button>
  </li>
);

Save the file and view the app in the browser.

Notice the button does not do anything yet!

Step 10

Let's add functionality to the remove button.

When we remove a meeting, it must be removed from the "state" of the App component. Then, the App component will pass the updated state to all the child component that rely on it so they all get updated (rendered again).

Therefore, the logic for deleting a meeting must exist in the App component. We can create a function (method) for this and pass it down to the Meeting component. The meeting component can then attach this "delete" method to the "onClick" event of the remove button.

Add the following method to App component:

removeMeeting = (meeting) => {
  this.setState((state) => {
    return {
      meetings: state.meetings.filter((m) => m._id !== meeting._id),
    };
  });
};

Notice setState is called with a function argument. This function receives the "current" state and it will return the "modified" (updated) state.

Let's now pass this method as props to the Meeting component; we must first pass it from App to ListMeetings:

- <ListMeetings meetings={this.state.meetings} />
+ <ListMeetings
+  meetings={this.state.meetings}
+  onDeleteMeeting={this.removeMeeting}
+ />

And then from ListMeetings to Meeting:

- <Meeting meeting={meeting} key={index} />
+ <Meeting
+   meeting={meeting}
+   key={index}
+   onDeleteMeeting={props.onDeleteMeeting}
+ />

Finally, update hook this method to the "onClick" event of the remove button in Meeting component:

- <button className="meeting-remove"></button>
+ <button
+   className="meeting-remove"
+   onClick={() => props.onDeleteMeeting(meeting)}
+ ></button>

Save the file and view the app in the browser.

Step 11

Let's update the Search component:

function Search() {
  return <input className="search-meetings" type="text" placeholder="search" />;
}

Save the file and view the app in the browser.

We want to filter the meetings based on the search (query) term which a client provides. We need to

  1. Store the "query" in a variable.
  2. Filter the meetings based on the "query".

To store the query, we have several options. I made a design decision to store the query as the state of the ListMeetings component. I did this so I can filter the meetings in the ListMeetings component before iteratively passing each meeting to the Meeting component.

So, let's update the ListMeeting to a class component with state!

import "./ListMeetings.css";
import Search from "./Search.js";
import Meeting from "./Meeting.js";
import { Component } from "react";

class ListMeetings extends Component {
  state = {
    query: ""
  }

  render() {
    return (
      <div className="list-meetings">
        <div className="list-meetings-top">
          <Search />
        </div>
        <ol className="meeting-list">
          {this.props.meetings.map((meeting, index) => (
            <Meeting
              meeting={meeting}
              key={index}
              onDeleteMeeting={this.props.onDeleteMeeting}
            />
          ))}
        </ol>
      </div>
    );
  }
}

export default ListMeetings;

Step 12

Let's add a method to ListMeetings component to update the query:

updateQuery = (query) => {
  this.setState({ query });
};

We can now pass the query and the updateQuery method to the Search component:

- <Search />
+ <Search query={this.state.query} updateQuery={this.updateQuery} />

Finally, update the Search component to use the props provided to it:

import "./Search.css";

function Search(props) {
  const { query, updateQuery } = props;
  return (
    <input
      className="search-meetings"
      type="text"
      placeholder="search"
      value={query}
      onChange={(event) => updateQuery(event.target.value)}
    />
  );
}

export default Search;

For sanity check, we can update the updateQuery method to print the updated query to the console:

updateQuery = (query) => {
  this.setState({ query });
+ console.log(this.state.query);
};

Save the file and view the app in the browser. Open the Browser Console and type in a query in the search bar.

Step 13

Let's filter the meetings based on the search query.

We will use two NPM packages, escape-string-regexp and sort-by, to help us with this.

Stop the app and install the packages.

npm install escape-string-regexp sort-by

Import these packages in the ListMeetings component:

import escapeRegExp from "escape-string-regexp";
import sortBy from "sort-by";

Update the ListMeetings.render method:

render() {
  const query = this.state.query.trim();
  let meetings = [];
  if (query) {
    const match = new RegExp(escapeRegExp(query), "i");
    meetings = this.props.meetings.filter(
      (m) =>
        match.test(m.course) || match.test(m.instructor) || match.test(m.time)
    );
  } else {
    meetings = this.props.meetings;
  }

  meetings.sort(sortBy("course"));

  return (
    <div className="list-meetings">
      <div className="list-meetings-top">
        <Search query={this.state.query} updateQuery={this.updateQuery} />
      </div>
      <ol className="meeting-list">
        {meetings.map((meeting, index) => (
          <Meeting
            meeting={meeting}
            key={index}
            onDeleteMeeting={this.props.onDeleteMeeting}
          />
        ))}
      </ol>
    </div>
  );
}

Save the file and view the app in the browser. Type in a query in the search bar!

Read this to understand the code better!

The following statement simply sorts the meetings by the "course" attribute.

meetings.sort(sortBy("course"));

The sort method sorts the elements of an array in place and returns the sorted array. It optionally takes in a function that defines the sort order. This is what sortBy does for us. I have used it to sort the "meeting" objects based on "course" attribute. The sort-by package more generally allows for sorting by multiple properties.

The following code snippet

const match = new RegExp(escapeRegExp(query), "i");
meetings = this.props.meetings.filter(
  (m) =>
    match.test(m.course) || match.test(m.instructor) || match.test(m.time)
);

We create a "Regular Expression" (regex) object and use that to test if the search query matches information about a meeting.

The regex will not work correctly when a user input (search query) contains inputs such as ? or * because those inputs have special meaning in a regex expression. The escapeRegExp helps us to circumvent this potential issue.

Regular expressions are too complex to get into in this program, but they're incredibly valuable in programming to verify patterns.

Check out what MDN has to say about Regular Expressions. Also, check out how the String .match() method uses Regular expressions to verify patterns of text.

Zirectory (Part II)

Recall "Zirectory" (Zoom Directory) is an application that contains a collection of Zoom meeting links for JHU courses. Faculty can add their courses' Zoom link to this directory and students can search to find the links, for fast and easy access to their lectures.

In this part, we will add a form (controlled component) to allow faculty provide information about their course "meetings". We will use React Router to provide transitions when navigating between different URL locations within our app.

The repository for part-2 (completed) is here.

Step 1

Here are the wireframes of the UI which we are going to build. We will update the UI for homepage to add a button with + icon to the search bar:

Once a user clicks on the + icon, they will be redirected to the /create endpoint with the following view:

Step 2

Create a file CreateMeeting.js with the following content:

function CreateMeeting() {
  return <div>Create Meeting</div>;
}

export default CreateMeeting;

Update ListMeetings.js to include a link to navigate to "create meeting" view!

<div className="list-meetings-top">
  <Search query={this.state.query} updateQuery={this.updateQuery} />
+ <a href="/create" className="add-meeting">Add Meeting</a>
</div>

Add the following styling to ListMeeting.css

.add-meeting {
  display: block;
  width: 73px;
  background: white;
  background-image: url("./icons/add.svg");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 28px;
  font-size: 0;
}

Save the file and revisit the app in your browser. Notice the icon added to the search bar!

Click on the icon; although the URL changes to http://localhost:3000/create, the view does not change!

Step 3

We need to build a (web) server, with NodeJS/Express for instance, to handle routing requests for different endpoints; when a user visits /create we must response with the HTML file (and associated styling/script files) for "create meeting" view.

In React apps, we can get away by using third-party libraries for routing. A popular choice is react-router which provides a collection of navigational components for React applications.

Stop your application and install react-router and the related package react-router-dom:

npm install react-router react-router-dom

Working with react-router is relatively simple!

Open index.js and import the BrowserRouter from the react-router package:

import { BrowserRouter as Router } from "react-router-dom";

Now wrap the App component:

ReactDOM.render(
  <Router>
    <App />
  </Router>,
  document.getElementById("root")
);

Next, open App.js and import the BrowserRouter from the react-router package:

import { Route, Switch } from "react-router";

Update the render method of App component:

render() {
  return (
    <Switch>
      <Route exact path="/">
        <ListMeetings
          meetings={this.state.meetings}
          onDeleteMeeting={this.removeMeeting}
        />
      </Route>
      <Route path="/create">
        <CreateMeeting />
      </Route>
    </Switch>
  );
}

Save the file and run the application. Open http://localhost:3000 to view it in the browser. Click on the add icon to navigate to the "create meeting" view and click on the back button to return to the homepage.

The react-router library manipulates and makes use of browser's session history to provide a sense of routing!

Libraries like react-router are "hacking" routing! In my experience, there are many problems with this approach ranging from when you deploy your application to a service like GitHub Pages (where the base URL is not the domain) to issues with revisiting a page after you have updated its state from another view.

There are alternatives to react-router and you can build your own too. I recommend the following readings if you are interested to learn more:

Step 4

Let's build the "create meeting" view!

Update the CreateMeeting.js file as follows:

import "./CreateMeeting.css";

function CreateMeeting() {
  return (
    <div>
      <a href="/" className="close-create-meeting">Close</a>
      <form className="create-meeting-form">
        <div className="create-meeting-details">
          <input type="text" name="course" placeholder="Course" />
          <input type="text" name="instructor" placeholder="Instructor" />
          <input type="text" name="time" placeholder="Meeting Times" />
          <input type="url" name="link" placeholder="Meeting Link" />
          <button>Add Meeting</button>
        </div>
      </form>
    </div>
  );
}

export default CreateMeeting;
And here is the styling CreateMeeting.css
.close-create-meeting {
  display: block;
  width: 60px;
  height: 60px;
  background-image: url("./icons/arrow-back.svg");
  background-position: center;
  background-repeat: no-repeat;
  background-size: 30px;
  font-size: 0;
  color: white;
}

.create-meeting-form {
  background-color: white;
  padding: 40px 40px 20px 20px;
  max-width: 500px;
  margin: 0 auto;
  display: flex;
}

.create-meeting-details {
  margin-left: 20px;
}

.create-meeting-details input {
  width: 100%;
  padding: 5px 10px;
  margin-bottom: 10px;
  font-size: inherit;
  background: transparent;
  border: none;
  border-bottom: 1px solid #ccc;
  outline: 0;
}

.create-meeting-details button {
  margin-top: 20px;
  background: #ccc;
  padding: 10px;
  text-transform: uppercase;
  font-size: inherit;
  border: none;
  outline: 0;
}

Save the file and revisit the app in your browser. Visit the "create meeting" view at http://localhost:3000/create:

Step 5

HTML form elements naturally keep some internal state and update it based on user input. In React, mutable state is kept in the state property of components, and only updated with setState(). We should combine the two by making the React state be the "single source of truth".

An input form element whose value is controlled by React is called a controlled component.

This means the React component that renders a form also controls what happens in that form on subsequent user input.

So, let's update the CreateMeeting component to be a class component with state:

import { Component } from "react";
import "./CreateMeeting.css";

class CreateMeeting extends Component {
  constructor(props) {
    super(props);
    this.state = {
      course: "",
      instructor: "",
      time: "",
      link: "",
    };
  }

  render() {
    return (
      <div>
        <a href="/" className="close-create-meeting">
          Close
        </a>
        <form className="create-meeting-form">
          <div className="create-meeting-details">
            <input type="text" name="course" placeholder="Course" />
            <input type="text" name="instructor" placeholder="Instructor" />
            <input type="text" name="time" placeholder="Meeting Times" />
            <input type="url" name="link" placeholder="Meeting Link" />
            <button>Add Meeting</button>
          </div>
        </form>
      </div>
    );
  }
}

export default CreateMeeting;

Step 6

To ensure the form elements use the component state, we will make a two-way integration so that

  1. the form element "value" is always the value stored in component state
  2. the component state is updated when the form value changes (as a result of user interaction)

For each HTML form, update value and onChange attributes; for example for input element for "course" make the following changes

<input
   type="text"
   name="course"
   placeholder="Course"
+  value={this.state.course}
+  onChange={this.handleChange}
/>

Since the value attribute is set on our form element, the displayed value will always be this.state.course, making the Component state the source of truth. The handleChange (which we have not implemented yet) will run on every keystroke to update the Component state as the user types.

Here is the complete updated render method
render() {
  return (
    <div>
      <a href="/" className="close-create-meeting">
        Close
      </a>
      <form className="create-meeting-form">
        <div className="create-meeting-details">
          <input
            type="text"
            name="course"
            placeholder="Course"
            value={this.state.course}
            onChange={this.handleChange}
          />
          <input
            type="text"
            name="instructor"
            placeholder="Instructor"
            value={this.state.instructor}
            onChange={this.handleChange}
          />
          <input
            type="text"
            name="time"
            placeholder="Meeting Times"
            value={this.state.time}
            onChange={this.handleChange}
          />
          <input
            type="url"
            name="link"
            placeholder="Meeting Link"
            value={this.state.link}
            onChange={this.handleChange}
          />
          <button>Add Meeting</button>
        </div>
      </form>
    </div>
  );
}

We must now implement the handleChange method:

handleChange(event) {
  this.setState({ [event.target.name]: event.target.value });
  console.log(this.state);
}

The console.log is for sanity check during development. We will remove it after testing that the method behaves as expected. Don't forget to bind the method to the CreateMeeting class; add the following statement to its constructor:

this.handleChange = this.handleChange.bind(this);

Save the file and revisit the app in your browser.

With a controlled component, the input's value is always driven by the Component's state. This may seem unnecessary to you but notice we can now pass the values in out form element to other UI elements too, or reset it from other event handlers.

Step 7

We must now handle the "form submission", that is when user clicks on "ADD MEETING".

Add the following method to CreateMeeting component:

handleSubmit(event) {
  alert("A meeting was added: " + JSON.stringify(this.state));
  event.preventDefault();
}

This method, add the moment, only shows an alert that form is submitted.

Note: A form has the default HTML form behavior of browsing to a new page when the user submits the form. Here the handleSubmit "prevents" the default behavior and gives you more control on what data to be submitted to what endpoint. We will update the handleSubmit to "create" a new meeting (save it to the state of the App).

Make sure to bind handSubmit to CreateMeeting; add the following to the constructor:

this.handleSubmit = this.handleSubmit.bind(this);

Finally, add onSubmit attribute to the form element:

- <form className="create-meeting-form" >
+ <form className="create-meeting-form" onSubmit={this.handleSubmit}>

Save the file and revisit the app in your browser, and try to add a meeting.

You can learn more on using HTML forms in React application by reading the React Documentations: Forms. There are also third-party React components, such as formik, that you can use in place of plain forms.

Step 8

We need to add the meeting to the state of App (similar to how we removed a meeting from it).

Open App.js and add the following method to the App class:

addMeeting = (meeting) => {
  this.setState({
    meetings: this.state.meetings.concat(meeting),
  });
};

Pass this method as a prop to CreateMeeting component:

- <CreateMeeting />
+ <CreateMeeting onCreateMeeting={this.addMeeting} />

Update the handleSubmit method in CreateMeeting:

handleSubmit(event) {
- alert("A meeting was added: " + JSON.stringify(this.state));
+ this.props.onCreateMeeting(this.state)
  event.preventDefault();
+ this.props.history.push("/");
}

The last statement allows us to "redirect" to the homepage. It does so using the history object in react-router library (which is based on the browser history API).

The history object is not passed as props, by default. To do so, you must wrap the CreateMeeting component in the withRouter higher-order component from the react-router package:

  1. Import withRouter in CreateMeeting.js
    import { withRouter } from "react-router";
    
  2. Update the export statement in CreateMeeting.js
    export default withRouter(CreateMeeting);
    

Save the file and revisit the app in your browser, and try to add a meeting.

Step 9

The Zirectory application is complete in terms of its functionality. We will now polish the source code by changing/adding a few things.

First, let's replace anchor elements (HTML <a> element) with react-router-dom Link component.

The Link component allows you to pass query parameters or link to specific parts of a page. It also allows you to pass "state" to the Component which you want to navigate to.

Open ListMeetings.js and import the Link component:

import { Link } from "react-router-dom";

Now update the anchor element to a Link component

- <a href="/create" className="add-meeting">Add Meeting</a>
+ <Link to="/create" className="add-meeting" />

Notice the href attribute on a element is changed to to props on the Link component.

Open CreateMeetings.js, import the Link component, and update the anchor element to a Link component:

- <a href="/" className="close-create-meeting">Close</a>
+ <Link to="/" className="close-create-meeting" />

Save the file and revisit the app in your browser; everything must look and function as before!

Step 10

Notice that react bundles all input data to a Component into an object called props. It is difficult to keep track of (and validate) the props that a component receives. To mitigate this shortcoming, the folks who created React also created a package called prop-types.

Stop the application and install prop-types:

npm install prop-types

Open the ListMeetings.js and import prop-types:

import PropTypes from "prop-types";

If you look inside App.js you will find we send two props to ListMeetings component

<ListMeetings
  meetings={this.state.meetings}
  onDeleteMeeting={this.removeMeeting}
/>

Add the following to the end of ListMeetings.js:

ListMeetings.propTypes = {
  meetings: PropTypes.array.isRequired,
  onDeleteMeeting: PropTypes.func.isRequired
};

Save the file and run the app; go to App.js and comment out onDeleteMeeting={this.removeMeeting}. Save the file and check out the browser console.

Notice how the PropTypes package gives you a warning.

PropTypes is made to "type check" the props. As an added advantage, you can use it as a documentation of the props which your component receives.

PropTypes can be made to check the nested structure of your props:

ListMeetings.propTypes = {
  meetings: PropTypes.arrayOf(
    PropTypes.shape({
      _id: PropTypes.string,
      course: PropTypes.string.isRequired,
      instructor: PropTypes.string.isRequired,
      time: PropTypes.string.isRequired,
      link: PropTypes.string.isRequired,
    })
  ).isRequired,
  onDeleteMeeting: PropTypes.func.isRequired,
};

You can learn more about PropTypes here.

Step 11

Let's add PropTypes to other components as well.

Open Meetings.js and import PropType:

import PropTypes from "prop-types";

Looking at the ListMeetings component, the Meeting component receives two props that it uses; the key is used for React's reconciliation algorithm.

<Meeting
  meeting={meeting}
  key={index}
  onDeleteMeeting={this.props.onDeleteMeeting}
/>

Add the following to the end of Meetings.js file:

Meeting.propTypes = {
  meeting: PropTypes.shape({
    _id: PropTypes.string,
    course: PropTypes.string.isRequired,
    instructor: PropTypes.string.isRequired,
    time: PropTypes.string.isRequired,
    link: PropTypes.string.isRequired,
  }).isRequired,
  onDeleteMeeting: PropTypes.func.isRequired,
};

Open Search.js and import PropType. Add the following to the end of this file:

Search.propTypes = {
  query: PropTypes.string,
  updateQuery: PropTypes.func.isRequired,
};

Open CreateMeeting.js and import PropType. Add the following to the end of this file:

CreateMeeting.propTypes = {
  onCreateMeeting: PropTypes.func.isRequired,
};

Save the file and revisit the app in your browser; everything must look and function as before!

Step 12

As the final step, I want to bring your attention to React Developer Tools, a browser extention that adds React debugging tools to the Chrome Developer Tools.

Install it. Run the a and open the browser Developer Tools.

Notice there are two new tabs: Components and Profiler. You can use these tabs to navigate through the React components (viewing their props, state, etc) or profile the performance of your application.

To learn more about this, I recommend reading How to use the React Developer Tools.

Zirectory (Part III)

Recall "Zirectory" (Zoom Directory) is an application that contains a collection of Zoom meeting links for JHU courses. Faculty can add their courses' Zoom link to this directory and students can search to find the links, for fast and easy access to their lectures.

In this part, we will connect the app to a backend that provides persistence, and then deploy the application.

The repository for part-3 (completed) is here.

Step 1

I have created the backend (API + persistence mechanism) for the Zirectory App. You can find it here.

  • Clone the repository.
  • Open the terminal and change directory to zirectory-api.
  • Install the dependencies: npm install
  • Create a .env file with the following content:
    DB_URI="YOUR_MONGODB_URI"
    
    • You must have a MongoDB cluster running in the cloud.
    • If not, create one, then obtain its URI to connect to it.
    • Make sure to replace <password> with your database admin password.
    • And, replace the <dbname> with the desired database name.
  • Run the backend using nodemon . (It will run at http://localhost:4567)
  • You can download this minimal Postman request collection to test the API.

Step 2

Run the Zirectory App alongside the backend server.

Open the App.js file. Notice we initialized the state with sample meetings. Let's delete these!

class App extends Component {
  state = {
    meetings: [],
  };

  // other methods not shown
}

We must now read the meetings data from the backend. The API endpoint to GET the meetings is

http://localhost:4567/api/meetings

We can easily get the data using axios; let's stop the app and install it:

npm install axios

To make a GET request to our backend, we can use axios.get:

axios.get("http://localhost:4567/api/meetings")
  .then(response => this.setState({ meetings: response.data.data }))
  .catch(err => console.log(err));

The question is: where should we put the above statement? In the constructor of App? In App.render()?

Step 3

There are several special methods declared in the React Component class that are called Lifecycle Methods.

React (automatically) calls the lifecycle methods at certain times during the "life" of a Component.

Here are the most commonly used ones (but there are more):

  • componentWillMount(): invoked immediately before the component is inserted into the DOM.
  • componentDidMount(): invoked immediately after the component is inserted into the DOM.
  • componentWillUnmount(): invoked immediately before a component is removed from the DOM.
  • componentWillReceiveProps(): invoked whenever the component is about to receive brand new props.

In fact, the constructor and the render methods are also considered lifecycle methods!

The image above is a screenshot from interactive react lifecycle methods diagram.

The componentDidMount() is the (lifecycle) method which is commonly used to read data from an external API.

Override componentDidMount() in App:

componentDidMount() {
  axios
    .get("http://localhost:4567/api/meetings")
    .then((response) => this.setState({ meetings: response.data.data }))
    .catch((err) => console.log(err));
}

Save the file and revisit the app in your browser; everything must look and function as before! You must see the sample meetings data but this time they are retrieved from the backend.

Read The React Component Lifecycle on Medium to learn more about it. There is also a section on React's documentation called State and Lifecycle which I recommend reading.

Caution: After introduction of "Hooks" in React, some consider lifecycle methods as legacy approach which should be replaced with use of hooks. Here is a good read on that topic: Replacing Lifecycle methods with React Hooks.

Step 4

When we delete a meeting, we must send a DELETE request to the backend to keep it in sync with the state of the app.

removeMeeting = (meeting) => {
  this.setState((state) => {
    return {
      meetings: state.meetings.filter((m) => m._id !== meeting._id),
    };
  });

  axios
    .delete(`http://localhost:4567/api/meetings/${meeting._id}`)
    .catch((err) => console.log(err));
};

Save the file and revisit the app in your browser. Delete a meeting and then refresh the app. The deleted meeting must be gone for good!

Step 5

When we add a meeting, we must send a POST request to the backend to keep it in sync with the state of the app.

addMeeting = (meeting) => {
  axios
    .post("http://localhost:4567/api/meetings", meeting)
    .then((response) =>
      this.setState({
        meetings: this.state.meetings.concat(response.data.data),
      })
    )
    .catch((err) => console.log(err));
};

Notice that I first create the meeting on the backend and then add the created meeting to the state. I do this to get the "ID" of the created meeting. Without the ID, we will not be able to delete the meeting.

Save the file and revisit the app in your browser. Add a meeting and then refresh the app. The added meeting must be there after the refresh!

Aside: There is a handy Axios cheat-sheet here.

Step 6

The App component now contains the logic for retrieving the data from the backend. This violates the Single Responsibility Principle. Let's refactor the code to separate the responsibilities.

Create a file ZirectoryApi.js with the following content:

import axios from "axios";

const BASE_URL = "http://localhost:4567";

async function getAll() {
  try {
    const response = await axios.get(`${BASE_URL}/api/meetings`);
    return response.data.data;
  } catch (err) {
    console.log(err);
    return [];
  }
}

async function remove(id) {
  try {
    const response = await axios.delete(`${BASE_URL}/api/meetings/${id}`);
    return response.data.data;
  } catch (err) {
    console.log(err);
    return null;
  }
}

async function add(meeting) {
  try {
    const response = await axios.post(`${BASE_URL}/api/meetings`, meeting);
    return response.data.data;
  } catch (err) {
    console.log(err);
    return null;
  }
}

export { getAll, remove, add };

Step 7

Let's update the App component to use the functions defined in ZirectoryApi.

First, import the functions:

import * as ZirectoryApi from "./ZirectoryApi.js";

Next, update componentDidMount():

componentDidMount() {
  ZirectoryApi
    .getAll()
    .then((meetings) => this.setState({ meetings }));
}

Then, update removeMeeting:

removeMeeting = (meeting) => {
  this.setState((state) => {
    return {
      meetings: state.meetings.filter((m) => m._id !== meeting._id),
    };
  });

  ZirectoryApi.remove(meeting._id);
};

Finally, update addMeeting:

addMeeting = (meeting) => {
  ZirectoryApi.add(meeting).then((m) =>
    this.setState({
      meetings: this.state.meetings.concat(m),
    })
  );
};

As the result of refactoring, the logic for working with the backend API is taken out of the App component and its operations are now simplified.

Save the file and revisit the app in your browser; everything must look and function as before! You must see the sample meetings data and be able to add/remove meetings.

Step 8

When you finally ready to deploy your React app (as we are right now), you can create a production bundle using the following command:

npm run build

This will generate an optimized build of your React application, ready to be deployed. The generated artifacts will be placed in the build.

To check it out locally, you should serve the content of the build folder (so don't directly open index.html). One of the easiest way to do this is to install serve and let it handle the rest:

npm install -g serve

Notice that I installed this package "globally".

After installation, serve your static site using the following command:

serve -s build

Note that you must be in the root of your application (zirectory-app folder) to run the above command.

By default, serve runs your site on port 5000.

The port can be adjusted using the -l or --listen flags:

serve -s build -l 4000

For more information, consult the documentation of serve.

Step 9

We have created a production bundle for our application. It is time to deploy it.

There are many options available to deploy your React app, from GitHub Pages to Heroku. Some of these are enumerated in the article 10 ways to deploy a React app for free by LogRocket. A more general discussion of the topic can be found in create-react-app documentation on deployment.

I found Netlify a good option for deploying React applications. Netlify is a service for deploying JAMStack applications. (If you are wondering what is JAMStack, read What (the Hell) Is the JAMstack?!)

Netlify has an article, Deploy React Apps in less than 30 Seconds, which we will follow here with some adjustments.

First, you need to sign up for a free account on Netlify.

Next, install Netlify CLI:

npm install -g netlify-cli

Then, connect your Netlify CLI to your Netlify account using the following command in the terminal:

netlify login

The command will open up your browser and take you to Netlify to grant permission.

Finally, deploy your React app using Netlify CLI. You must first change directory to zirectory-app folder:

netlify deploy

The command above will present you with a number of prompts.

Notice, when you are asked for "deploy path", you must answer "build" which is the folder that contains your optimized React app ready for production.

This is going to create a website and deploy it to a draft URL first which you can view at the provided URL. The draft URL is like a staging environment for previewing and testing your app. Once you are happy with the deployed app, you can take it live by the following command:

netlify deploy --prod

This will ask one more time for the "deploy path" which you must answer with "build" (to point to the build folder). It then deploys the app and gives you two URLs:

I get two URLs: a Unique Deploy URL which represents the unique URL for each individual deployment and a Live URL which always displays your latest deployment.

Each time you update your site and deploy it with Netlify deploy, you're going to get a unique URL for that deployment.

So if you deploy your app multiple times, you will have multiple unique URLs so that you can point users to specific versions of your app. But the live URL always displays your latest changes at the same URL. This is one of the features that I really like about Netlify!

Another thing I like about Netlify is that it automatically secures your site over HTTPS at no cost.

Step 10

If you're publishing a React app that uses a router that is based on HTML5 History API (like react-router, as we do), you'll need to configure redirects and rewrite rules for your URLs. To understand the issue, read Serving Apps with Client-Side Routing on create-react-app documentation.

Netlify makes configuring redirects and rewrite rules for your URLs really easy. You'll need to add a file inside the built folder of your app named, _redirects. Inside the file, include the following rewrite rule.

/*    /index.html   200

Then, run the deploy command again and follow the prompt:

netlify deploy

Step 11

I have deployed the backed on Heroku and updated the frontend to use the deployed backed instead of the local backend. In doing so, I decided to read the BASE_URL variable for the API server from environment variables.

React actually uses the dotenv library under the hood. You can work with environment variables this way:

  • create .env file in the root of the project
  • set environment variables starting with REACT_APP_
  • access the variable by process.env.REACT_APP_... in components

For example, I created a .env file with the following content:

REACT_APP_BASE_URL=http://localhost:4567

And, updated ZirectoryApi.js file:

- const BASE_URL = "http://localhost:4567";
+ const BASE_URL = process.env.REACT_APP_BASE_URL;

Running the backend and frontend locally, this will work. For deployment, changed the REACT_APP_BASE_URL to the deployed backend and then build the frontend again (with npm run build). Finally, deploy the frontend using netlify deploy command (don't forget to include the _redirects file in the build folder, first).

Feel free to visit the deployed app at https://zirectory.netlify.app:

Appendix

Recorded Lectures

LectureTopicsNotesCode
9/1Course Overview, SleepTime App1.1 - 2.7repl.it
9/3SleepTime App, JavaScript Basics2.8 - 3.5repl.it
9/8JavaScript Basics, Git/GitHub3.6 - 4.4
9/10Git/GitHub, Control Flow4.5 - 5.1GitHub
9/15Control Flow, TicTacToe5.2 - 6.8GitHub
9/17Functions7.1 - 7.14
9/22Brick Breaker8.1 - 8.17GitHub
9/24Class (HW1 out)9.1 - 8.11
9/29SIS API & Fetch10.1 - 10.17GitHub
10/1Asynchronous Programming11.1 - 10.12
10/6Modules, NodeJS12.1 - 13.5
10/8NodeJS, Express13.6 - 14.7GitHub
10/13Express, Heroku14.8 - 15.2GitHub
10/15YouNote API16.1 - 16.9GitHub
10/20YouNote API Refactored16.10 - 17.4GitHub
10/29YouNote Refactor, MongoDB17.5 - 18.6GitHub
11/3MongoDB, YouNote Persisted18.7 - 19.3GitHub
11/5YouNote API Persisted & App19.4 - 20.4GitHub
11/10YouNote App & Users App20.5 - 21.9GitHub
11/12Users App21.10 - 21.20GitHub
11/17React: The basics22.1 - 22.8
11/19React: The basics22.9 - 22.15GitHub
12/1Zirectory (Part I)23.1 - 23.12GitHub
12/3Zirectory (Part II)23.13 - 24.6GitHub
12/8Zirectory (Part III)24.7 - 25.11GitHub

HTML

HTML (HyperText Markup Language) is used to lay out the structure of a webpage.

HTML is made up of tags.

  • Tags generally come in pairs, with data being in between the tags.
  • Tags are indented to help visualize their hierarchy, but any indentation is purely stylistic.
  • Tags can also have attributes, which are data fields, sometimes required and sometimes optional, that provide additional information to the browser about how to render the data.

We will explore some common tags below.

Basic structure

  • <!DOCTYPE html> is placed at the start of an HTML file to indicate to the browser that HTML5 is being used.
  • <html></html>: contents of website
  • <head></head>: metadata about the page that is useful for the browser when displaying the page
  • <title></title>: title of the page
  • <body></body>: body of the page
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>

Basic tags

Explore the CodePen below for basic HTML tags and their effect.

See the Pen HTML Basic tags by Ali Madooei (@madooei) on CodePen.

Lists

  • <ul></ul>: unordered list
  • <ol></ol>: ordered list
  • <li></li>: list item (must be inside either <ul></ul> or <ol></ol>)

See the Pen HTML List tags by Ali Madooei (@madooei) on CodePen.

Link and Image

  • <a href="path/to/hello.html">Click here!</a>: link to hello.html, some URL, or some other content marked by id by passing #id to href
  • <img src="path/to/img.jpg" height="200" width="300">: image stored at src attribute, which can also be a URL
    • note that this is a single tag without an end tag
    • both height and width are optional (if one is omitted, the browser will auto-size the image), and can also take a percentage: height=50% to automatically scale the image to a certain portion of the page

See the Pen HTML img tag by Ali Madooei (@madooei) on CodePen.

Table

  • <table></table> : table
  • <tr></tr>: table row (must be inside <table></table>)
  • <th></th>: table header (must be inside <td></td>)
  • <td></td>: table data (cell) (must be inside <td></td>)

See the Pen HTML table tag by Ali Madooei (@madooei) on CodePen.

Forms

You can create forms in HTML to collect data; we will explore this later. Here is a CodePen for illustration of some basic form tags:

See the Pen HTML Basic form tags by Ali Madooei (@madooei) on CodePen.

div and span

Two special tags allow us to break up a our webpage into sections:

  • <div></div>: vertical division of a webpage
  • <span></span>: section of a webpage inside, for example, text

Both <div></div> and <span></span> don't really do much by themselves, but they allow for the labelling of sections of a webpage.

Different sections of a webpage can be referenced with the id and class attributes. ids uniquely identify elements, but there can be an arbitrary number of elements with a given class.

DOM

The Document Object Model is a way to conceptualize webpages by representing them as a interconnected hierarchy of nodes. In HTML, the nodes of the DOM would be the different tags and their contained data, with the <html></html> tag being at the very top of the tree. While this might seem a little unintuitive at first, this model will become very useful in the future.

See the Pen HTML DOM by Ali Madooei (@madooei) on CodePen.

You will find a wealth of resources on HTML (and Web Development in general) at MDN Web Docs and W3School.

CSS

CSS (Cascading Style Sheets) is a language used to interact with and style HTML, changing the way it looks according to a series of rules. CSS is what makes websites look nice.

CSS can be applied to HTML in a variety of ways:

  • The inline style attribute: 

    See the Pen Inline CSS by Ali Madooei (@madooei) on CodePen.

  • The internal style defined inside <style></style> tags, inside the <head> section of HTML.

    See the Pen Internal CSS by Ali Madooei (@madooei) on CodePen.

    The CodePen above is styled with the following:

    <style>
      div {
        color: blue;
      }
    
      #danger {
        color: red;
      }
    
      .highlight {
        background-color: yellow;
      }
    </style>
    
  • A separate .css file:

    • add <link rel="stylesheet" href="path/to/styles.css"> to the header,
    • move the CSS code (same format as used inside the <style></style> tags.

    This is often an even better paradigm because it separates two distinctly different things: structure (HTML) and style (CSS), while also being easier to manage and read.

CSS Selectors

CSS selectors are used to select different parts of an HTML page to style in particular ways. In the above example, div, #danger and .highlight are CSS selectors (to select all div elements, the element with id="danger and all elements with class="highlight", respectively.)

Here are some fancier ways to work with the selectors:

  • Select h1 and h2 elements

    h1, h2 { 
      color: blue; 
    }
    
  • Select all p elements inside an element with class="note" 

    .note p { 
      color: blue; 
    }
    
  • Select all input fields with the attribute type=text

    input[type=text] { 
      color: blue; 
    }
    

Here it gets even fancier:

  • Add two # before all h2 elements:

    h2::before { 
      content: "##"; 
    }
    

    before is called a pseudo-element!

  • Change the color of a button when the cursor is hovering over it.

    button:hover { 
      background-color: gray; 
    }
    

    hover is called a pseudo-class!

CSS Properties

Here are some common CSS properties:

background-color: teal; 
color: blue; /* sets the content (text) color */
/* color can be one of ~140 named colors, or a hexadecimal value that represents an RGB value */ 

text-align: left; /* other possible values are center, right, or justify */

height: 150px; /* sets the height of an area */
width: 150px; /* sets the width of an area */
/* arguments in pixels often can take a percentage or relative values too */

margin: 30px; /* sets the margin around all four sides of an area */
/* margin also be broken up into "margin-left", "margin-right", "margin-top", and "margin-bottom" */
padding: 20px; /* sets the padding around text inside an area */
/* padding be broken up the same way as "margin" */

border: 3px solid blue; /* sets a border around an area */

font-family: Arial, sans-serif; /* sets the font family to be used */
font-size: 28px; /* sets the font size */
font-weight: bold; /* sets the font weight to quality, a relative measure ("lighter"), or a number ("200") */

There are lots of CSS properties that can be used in a lot of different ways. Check out the wonderfully extensive documentation for more information.

Git & GitHub

Jargons

  • Repository: A collection of files tracked by Git. For example, the files that make up the content of this website is kept in a Git repository.

  • Remote: Any version of a repository that is not stored locally on a device is called a "remote". (So, GitHub is a service for you to host remote repositories). "Origin" is used to refer to the "remote" from which the local repository was originally downloaded from.

  • Commit: Git does not save any changes made to the files within your repository until you "commit" it. So, as a verb, it is the action of storing a new snapshot of the repository's state in the Git history. When "commit" is used as a noun, it refers to a single point in the Git history.

  • Staging: Let's explain this one with an example; assume you made changes to 4 files within your repository, but you only want to commit 2 of them because the other 2 are buggy or not complete yet. How do you commit only 2? Well, you put them in the "staging area" after which you commit. So, staging a file means that you have marked it for a commit.

Summary of useful commands

  • git init: create an empty Git repository in a directory.
  • git add <filename(s)>: add files to the staging area to be included in the next commit
    • git add . : adds all files
  • git commit -m "message": take a snapshot of the repository and save it with a message about the changes
    • git commit -am "message": add files and commit changes all in one
  • git status : print what is currently going on with the repository
  • git log: print a history of all the commits that have been made
    • git log --pretty=oneline: list commit history in a compact format
  • git diff <commit> <commit>: show the changes made between the two commits (identified by their IDs)
  • git checkout <commit>: revert the repository back to a given commit. Use it if you want to discard changes to un-staged file/s.
  • git reset --hard <commit>: reset the repository to a given commit. Use it if you want to undo staging of a modified file.
  • git clone <url>: take a repository stored on a server (like GitHub) and downloads it
    • git clone <url> folder-name: clone to folder-name
    • git clone -b <branch> <url>: clone a specific branch
  • git push: push any local changes (commits) to a remote server
    • push only after you staged and committed the changes
  • git fetch: download all of the latest commits from a remote to a local device
  • git pull: pull any remote changes from a remote server to a local computer
  • git branch: list all the branches currently in a repository
  • git branch <name>: create a new branch called name
  • git checkout <name>: switch current working branch to name
  • git merge <name>: merge branch name into current working branch (normally master)
  • git merge origin/master: merge origin/master, which is the remote version of a repository normally downloaded with git fetch, into the local, preexisting master branch
  • git remote set-url origin https://github.com/USERNAME/REPOSITORY.git: changing a remote's URL

Here is a two-page PDF containing the most useful Git commands.

Learn More!

There are many resources to learn more about Git and GitHub. Check out javinpaul's article My Favorite Free Courses to Learn Git and Github — Best of Lot.

Not a fan of the Terminal!

If you don't like working with terminal to manage your git repository, you are in luck!

  • Most editors/IDEs have built in tools for working with Git/GitHub. For example, if you are using VSCode, checkout this article: Working with GitHub in VS Code.
  • There are also several great software that provide a graphical user interface (GUI) for using Git/GitHub. You may want to checkout gitkaraken, sourcetree, or gittower.

Styling Guidelines

There are many opinions on the "ideal" style in the world of Software Development. Therefore, in order to reduce the confusion on what style students should follow, we urge you to refer to this style guide.

General Formatting Rules

Trailing Whitespace

Remove trailing white spaces.

Trailing white spaces are unnecessary and can complicate diffs.

Example
const name = "John Smith";__

Recommended:

const name = "John Smith";

Indentation

Indentation should be consistent throughout the entire file. Whether you choose to use tabs or spaces, or 2-spaces vs. 4-spaces - just be consistent!

Encoding

Use UTF-8.

Make sure your editor uses UTF-8 as character encoding, without a byte order mark.

General Meta Rules

Comments

Use comments pragmatically. The code must explain itself. Add comments when further clarification is needed.

Action Items

Mark todos and action items with TODO:.

Highlight todos by using the keyword TODO only, not other formats like @@. Append action items after a colon like this: TODO: action item.

Example

Recommended:

// TODO: add other fruits

JavaScript Language Rules

Variable Declaration

Declare your variables with const, first. If you find that you need to reassign the variable later, use let. There isn't a good reason to use the var keyword anymore for variable declaration.

Semicolons

Always use semicolons.

Relying on implicit insertion can cause subtle, hard to debug problems. Semicolons should be included at the end of function expressions, but not at the end of function declarations.

Example

Not Recommended:

const foo = () => {
    return true // Missing semicolon
} // Missing semicolon

function foo() {
    return true;
}; // Extra semicolon

Recommended:

const foo = () => {
    return true;
};

function foo() {
    return true;
}

Wrapper Objects for Primitive Types

There's no reason to use wrapper objects for primitive types. However, type casting is okay.

Example

Not Recommended:

const x = new Boolean(0);
if (x) {
    alert("hi");    // Shows 'hi' because typeof x is truthy object
}

Recommended:

const x = Boolean(false);
if (x) {
    alert("hi");    // Show 'hi' because typeof x is a falsey boolean
}

Closures

Yes, but be careful.

The ability to create closures is one the most useful (and often overlooked) feature in JavaScript. One thing to keep in mind, however, is that a closure keeps a pointer to its enclosing scope. As a result, attaching a closure to a DOM element can create a circular reference and thus, a memory leak.

Example

Not Recommended:

function foo(element, a, b) {
    element.onclick = function() { /* uses a and b */ }
}

Recommended:

function foo(element, a, b) {
    element.onclick = bar(a, b);
}

function bar(a, b) {
    return function() { /* uses a and b */ }
}

Array and Object Literals

Use Array and Object literals instead of Array and Object constructors.

Example

Not Recommended:

const myArray = new Array(x1, x2, x3);

const myObject = new Object();
myObject.a = 0;

Recommended:

const myArray = [x1, x2, x3];

const myObject = {
    a: 0
};

JavaScript Style Rules

Naming

In general, functionNamesLikeThis, variableNamesLikeThis, ClassNamesLikeThis, methodNamesLikeThis, CONSTANT_VALUES_LIKE_THIS and filenameslikethis.js (unless the file contains a class in which case ClassNameLikeThis.js).

Code Formatting

Because of implicit semicolon insertion, always start your curly braces on the same line as whatever they're opening.

Example

Recommended:

if (something) {
    // Do something
} else {
    // Do something else
}

Not Recommended:

if (something) 
{
    // Do something
} 
else 
{
    // Do something else
}

Array/Object literals

Single-line array and object initializers are recommended when they fit on one line. Otherwise use multiline initializers.

Example

There should be no spaces after the opening bracket or before the closing bracket:

const array = [1, 2, 3];
const object = {a: 1, b: 2, c: 3};

Multiline array and object initializers are indented one-level, with the braces on their own line, just like blocks:

const array = [
    "Joe <joe@email.com>",
    "Sal <sal@email.com>",
    "Murr <murr@email.com>",
    "Q <q@email.com>"
];

const object = {
    id: "foo",
    class: "foo-important",
    name: "notification"
};

Parentheses, Brackets

Use them, even when you can get away with not using them. They generally make for a more readable code.

String literal

For consistency double-quotes (") are preferred over single-quotes (").

Use template literal for multiline strings.

Conditional Ternary Operator

The conditional ternary operator is recommended, although not required, for writing concise code.

Example

Not Recommended:

if (val) {
    return foo();
} else {
    return bar();
}

Recommended:

return val ? foo() : bar();

&& and ||

These binary boolean operators are short-circuited and evaluate to the last evaluated term. They are often used for purposes other than boolean expression.

Example

The || has been called the default operator because you can write this:

const foo = (name) => {
    const theName = name || 'John';
};

instead of:

const foo = (name) => {
    const theName;
    if (name) {
        theName = name;
    } else {
        theName = 'John';
    }
};

Using the boolean operators in capacity other than creating boolean expression is discouraged. There are usually better alternatives.

Example

You can do this for default argument:

const foo = (name = "John") => {
    const theName = name || 'John';
};

Quiz

Take-home assessments are popular these days considering the logistical challenges of remote teaching with many of our students in distant time zones. We will employ the same strategy for your Quiz. Essentially, we are replacing the Quiz with a tiny homework, so let's call it quiz-homework (or do you prefer the term take-home exam?).

  • On Tue, Oct 27, at 9:00 AM ET, we will release the quiz-homework.
  • The format will be similar to other homework (you will have a GitHub repository, you will link it to CodeGrade, etc.)
  • It is expected to take a prepared student no more than 75 minutes to finish the quiz-homework.
  • However, we will give you a window of 36 hours (to account for all sort of individual challenges you may be facing: time difference, other classes/homework/exams, etc.) to submit it.
  • So, the deadline will be Wed, Oct 28, at 9:00 PM ET.
  • Much like other homework, the quiz-homework will be open-book. You are allowed (and encouraged) to refer to your notes and other course materials/resources.
  • However, unlike homework, you are not going to have any help from the course staff.
  • Moreover, you must work alone on the quiz-homework. Do not discuss it with other students.
  • More to that point, you should not post any question about quiz-homework on Campuswire unless you have logistical issues or you need to ask any clarifying question. In such cases, please make a private (only visible to instructor and TAs) question on Campuswire.
  • Any violation of above-stated policies will be dealt with as a case of Academic Misconduct & Dishonesty.

You may not use a late-day towards the quiz-homework.

You may not ask for extension except under extenuating circumstances.

Examples of extenuating circumstances are illness, accidents or serious family problems. The assumption is that, during the 36 hours window, you will be able to put about 75 minutes of work into completing the quiz-homework.

We will not have a lecture on Tue, Oct 27, so you can put the lecture time towards working on the quiz-homework.

Grading and feedback: We will grade your submission similar to homework and release the grade/feedback once we are done grading all submissions.

Topics: The quiz covers the topic from lecture-1 (Course Overview, SleepTime App) up to and including lecture-15 (YouNote API (Refactored)).

JHU SSO

This tutorial explains how to use JHU SSO (Johns Hopkins University's Single Sign On) for user authentication in your web applications.

Download Completed App

Introduction: Terms & Jargons

JHU uses Shibboleth which is an open source software product that implements SAML (Security Assertion Markup Language) for user authorization and authentication.

  • Single Sign On (SSO): Any system where a single authentication provides access to multiple applications. For instance, you access Blackboard, SIS (Student Information System), JHU Email and several other JHU online services using the same credentials, though the same sing-in page.

  • Security Assertion Markup Language (SAML): A framework, and XML schema, for implementing Single Sign On.

  • Principal: The user who is attempting to gain access to our application.

  • Assertions: Data about the principal which are included as part of the SAML response.

  • Service Provider (SP): This is the application, or system, that the user is attempting to access; the app we are building is the service provider.

  • Protected Resource: A resource (typically a webpage) on SP that is identified by its URL. When a user accesses a protected resource, the SP will intercept the request and redirect the user to authenticate first to verify it has access to the protected resource.

  • Identity Provider (IdP): This is a remote application, or system, that authenticates the user and returns data back to the service provider. In our case, JHU is the identity provider. (Well, to be accurate, JHU has an internal IdP which we will use to authenticate our users.)

  • Globally Unique Identifier: A value that the IdP will use to identify an SP.

How does SSO work?

  • When a user attempts to gain access to a protected resource on SP, she will be redirected to SSO software solution to login.

  • User enters their username/password they have for SSO.

  • The SSO requests authentication from its internal IdP to verify user identity.

  • If authentication succeeds, SSO redirects the user back to the SP and passes assertions to the SP (the assertions never includes user's password).

How does SAML work?

SAML is an XML-based framework for implementing SSO:

  1. The SP will send an authentication request to IdP in XML format (visit https://www.samltool.com/generic_sso_req.php to see examples of what this XML looks like).

  2. The IdP sends its response back to SP in XML format (visit https://www.samltool.com/generic_sso_res.php to see examples of what this XML looks like).

  3. Before authentication between an IdP and an SP occurs, a trust must be created between the two systems. This trust occurs through a metadata file exchange. The metadata file is also in XML format. For example, JHU SSO metadata is here: https://idp.jh.edu/idp/shibboleth.

The metadata file contains the configuration data used to define how and on what URLs SP and IdP will communicate with each other. Both the SP and the IdP have their own metadata file that need to be shared with the other component.

Creating these XML files with all the requires information and proper formatting according to SAML standard is a tedious task. As a developer, you often use a library for SAML-based SSO that provides an interface to abstract away much of this work.

SAML v2.0 OASIS Standard set (PDF format) and schema files are available at https://www.oasis-open.org/standards#samlv2.0.

The Demo App: Setup

We will build a server application in NodeJS using Express framework.

The app is striped down to minimally required components; it must not be taken as an example of good practices. Namely, there is no persistence; this is deliberate to keep things simple.

  • Install NodeJS, the JavaScript runtime environment, for your operating system (if not already installed). The installer will also install NPM (node package management) tool. You can verify the installation by opening the terminal and checking the version of node and npm

    node -v
    npm -v
    
  • Create a folder where you want to store the source code of this demo app. I'm going to call this folder app-folder. Open the terminal, change directory to app-folder. Type in the following command

    npm init -y
    

    The command will create a file package.json in your app-folder. Feel free to checkout the content of this file.

  • Next, key in the following command to install ExpressJS as a dependency of your app.

    npm install --save express
    

    Express is a minimalist web framework for NodeJS.

  • Create a file index.js in your app-folder and copy the following code snipped into it

    const express = require("express");
    
    // Initialize express.
    const app = express();
    
    // Set up port.
    const port = process.env.PORT || 7000;
    
    // Set up homepage route
    app.get("/", (req, res) => {
      res.send("Test Home Page!");
    });
    
    // Start the server.
    app.listen(port, () => {
      console.log(`Listening on http://localhost:${port}/`);
    });
    

    The code is self explanatory (if you have created web servers before); it sets up a server that listens to requests on localhost port 7000.

  • Head over to the terminal and run the server

    node index.js
    

    Your app must be running at http://localhost:7000/.

Passport-SAML Library

We will be using Passport-SAML to configure SSO for our application. There are alternatives such as SAML2-JS and SamlifyJS which we could use.

There are libraries that assist with SAML-based SSO in other programming languages. There is, for instance, Pack4j for servers running on Java, flask-saml2 for servers running on Python and Flask, etc.

Go to terminal and run the following command to add Passport-SAML as a dependency to our app

npm install --save passport passport-saml

Login Route

Open index.js and add the following to the top of the file

const passport = require("passport");

Now create a login route

app.get(
  "/jhu/login",
  (req, res, next) => {
    next();
  },
  passport.authenticate("samlStrategy")
);

As you see, I've decided to use /jhu/login/ as the login route. It means, when a user tries to access a protected resource, I will redirect them to this route. Here, we must again redirect the user to JHU SSO (send the authentication XML request along with it). Normally, it would look like this:

app.get("/jhu/login", (req, res) => {
    // build the authentication request XML
    // redirect the user to JHU SSO with the request XML
});

But instead we are delegating that process to passport library by calling next() in the first callback function where next is the "next" callback: passport.authenticate("samlStrategy").

Diff
diff --git a/code/index.js b/code/index.js
index f2c2481..acb9c69 100644
--- a/code/index.js
+++ b/code/index.js
@@ -1,4 +1,5 @@
 const express = require("express");
+const passport = require("passport");
 
 // Initialize express.
 const app = express();
@@ -11,6 +12,15 @@ app.get("/", (req, res) => {
   res.send("Test Home Page!");
 });
 
+// login route
+app.get(
+  "/jhu/login",
+  (req, res, next) => {
+    next();
+  },
+  passport.authenticate("samlStrategy")
+);
+
 // Start the server.
 app.listen(port, () => {
   console.log(`Listening on http://localhost:${port}/`);

Callback Route

Once JHU SSO has authenticated a user, it will send back a POST request to the SP (our app) with user data (assertions). It is up to us what that POST endpoint will be (but we shall let JHU IdP know about it through our metadata file).

app.post(
  "/jhu/login/callback",
  (req, res, next) => {
    next();
  },
  passport.authenticate("samlStrategy"),
  (req, res) => {
    // the user data is in req.user
    res.send(`welcome ${req.user.first_name}`);
  }
);
Diff
diff --git a/code/index.js b/code/index.js
index acb9c69..826206b 100644
--- a/code/index.js
+++ b/code/index.js
@@ -21,6 +21,19 @@ app.get(
   passport.authenticate("samlStrategy")
 );
 
+// callback route
+app.post(
+  "/jhu/login/callback",
+  (req, res, next) => {
+    next();
+  },
+  passport.authenticate("samlStrategy"),
+  (req, res) => {
+    // the user data is in req.user
+    res.send(`welcome ${req.user.first_name}`);
+  }
+);
+
 // Start the server.
 app.listen(port, () => {
   console.log(`Listening on http://localhost:${port}/`);

The SAML Strategy

You may have noticed we used the following middleware in both login and callback routes:

passport.authenticate("samlStrategy"),

Passport-SAML library allows us to configure the samlStrategy object.

Open index.js and add the following to the top of the file

const saml = require("passport-saml");

// Setup SAML strategy
const samlStrategy = new saml.Strategy(
  {
    // config options here
  },
  (profile, done) => {
    return done(null, profile);
  }
);

// Tell passport to use the samlStrategy
passport.use("samlStrategy", samlStrategy);

The saml.Strategy() accepts two arguments:

  • The first is a configuration object, which I left blank for the moment.
  • The second is a function which processes the user.
    • The first argument into the function is a profile object, and the second is done, a callback.
    • For our purposes, we are just executing the callback and sending it the profile object unchanged.
    • If we needed to do more, such as load application specific permissions from a database, this could be done here.
Diff
diff --git a/code/index.js b/code/index.js
index 826206b..4eb9a33 100644
--- a/code/index.js
+++ b/code/index.js
@@ -1,5 +1,19 @@
 const express = require("express");
 const passport = require("passport");
+const saml = require("passport-saml");
+
+// Setup SAML strategy
+const samlStrategy = new saml.Strategy(
+  {
+    // config options here
+  },
+  (profile, done) => {
+    return done(null, profile);
+  }
+);
+
+// Tell passport to use the samlStrategy
+passport.use("samlStrategy", samlStrategy);
 
 // Initialize express.
 const app = express();

The configuration options

Here are the configuration options we will use

entryPoint

entryPoint: JHU_SSO_URL,

where JHU_SSO_URL is declared as

const JHU_SSO_URL = 
    "https://idp.jh.edu/idp/profile/SAML2/Redirect/SSO";

The entryPoint is an endpoint provided by the SSO software solution where we will send our request to in order to let the user authenticate. This endpoint is provided in the IdP metadata XML.

Looking at JHU SSO metadata XML at https://idp.jh.edu/idp/shibboleth, you'll find they allow for several options (including using a POST endpoint) but the entity with Binding of urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect is what gives the familiar login (and sign-off) page that students are used to.

callbackUrl

callbackUrl: `${BASE_URL}/jhu/login/callback`,

The callbackUrl is a POST endpoint in SP (our application) where the IdP will post back the assertions after a successful user authentication.

When you develop your server locally, the BASE_URL is going to be localhost (e.g. http://localhost:7000/). However, JHU does not accept a locally running server as a trusted SP. So, you need to deploy your server! For example, I have deployed this demo app on Heroku and it runs on https://glacial-plateau-47269.herokuapp.com. Therefore, I've set the BASE_URL as:

const BASE_URL = 
    "https://glacial-plateau-47269.herokuapp.com";

issuer

issuer: SP_NAME,

The issuer is a globally unique identifier for an SP. This is basically our app's name. It is common practice to user your app's domain name in here. So, I've set SP_NAME as:

const SP_NAME = "glacial-plateau-47269";

Putting it all together

This is how the code snippet for configuration of SAML strategy looks like right now:

const saml = require("passport-saml");

const JHU_SSO_URL = "https://idp.jh.edu/idp/profile/SAML2/Redirect/SSO";
const SP_NAME = "glacial-plateau-47269";
const BASE_URL = "https://glacial-plateau-47269.herokuapp.com";

// Setup SAML strategy
const samlStrategy = new saml.Strategy(
  {
    // config options here
    entryPoint: JHU_SSO_URL,
    issuer: SP_NAME,
    callbackUrl: `${BASE_URL}/jhu/login/callback`,
  },
  (profile, done) => {
    return done(null, profile);
  }
);

// Tell passport to use the samlStrategy
passport.use("samlStrategy", samlStrategy);
Diff
diff --git a/code/index.js b/code/index.js
index 4eb9a33..70c4a15 100644
--- a/code/index.js
+++ b/code/index.js
@@ -2,10 +2,17 @@ const express = require("express");
 const passport = require("passport");
 const saml = require("passport-saml");
 
+const JHU_SSO_URL = "https://idp.jh.edu/idp/profile/SAML2/Redirect/SSO";
+const SP_NAME = "glacial-plateau-47269";
+const BASE_URL = "https://glacial-plateau-47269.herokuapp.com";
+
 // Setup SAML strategy
 const samlStrategy = new saml.Strategy(
   {
     // config options here
+    entryPoint: JHU_SSO_URL,
+    issuer: SP_NAME,
+    callbackUrl: `${BASE_URL}/jhu/login/callback`,
   },
   (profile, done) => {
     return done(null, profile);

Generate the XML file!

Recall: before authentication between an IdP and an SP occurs, a trust must be created between the two systems. This trust occurs through a metadata file exchange.

JHU SSO SAML metadata XML file is here: https://idp.jh.edu/idp/shibboleth.

We can have PassportJS to generate a metadata file for us.

app.get("/jhu/metadata", (req, res) => {
  res.type("application/xml");
  res.status(200);
  res.send(samlStrategy.generateServiceProviderMetadata());
});

Run the app (using node index.js) and point your browser to http://localhost:7000/jhu/metadata to see the content of the metadata.

You need to send the metadata XML file to enterpriseauth@jhmi.edu along with your request for your app to be added as a trusted SP.

Once your app is deployed, you can point JHU SSO admins to the metadata route instead of sending an actual file. Keep in mind though, if you make a change to this file, you must resend it to JHU SSO admins (the file is manually uploaded and it will not be linked to your applications metadata route).

Diff
diff --git a/code/index.js b/code/index.js
index 70c4a15..8e59655 100644
--- a/code/index.js
+++ b/code/index.js
@@ -55,6 +55,13 @@ app.post(
   }
 );
 
+// route to metadata
+app.get("/jhu/metadata", (req, res) => {
+  res.type("application/xml");
+  res.status(200);
+  res.send(samlStrategy.generateServiceProviderMetadata());
+});
+
 // Start the server.
 app.listen(port, () => {
   console.log(`Listening on http://localhost:${port}/`);

Public-key cryptography

In SAML-based SSO, it is common to use end-to-end encryption using PKC, Public-key cryptography.

The Public-key cryptography employs a pair of public and private keys. Having a friend's public key allows you to encrypt messages to them. Your private key is used to decrypt messages encrypted to you.

In this scheme, when JHU sends user attributes to our app, it would encrypt it using our app's public key. (We would have to let them know of our public key through the metadata file). Our app then uses its private key to decrypt the encrypted data. So, we must:

  1. create a pair of private and public keys
  2. share the public key with IdP (i.e. include it in our metadata XML file)

The generation of public/private keys is done using cryptographic algorithms. There are different algorithms and standards for this. The standard used by SAML is X.509.

My preferred way of generating the encryption keys is using a little CLI tool called OpenSSL which can be obtained and installed here: https://www.openssl.org/.

Using OpenSSL Tool

Once OpenSSL is installed, open terminal and execute the following commands

mkdir certs
openssl req -x509 -newkey rsa:4096 -keyout certs/key.pem -out certs/cert.pem -nodes -days 365

This will create a folder certs and add two files key.pem (the private key) and cert.pem (the public key) to it.

Note: before the keys are generated, OpenSSL will prompt you to provide pertinent information. For the purposes of this demo, the information you provide need not to be accurate (e.g. just put something as organization name).

You must keep the certs folder in the app-folder. However, you should not upload it to a public repository like on GitHub (thus the folder is not included in the repository for this demo app).

Using the generated keys

Add this to index.js

const fs = require("fs");

const PbK = fs.readFileSync(__dirname + "/certs/cert.pem", "utf8");
const PvK = fs.readFileSync(__dirname + "/certs/key.pem", "utf8");

Add the following key-value pair to the SAML strategy config options object

decryptionPvk: PvK,

Add the public key to your metadata by passing it as an argument to generateServiceProviderMetadata function:

app.get("/jhu/metadata", (req, res) => {
  res.type("application/xml");
  res.status(200);
  res.send(samlStrategy.generateServiceProviderMetadata(PbK));
});

Now run the server and head over to http://localhost:7000/jhu/metadata. Notice the <KeyDescriptor use="encryption"> element which is added to metadata XML.

Diff
diff --git a/code/index.js b/code/index.js
index 8e59655..468616d 100644
--- a/code/index.js
+++ b/code/index.js
@@ -1,6 +1,10 @@
 const express = require("express");
 const passport = require("passport");
 const saml = require("passport-saml");
+const fs = require("fs");
+
+const PbK = fs.readFileSync(__dirname + "/certs/cert.pem", "utf8");
+const PvK = fs.readFileSync(__dirname + "/certs/key.pem", "utf8");
 
 const JHU_SSO_URL = "https://idp.jh.edu/idp/profile/SAML2/Redirect/SSO";
 const SP_NAME = "glacial-plateau-47269";
@@ -13,6 +17,7 @@ const samlStrategy = new saml.Strategy(
     entryPoint: JHU_SSO_URL,
     issuer: SP_NAME,
     callbackUrl: `${BASE_URL}/jhu/login/callback`,
+    decryptionPvk: PvK,
   },
   (profile, done) => {
     return done(null, profile);
@@ -59,7 +64,7 @@ app.post(
 app.get("/jhu/metadata", (req, res) => {
   res.type("application/xml");
   res.status(200);
-  res.send(samlStrategy.generateServiceProviderMetadata());
+  res.send(samlStrategy.generateServiceProviderMetadata(PbK));
 });
 
 // Start the server.

Reuse the generated keys

Another common practice in SAML-based SSO is to certify request/response XML objects by including XML_Signature. Typically, one reuses the public key for encryption for certification as well. You can generate a new set of keys for this purpose too.

Add the following key-value pair to the SAML strategy config options object

privateCert: PvK,

Add the public key as a signing certificate to your metadata by passing it as the second argument to generateServiceProviderMetadata function:

app.get("/jhu/metadata", (req, res) => {
  res.type("application/xml");
  res.status(200);
  res.send(samlStrategy.generateServiceProviderMetadata(PbK, PbK));
});

Now run the server and head over to http://localhost:7000/jhu/metadata. Notice the <KeyDescriptor use="signing"> element which is added to metadata XML.

Diff
diff --git a/code/index.js b/code/index.js
index 468616d..25c43d4 100644
--- a/code/index.js
+++ b/code/index.js
@@ -18,6 +18,7 @@ const samlStrategy = new saml.Strategy(
     issuer: SP_NAME,
     callbackUrl: `${BASE_URL}/jhu/login/callback`,
     decryptionPvk: PvK,
+    privateCert: PvK,
   },
   (profile, done) => {
     return done(null, profile);
@@ -64,7 +65,7 @@ app.post(
 app.get("/jhu/metadata", (req, res) => {
   res.type("application/xml");
   res.status(200);
-  res.send(samlStrategy.generateServiceProviderMetadata(PbK));
+  res.send(samlStrategy.generateServiceProviderMetadata(PbK, PbK));
 });
 
 // Start the server.

Middlewares: Passport library needs more!

We now need to add a few other thing to our application. These are entirely related to how PassportJS library work and have nothing to do with the SAML-based SSO.

Open the terminal and install the following dependencies:

npm install --save express-session body-parser

Add the dependency modules to the top of index.js

const session = require("express-session");
const bodyParser = require("body-parser");
// Middleware
app.use(bodyParser.urlencoded({ extended: false }));
app.use(
  session({ secret: "use-any-secret", resave: false, saveUninitialized: true })
);
app.use(passport.initialize({}));
app.use(passport.session({}));

I briefly explain the above statements:

Use bodyParser

const bodyParser = require("body-parser");

app.use(bodyParser.urlencoded({ extended: false }));
  • The bodyParser can turn the body of a URL request into a simple object for us to access.
  • The urlencoded() command will handle application/x-www/form-urlencoded values. Passport needs this when handling the IdP response that is directed to our callback POST endpoint.

Use express-session

const session = require("express-session");

app.use(
  session({ secret: "use-any-secret", resave: false, saveUninitialized: true })
);
  • The session store user information on the server side (and session ID on client-side using cookies).
  • The secret value is used to sign a sessionID cookie. The sessionID will reference the server-side session. We can use any value we want for the secret key.
  • The resave value determines whether to save the session value back into the session store after every request, even if it was not changed.
  • The saveUninitailized value is set to true. This means that a session is always saved after it was created even if it did not change.

Setting passport in action

app.use(passport.initialize({}));
app.use(passport.session({}));

The two statements are needed to set passport in action.

Declaring your middleware (all the statements that start with app.use) must be done before declaring the routes (statement that start with app.get or app.post).

Diff
diff --git a/code/index.js b/code/index.js
index 25c43d4..7b66e7c 100644
--- a/code/index.js
+++ b/code/index.js
@@ -1,6 +1,8 @@
 const express = require("express");
 const passport = require("passport");
 const saml = require("passport-saml");
+const session = require("express-session");
+const bodyParser = require("body-parser");
 const fs = require("fs");
 
 const PbK = fs.readFileSync(__dirname + "/certs/cert.pem", "utf8");
@@ -34,6 +36,14 @@ const app = express();
 // Set up port.
 const port = process.env.PORT || 7000;
 
+// Middleware
+app.use(bodyParser.urlencoded({ extended: false }));
+app.use(
+  session({ secret: "use-any-secret", resave: false, saveUninitialized: true })
+);
+app.use(passport.initialize({}));
+app.use(passport.session({}));
+
 // Set up homepage route
 app.get("/", (req, res) => {
   res.send("Test Home Page!");

Final touches: Pre & Post process User object

Passport requires that we add functions to serialize and deserialize the user:

// Serialize and deserialize user for paqssport
passport.serializeUser(function (user, done) {
  done(null, user);
});

passport.deserializeUser(function (user, done) {
  done(null, user);
});

The serializeUser/deserializeUser pre and post process the user object:

  • The first argument into the function is a user object, and the second is done, a callback.
  • For our purposes, we are just executing the callback and sending it the user object unchanged.
  • If we needed to do more with the user object (e.g. check it agains our database, etc), this could be done here.

These functions are default functions that just output the user to the console, which is a great debugging tool.

Diff
diff --git a/code/index.js b/code/index.js
index 7b66e7c..da2f90b 100644
--- a/code/index.js
+++ b/code/index.js
@@ -30,6 +30,15 @@ const samlStrategy = new saml.Strategy(
 // Tell passport to use the samlStrategy
 passport.use("samlStrategy", samlStrategy);
 
+// Serialize and deserialize user for paqssport
+passport.serializeUser(function (user, done) {
+  done(null, user);
+});
+
+passport.deserializeUser(function (user, done) {
+  done(null, user);
+});
+
 // Initialize express.
 const app = express();

Deploy: The final application!

Here is what the index.js looks like:

const express = require("express");
const passport = require("passport");
const saml = require("passport-saml");
const session = require("express-session");
const bodyParser = require("body-parser");
const fs = require("fs");

const PbK = fs.readFileSync(__dirname + "/certs/cert.pem", "utf8");
const PvK = fs.readFileSync(__dirname + "/certs/key.pem", "utf8");

const JHU_SSO_URL = "https://idp.jh.edu/idp/profile/SAML2/Redirect/SSO";
const SP_NAME = "glacial-plateau-47269";
const BASE_URL = "https://glacial-plateau-47269.herokuapp.com";

// Setup SAML strategy
const samlStrategy = new saml.Strategy(
  {
    // config options here
    entryPoint: JHU_SSO_URL,
    issuer: SP_NAME,
    callbackUrl: `${BASE_URL}/jhu/login/callback`,
    decryptionPvk: PvK,
    privateCert: PvK,
  },
  (profile, done) => {
    return done(null, profile);
  }
);

// Tell passport to use the samlStrategy
passport.use("samlStrategy", samlStrategy);

// Serialize and deserialize user for paqssport
passport.serializeUser(function (user, done) {
  done(null, user);
});

passport.deserializeUser(function (user, done) {
  done(null, user);
});

// Initialize express.
const app = express();

// Set up port.
const port = process.env.PORT || 7000;

// Middleware
app.use(bodyParser.urlencoded({ extended: false }));
app.use(
  session({ secret: "use-any-secret", resave: false, saveUninitialized: true })
);
app.use(passport.initialize({}));
app.use(passport.session({}));

// Set up homepage route
app.get("/", (req, res) => {
  res.send("Test Home Page!");
});

// login route
app.get(
  "/jhu/login",
  (req, res, next) => {
    next();
  },
  passport.authenticate("samlStrategy")
);

// callback route
app.post(
  "/jhu/login/callback",
  (req, res, next) => {
    next();
  },
  passport.authenticate("samlStrategy"),
  (req, res) => {
    // the user data is in req.user
    res.send(`welcome ${req.user.first_name}`);
  }
);

// route to metadata
app.get("/jhu/metadata", (req, res) => {
  res.type("application/xml");
  res.status(200);
  res.send(samlStrategy.generateServiceProviderMetadata(PbK, PbK));
});

// Start the server.
app.listen(port, () => {
  console.log(`Listening on http://localhost:${port}/`);
});

I have deployed the app on Heroku and it is available at https://glacial-plateau-47269.herokuapp.com. Feel free to experiment with it:

  • Go to https://glacial-plateau-47269.herokuapp.com/jhu/login
  • It must redirect you to JHU SSO sign-in page
  • Enter your credential and login
  • It must redirect back to https://glacial-plateau-47269.herokuapp.com/jhu/login/callback
  • You must see a welcome message!

Assertion attributes

The IdP response includes a list of user attributes (assertions). You need to specify, in your correspondence with enterpriseauth@jhmi.edu, what attributes you need so they send it your way.

Here are some of the attributes you can receive (which will be accessible through req.user object in the callback route):

  • Username: req.user.username (your JHED ID)
  • Affiliation: req.user.user_field_affiliation (either STUDENT, FACULTY or STAFF)
  • Job title: req.user.user_field_job_title (e.g. mine comes up as LECTURER)
  • Last name: req.user.last_name
  • First name: req.user.first_name
  • Given name: req.user.given_name
  • Email: req.user.email