Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
JavaScript by Example

You're reading from   JavaScript by Example Learn modern web development with real-world applications

Arrow left icon
Product type Paperback
Published in Aug 2017
Publisher Packt
ISBN-13 9781788293969
Length 298 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Dani Akash S Dani Akash S
Author Profile Icon Dani Akash S
Dani Akash S
Arrow right icon
View More author details
Toc

ToDo List app

Let's take a look at the application we are about to build:

We are going to build this simple ToDo List app, which allows us to create a list of tasks, mark them as completed, and delete tasks from the list.

Let's get started by using the starter code of Chapter 1 in the book's code files. The starter code will contain three files: index.html, scripts.js, and styles.css. Open the index.html file in a web browser to see the basic design of the ToDo List app, as shown in the preceding screenshot.

The JavaScript file will be empty, in which we are going to write scripts to create the application. Let's take a look at the HTML file. In the <head> section, a reference to the styles.css file and BootstrapCDN are included, and at the end of the <body> tag, jQuery and Bootstrap's JS files are included along with our scripts.js file:

  • Bootstrap is a UI development framework that helps us to build responsive HTML designs faster. Bootstrap comes with set of JavaScript codes that requires jQuery to run.

  • jQuery is a JavaScript library that simplifies JavaScript functions for DOM traversal, DOM manipulation, event handling, and so on.

Bootstrap and jQuery are widely used together for building web applications. In this book, we will be focusing more on using JavaScript. Hence, both of them will not be covered in detail. However, you can take a look at w3school's website for learning Bootstrap: https://www.w3schools.com/bootstrap/default.asp and jQuery: https://www.w3schools.com/jquery/default.asp in detail.

In our HTML file, the styles in the CSS file included last will overwrite the styles in the previous file. Hence, it's a good practice to include our own CSS files after the default framework's CSS files (Bootstrap in our case) if we plan to rewrite any of the framework's default CSS properties. We don't have to worry about CSS in this chapter, since we are not going to edit default styles of Bootstrap in this chapter. We only need to concentrate on our JS files. JavaScript files must be included in the given order as in the starter code:

<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="scripts.js"></script>

We are including the jQuery code first after which Bootstrap JS files are included. This is because Bootstrap's JS files require jQuery to run. If we include Bootstrap JS first, it will print an error in the console, saying Bootstrap requires jQuery to run. Try moving the Bootstrap code above the jQuery code and open up your browser's console. For Google Chrome, it's Ctrl+Shift+J on Windows or Linux and command+option+J on Mac. You will receive an error similar to this:

Hence, we are currently managing dependencies by including the JS files in the right order. However, in larger projects, this could be really difficult. We'll look at a better way to manage our JS files in the next chapter. For now, let's continue on to build our application.

The body of our HTML file is divided into two sections:

  • Navigation bar
  • Container

We usually use the navigation bar to add links to the different sections of our web app. Since we are only dealing with a single page in this app, we will only include the page title in the navigation bar.

I have included many classes to the HTML elements, such as navbar, navbar-inverse, navbar-fixed-top, container, col-md-2, col-xs-2, and so on. They are used for styling the elements using Bootstrap. We'll discuss them in later chapters. For now, let's focus only on the functionality part.

Chrome DevTools

In the body section, we have an input field with a button to add a new task and an unordered list to list out the tasks. The unordered list will have a checkbox to mark the task as completed and a delete icon to remove the task from the list. You might notice that the first item in the list is marked completed using a strike-through line. If you inspect the element using Chrome DevTools, you will notice that it has an additional class complete, which adds a strike-through line on the text using CSS, which is defined in our styles.css file.

To inspect an element using Chrome DevTools, right-click over that element and select inspect. You can also click Ctrl+Shift+C on Windows or Linux, or command+shift+C on Mac, and then, hover the cursor over the element to see its details. You can also directly edit the element's HTML or CSS to see the changes reflected on the page. Delete the complete class from the div of the first item in the list. You'll see that the strike-through line has gone. The changes made directly in the DevTools are temporary and will be cleaned when the page is refreshed. Take a look at the following image for a list of tools available to inspect an element in Chrome:

  • A: Inspect element from right-click
  • B: Click the cursor icon and select a different element by hovering the cursor over the element
  • C: Directly edit the HTML of the page
  • D: Directly edit the CSS associated with an element

One other nice feature of Chrome DevTools is that you can write debugger anywhere in your JavaScript code and Google Chrome will pause the execution of the script at the point in which debugger was called. Once the execution is paused, you can hover your cursor over the source code in sources tab and it will show the value contained in the variable in a popup. You can also type in the variable's name in the console tab to see its value.

This is the screenshot of Google Chrome debugger in action:

Feel free to explore the different sections of the Chrome Developer Tools to understand more about the tools it provides for the developers.

Getting started with ES6

Now that you have a good idea about the developer tools, let's start the coding part. You should already be familiar with the JavaScript ES5 syntax. So, let's explore JavaScript with the ES6 syntax in this chapter. ES6 (ECMAScript 2015) is the sixth major release of ECMAScript language specification. JavaScript is an implementation of ECMAScript language specification.

At the time of writing this book, ES8 is the latest release of JavaScript language. However, for simplicity and ease of understanding, this book only focuses on ES6. You can always learn about the latest features introduced in ES7 and beyond on the Internet easily once you grasp the knowledge of ES6.

At the time of writing this book, all the modern browsers support most of the ES6 features. However, older browsers don't know about the new JavaScript syntax and, hence, they will throw errors. To resolve such backward compatibility issues, we will have to transpile our ES6 code to ES5 before deploying the app. Let's look into that at the end of the chapter. The latest version of Chrome supports ES6; so, for now, we'll directly create our ToDo List with the ES6 syntax.

I'll explain in detail about the new ES6 syntax. If you find difficulties understanding normal JavaScript syntax and data types, do refer to the respective section in the following w3schools page: https://www.w3schools.com/js/default.asp.

Open up the scripts.js file in your text editor. First of all, we will create a class that contains the methods of our ToDo List app, and yeah! Classes are a new addition to JavaScript in ES6. It's simple to create objects using classes in JavaScript. It lets us organize our code as modules. Create a class named ToDoClass with the following code in the scripts file and refresh the browser:

class ToDoClass {
constructor() {
alert('Hello World!');
}
}
window.addEventListener("load", function() {
var toDo = new ToDoClass();
});

Your browser will now throw an alert saying "Hello World!". So here's what the code is doing. First, window.addEventListener will attach an event listener to the window and wait for the window to finish loading all the needed resources. Once it is loaded, the load event is fired, which calls the callback function of our event listener that initializes ToDoClass and assigns it to a variable toDo. While ToDoClass is initialized, it automatically calls the constructor, which creates an alert saying "Hello World!". We can further modify our code to take advantage of ES6. In the window.addEventListener part, you can rewrite it as:

let toDo;
window.addEventListener("load", () => {
toDo = new ToDoClass();
});

First, we replace the anonymous callback function function () {} with the new arrow function () => {}. Second, we define the variable with let instead of var.

Arrow functions

Arrow functions are a cleaner and shorter way to define functions in JavaScript and they simply inherit the this object of its parent instead of binding its own. We'll see more about the this binding soon. Let's just look into using the new syntax. Consider the following functions:

let a = function(x) {
}
let b = function(x, y) {
}

The equivalent arrow functions can be written as:

let a = x => {}
let b = (x,y) => {}

You can see that () are optional, when we have to pass the only single argument to the function.

Sometimes, we just return a value in a single line in our functions, such as:

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

If we want to directly return a value in our arrow function in a single line, we can directly ignore the return keyword and {} curly braces and write it as:

let sum = (x, y) => x+y;

That's it! It will automatically return the sum of x and y. However, this can be used only when you want to return the value immediately in a single line.

let, var, and const

Next, we have the let keyword. ES6 has two new keywords for declaring variables, let and const. let and var differ by the scope of the variables declared using them. The scope of variables declared using var is within the function it is defined and global if it is not defined inside any function, while the scope of let is restricted to within the enclosing block it was declared in and global if it is not defined inside any enclosing block. Look at the following code:

var toDo;
window.addEventListener("load", () => {
var toDo = new ToDoClass();
});

If you were to accidentally re-declare toDo somewhere along the code, as follows, your class object gets overwritten:

var toDo = "some value";

This behavior is confusing and quite difficult to maintain variables for large applications. Hence, let was introduced in ES6. It restricts the scope of variables only within the enclosing in which it was declared. In ES6, it is encouraged to use let instead of var for declaring variables. Look at the following code:

let toDo;
window.addEventListener("load", () => {
toDo = new ToDoClass();
});

Now, even if you accidentally re-declare toDo somewhere else in the code, JavaScript will throw an error, saving you from a runtime exception. An enclosing block is a block of code between two curly braces {} and the curly braces may or may not belong to a function.

We need a toDo variable to be accessible throughout the application. So, we declare toDo above the event listener and assign it to the class object inside the callback function. This way, the toDo variable will be accessible throughout the page.

let is very useful for defining variables in for loops. You can create a for loop such that for(let i=0; i<3; i++) {} and the scope of the variable i will only be within the for loop. You can easily use the same variable name in other places of your code.

Let's take a look at the other keyword const. The working of const is the same as that of let, except that variables declared using const cannot be changed (reassigned). Hence, const is used for constants. However, an entire constant cannot be reassigned but their properties can be changed. For example:

const a = 5;
a = 7; // this will not work
const b = {
a: 1,
b: 2
};
b = { a: 2, b: 2 }; // this will not work
b.a = 2; // this will work since only a property of b is changed
While writing code in ES6, always use const to declare your variables. Use let only when you need to perform any changes (reassignments) to the variable and completely avoid using var.

The toDo object contains the class variables and functions as properties and methods of the object. If you need a clear picture of how the object is structured in JavaScript, see: https://www.w3schools.com/js/js_objects.asp.

Loading the tasks from data

The first thing we want to do in our application is to load the tasks dynamically from a set of data. Let's declare a class variable that contains the data for tasks along with methods needed to pre-populate the tasks. ES6 does not provide a direct way to declare class variables. We need to declare variables using the constructor. We also need a function to load tasks into the HTML elements. So, we'll create a loadTasks() method:

class ToDoClass {
constructor() {
this.tasks = [
{task: 'Go to Dentist', isComplete: false},
{task: 'Do Gardening', isComplete: true},
{task: 'Renew Library Account', isComplete: false},
];
this.loadTasks();
}

loadTasks() {
}
}

The tasks variable is declared inside the constructor as this.tasks, which means the tasks variable belongs to this (ToDoClass). The variable is an array of objects that contain the task details and its completion status. The second task is set to be completed. Now, we need to generate an HTML code for the data. We'll reuse the code of the <li> element from the HTML to generate a task dynamically:

 <li class="list-group-item checkbox">
<div class="row">
<div class="col-md-1 col-xs-1 col-lg-1 col-sm-1 checkbox">
<label><input type="checkbox" value="" class="" checked></label>
</div>
<div class="col-md-10 col-xs-10 col-lg-10 col-sm-10 task-text complete">
First item
</div>
<div class="col-md-1 col-xs-1 col-lg-1 col-sm-1 delete-icon-area">
<a class="" href="/"><i class="delete-icon glyphicon glyphicon-trash"></i></a>
</div>
</div>
</li>
In JavaScript, an instance of a class is called the class object or simply object. The class objects are structured similarly to JSON objects in key-value pairs. The functions associated with a class object are called its methods and the variables/values associated with a class object are called its properties.

Template literals

Traditionally, in JavaScript, we concatenate strings using the + operator. However, if we want to concatenate multi-line strings, then we have to use the escape code \ to escape new lines, such as:

let a = '<div> \
<li>' + myVariable+ '</li> \
</div>'

This can be very confusing when we have to write a string that contains a large amount of HTML. In this case, we can use ES6 template strings. Template strings are strings surrounded by backticks ` ` instead of single quotation marks ' '. By using this, we can create multi-line strings in an easier way:

let a = `
<div>
<li> ${myVariable} </li>
</div>
`

As you can see, we can create DOM elements in a similar way; we type them in HTML without worrying about spaces or multi-lines. Because whatever formatting, such as tabs or new lines, present inside the template strings is directly recorded in the variable. And we can declare variables inside the strings using ${}. So, in our case, we need to generate a list of items for each task. First, we will create a function to loop through the array and generate the HTML. In our loadTasks() method, write the following code:

loadTasks() {
let tasksHtml = this.tasks.reduce((html, task, index) => html +=
this.generateTaskHtml(task, index), '');
document.getElementById('taskList').innerHTML = tasksHtml;
}

After that, create a generateTaskHtml() function inside ToDoClass, with the code:

generateTaskHtml(task, index) {
return `
<li class="list-group-item checkbox">
<div class="row">
<div class="col-md-1 col-xs-1 col-lg-1 col-sm-1 checkbox">
<label><input id="toggleTaskStatus" type="checkbox"
onchange="toDo.toggleTaskStatus(${index})" value="" class=""
${task.isComplete?'checked':''}></label>
</div>
<div class="col-md-10 col-xs-10 col-lg-10 col-sm-10 task-text ${task.isComplete?'complete':''}">
${task.task}
</div>
<div class="col-md-1 col-xs-1 col-lg-1 col-sm-1 delete-icon-area">
<a class="" href="/" onClick="toDo.deleteTask(event, ${index})"><i
id="deleteTask" data-id="${index}" class="delete-icon glyphicon
glyphicon-trash"></i></a>
</div>
</div>
</li>
`;
}

Now, refresh the page, and wow! Our application is loaded with tasks from our tasks variable. That should look like a lot of code at first, but let's look into it line by line.

In case the changes aren't reflected when you refresh the page, it's because Chrome has cached the JavaScript files and is not retrieving the latest one. To make it retrieve the latest code, you will have to do a hard reload by pressing Ctrl+Shift+R on Windows or Linux and command+Shift+R on Mac.

In the loadTasks() function, we declare a variable tasksHtml with a value that is returned by the callback function of the array reduce() method of the tasks variable. Each array object in JavaScript has some methods associated with it. reduce is one such method of JS array that applies a function to each element of the array from left to right and applies the values to an accumulator so that the array gets reduced to a single value and then it returns that final value. The reduce method accepts two parameters; first is the callback function, which is applied to each element of the array, and the second one is the initial value of the accumulator. Let's look at our function in normal ES5 syntax:

let tasksHtml = this.tasks.reduce(function(html, task, index, tasks) { 
return html += this.generateTaskHtml(task, index)
}.bind(this), '');
  • The first parameter is the callback function, whose four parameters are html, which is our accumulator, task, which is an element from the tasks array, index, which gives the current index of the array element in the iteration, and tasks, which contains the entire array on which the reduce method is applied on (we don't need the entire array inside the callback function for our use case, so the fourth parameter is ignored in our code).
  • The second parameter is optional, which contains the initial value of the accumulator. In our case, the initial HTML string is an empty string ''.
  • Also, note that we have to bind the callback function with this (which is our class) object so that the methods of ToDoClass and the variables are accessible within the callback function. This is because, otherwise, every function will define its own this object and the parent's this object will be inaccessible within that function.

What the callback function does is it takes the empty html string (accumulator) first and concatenates it with the value returned by the generateTaskHtml() method of ToDoClass, whose parameters are the first element of the array and its index. The returned value, of course, should be a string, otherwise, it will throw an error. Then, it repeats the operation for each element of the array with an updated value of the accumulator, which is finally returned at the end of the iteration. The final reduced value contains the entire HTML code for populating our tasks as a string.

By applying ES6 arrow functions, the entire operation can be achieved in a single line as:

let tasksHtml = this.tasks.reduce((html, task, index) => html += this.generateTaskHtml(task, index), '');

Isn't that simple! Since we are just returning the value in a single line, we can ignore both the {} curly braces and return keyword. Also, arrow functions do not define their own this object; they simply inherit the this object of their parents. So we can also ignore the .bind(this) method. Now, we have made our code cleaner and much simpler to understand using arrow functions.

Before we move on to the next line of the loadTasks() method, let's look at the working of the generateTaskHtml() method. This function takes two arguments--an array element task in the tasks data and its index and returns a string that contains the HTML code for populating our tasks. Note that we have included variables in the code for the checkbox:

<input id="toggleTaskStatus" type="checkbox" onchange="toDo.toggleTaskStatus(${index})" value="" class="" ${task.isComplete?'checked':''}>

It says that "on change of checkbox's status", call toggleTaskStatus() method of the toDo object with the index of the task that was changed. We haven't defined the toggleTaskStatus() method yet, so when you click the checkbox on the website now, it will throw an error in Chrome's console and nothing special happens in the browser window. Also, we have added a conditional operator ()?: to return a checked attribute for the input tag if the task status is complete. This is useful to render the list with a prechecked check box if the task is already complete.

Similarly, we have included ${task.isComplete?'complete':''} in the div that contains the task text so that an additional class gets added to the task if the task is complete, and CSS has been written in the styles.css file for that class to render a strike-through line over the text.

Finally, in the anchor tag, we have included onClick="toDo.deleteTask(event, ${index})" to call the deleteTask() method of the toDo object with parameters--the click event itself and the index of the task. We haven't defined the deleteTask() method yet, so clicking on the delete icon is going to take you to the root of your file system!

onclick and onchange are some of HTML attributes that are used to call JavaScript functions when the specified event occurs on the parent element on which the attributes are defined. Since these attributes belong to HTML, they are case insensitive.

Now, let's look at the second line of the loadTasks() method:

document.getElementById('taskList').innerHTML = tasksHtml;

We just replaced the HTML code of the DOM element with the ID taskList with our newly generated string tasksHTML. Now, the ToDo List is populated. Time to define the two new methods of the toDo object, which we included in our generated HTML code.

Managing task status

Inside ToDoClass, include the two new methods:

 toggleTaskStatus(index) {
this.tasks[index].isComplete = !this.tasks[index].isComplete;
this.loadTasks();
}
deleteTask(event, taskIndex) {
event.preventDefault();
this.tasks.splice(taskIndex, 1);
this.loadTasks();
}

The first method, toggleTaskStatus(), is used to mark a task as completed or incomplete. It is called when a checkbox is clicked (onChange) with the index of the task, which was clicked as the parameter:

  • Using the task's index, we assign the task's isComplete status as the negation of its current status not using the (!) operator. Hence, the completion status of the tasks can be toggled in this function.
  • Once the tasks variable is updated with new data, this.loadTasks() is called to re-render all the tasks with the updated value.

The second method, deleteTask(), is used to delete a task from the list. Currently, clicking the delete icon will take you to the root of the file system. However, before navigating you to the root of the file system, a call to toDo.deleteTask() is made with the click event and task's index as the parameters:

  • The first parameter event contains the entire event object that contains various properties and methods about the click event that just happened (try console.log(event) inside the deleteTask() function to see all the details in Chrome's console).
  • To prevent any default action (opening a URL) from happening once, we click the delete icon (the <a> tag). Initially, we need to specify event.preventDefault().
  • Then, we need to remove the task element of the array that was deleted from the tasks variable. For that, we use the splice() method, which deletes a specified number of elements from an array from a specified index. In our case, from the index of the task, which needs to be deleted, delete only a single element. This removes the task to be deleted from the tasks variable.
  • this.loadTasks() is called to re-render all the tasks with the updated value.

Refresh the page (hard reload if needed) to see how our current application works with the new code. You can now mark a task as completed and can delete a task from the list.

Adding new tasks to the list

We now have the options to toggle a task status and to delete a task. But we need to add more tasks to the list. For that, we need to use the text box provided in the HTML file to allow users to type in new tasks. The first step will be adding the onclick attribute to the add task <button>:

<button class="btn btn-primary" onclick="toDo.addTaskClick()">Add</button>

Now, every button click will call the addTaskClick() method of the toDo object, which is not yet defined. So, let's define it inside our ToDoClass:

addTaskClick() {
let target = document.getElementById('addTask');
this.addTask(target.value);
target.value = ""
}
addTask(task) {
let newTask = {
task,
isComplete: false,
};
let parentDiv = document.getElementById('addTask').parentElement;
if(task === '') {
parentDiv.classList.add('has-error');
} else {
parentDiv.classList.remove('has-error');
this.tasks.push(newTask);
this.loadTasks();
}
}

Reload Chrome and try adding a new task by clicking the Add button. If everything's fine, you should see a new task get appended to the list. Also, when you click the Add button without typing anything in the input field, then it will highlight the input field with a red border, indicating the user should input text in the input field.

See how I have divided our add task operation across two functions? I did a similar thing for the loadTask() function. In programming, it is a best practice to organize all the tasks into smaller, more generic functions, which will allow you to reuse those functions in the future.

Let's see how the addTaskClick() method works:

  • addTaskClick() function doesn't have any request parameters. First, to read the new task's text, we get the <input> element with the ID addTask, which contains the text needed for the task. using document.getElementById('addTask'), and assign it to target variable. Now, the target variable contains all the properties and methods of the <input> element, which can be read and modified (try console.log(target) to see all the details contained in the variable).
  • The value property contains the required text. So, we pass target.value to the addTask() function, which handles adding a new task to the list.
  • Finally, we reset the input field to an empty state by setting target.value to an empty string ''.

That's the event handling part for the click event. Let's see how the task gets appended to the list in the addTask() method. The task variable contains the text for the new task:

  • Ideally, the first step in this function is to construct the JSON data that defines our task:
let newTask = {
task: task,
isComplete: false
}
  • Here's another ES6 feature object literal property value shorthand; instead of writing {task: task} in our JSON object, we can simply write {task}. The variable name will become the key and the value stored in the variable becomes the value. This will throw an error if the variable is undefined.
  • We also need to create another variable parentDiv to store the object of the parent <div> element of our target <input> element. It's useful because, when the task is an empty string, we can add the has-error class to the parent element parentDiv.classList.add('has-error'), which by Bootstrap's CSS, renders a red border to our <input> element. This is how we can indicate to the user that they need to enter a text before clicking the Add button.
  • However, if the input text is not empty, we should remove the has-error class from our parent element to ensure the red border is not shown to the user and then simply push our newTask variable to the tasks variable of our class. Also, we need to call loadTasks() again so that the new task gets rendered.

Adding tasks by hitting Enter button

Well, this is one way of adding tasks, but some users prefer adding tasks directly by hitting the Enter button. For that, let's use event listeners to detect the Enter key press in the <input> element. We can also use the onchange attribute of our <input> element, but let's give event listeners a try. The best way to add event listeners to a class is to call them in the constructor so that the event listeners are set up when the class is initialized.

So, in our class, create a new function addEventListeners() and call it in our constructor. We are going to add event listeners inside this function:

constructor() {
...
this.addEventListeners();
}
addEventListeners() {
document.getElementById('addTask').addEventListener('keypress', event => {
if(event.keyCode === 13) {
this.addTask(event.target.value);
event.target.value = '';
}
});
}

And that's it! Reload Chrome, type in the text, and hit Enter. This should add tasks to our list just like how the add button works. Let's go through our new event listener:

  • For every keypress happening in the <input> element with the ID addTask, we run the callback function with the event object as the parameter.
  • This event object contains the keycode of the key that was pressed. For the Enter key, the keycode is 13. If the key code is equal to 13, we simply call the this.addTask() function with the task's text event.target.value as its parameter.
  • Now, the addTask() function handles adding the task to the list. We can simply reset <input> back to an empty string. This is a great advantage of organizing every operation into functions. We can simply reuse the functions wherever they're needed.

Persisting data in the browser

Now, functionality-wise, our ToDo List is ready. However, on refreshing the page, the data will be gone. Let's see how to persist data in the browser. Usually, web apps connect with APIs from the server-side to load data dynamically. Here, we are not looking into server-side implementation. So, we need to look for an alternate way to store data in the browser. There are three ways to store data in the browser. They are as follows:

  • cookie: A cookie is a small information that is stored on the client-side (browser) by the server with an expiry date. It is useful for reading information from the client, such as login tokens, user preferences, and so on. Cookies are primarily used on the server-side and the amount of data that can be stored in the cookie is limited to 4093 bytes. In JavaScript, cookies can be managed using the document.cookie object.
  • localStorage: HTML5's localStorage stores information with no expiry date and the data will persist even after closing and opening the web page. It provides a storage space of 5 MB per domain.
  • sessionStorage: sessionStorage is equivalent to that of localStorage, except that the data is only valid per session (the current tab that the user is working on). The data expires when the website is closed.

For our use case, localStorage is the best choice for persisting task data. localStorage stores data as key-value pairs, while the value needs to be a string. Let's look at the implementation part. Inside the constructor, instead of assigning the value to this.tasks directly, change it to the following:

constructor() {
this.tasks = JSON.parse(localStorage.getItem('TASKS'));
if(!this.tasks) {
this.tasks = [
{task: 'Go to Dentist', isComplete: false},
{task: 'Do Gardening', isComplete: true},
{task: 'Renew Library Account', isComplete: false},
];
}
...
}

We are going to save our tasks in localStorage as a string with 'TASKS' as its key. So when the user opens the website for the first time, we need to check whether any data is present in localStorage with the key 'TASKS'. If no data is present, it will return null, which means this is the first time a user is visiting the website. We need to use JSON.parse() to convert the data retrieved from localStorage from a string to an object:

  • If no data is present in localStorage (user visiting the site for the first time), we shall prepopulate some data for them using the tasks variable. The best place to add the code to persist task data in our application will be the loadTasks() function because it is called every time a change in tasks is made. In the loadTasks() function, add an additional line:
 localStorage.setItem('TASKS', JSON.stringify(this.tasks));
  • This will convert our tasks variable to string and store it in localStorage. Now, you can add tasks and refresh the page, and the data will be persisted in your browser.
  • If you want to empty localStorage for development purposes, you can use localStorage.removeItem('TASKS') to delete the key or you can use localStorage.clear() to completely remove all the data stored in localStorage.
Everything in JavaScript has an inherent Boolean value, which can be called truthy or falsy. The following values are always falsy - null, "" (empty string), false, 0 (zero), NaN (not a number), and undefined. Other values are considered truthy. Hence, they can be directly used in conditional statements like how we used if(!this.tasks) {} in our code.

Now that our application is complete, you can remove the contents of the <ul> element in the index.html file. The contents will now be directly populated from our JavaScript code. Otherwise, you will see the default HTML code flash in the page when the page is loaded or refreshed. This is because our JavaScript code executes only after all the resources are finished loading due to the following code:

window.addEventListener("load", function() {
toDo = new ToDoClass();
});

If everything works fine, then congratulations! You have successfully built your first JavaScript application and you have learned about the new ES6 features of JavaScript. Oh wait! Looks like we forgot something important!

All the storage options discussed here are unencrypted and, hence, should not be used for storing sensitive information, such as password, API keys, authentication tokens, and so on.

Compatibility with older browsers

While ES6 works with almost all modern browsers, there are still many users who use older versions of Internet Explorer or Firefox. So, how are we going to make our application work for them? Well, the good thing about ES6 is that all it's new features can be implemented using the ES5 specification. This means that we can easily transpile our code to ES5, which will work on all modern browsers. For this purpose, we are going to use Babel: https://babeljs.io/, as the compiler for converting ES6 to ES5.

Remember how, in the beginning of our chapter, we installed Node.js in our system? Well, it's finally time to use it. Before we start compiling our code to ES5, we need to learn about Node and the npm.

Node.js and npm

Node.js is a JavaScript runtime built on Chrome's V8 engine. It lets developers run JavaScript outside of the browser. Due to the non-blocking I/O model of Node.js, it is widely used for building data-intensive, real-time applications. You can use it to build backend for your web application in JavaScript, just like PHP, Ruby, or other server-side languages.

One great advantage of Node.js is that it lets you organize your code into modules. A module is a set of code used to perform a specific function. So far, we have included the JavaScript code one after another inside the <script> tag in the browser. But in Node.js, we can simply call the dependency inside the code by creating the reference to the module. For example, if we need to jQuery, we can simply write the following:

const $ = require('jquery');

Or, we can write the following:

import $ from 'jquery';

The jQuery module will be included in our code. All the properties and methods of jQuery will be accessible inside the $ object. The scope of $ will be only within the file it is called. So, in each file, we can specify the dependencies individually and all of them will be bundled together during compilation.

But wait! For including jquery, we need to download the jquery package that contains the required module and save it in a folder. Then, we need to assign $ the reference of the file in the folder containing the module. And as the project grows, we will be adding a lot of packages and refer ring the modules in our code. So, how are we going to manage all the packages. Well, we have a nice little tool that gets installed along with Node.js called the Node Package Manager (npm):

  • For Linux and Mac users, npm is similar to one of these: apt-get, yum, dnf, and Homebrew.
  • For Windows users, you might not be familiar with the concept of package management yet. So, let's say you need jQuery. But you don't know what dependencies are needed for jQuery to run. That's where package managers come into play. You can simply run a command to install a package (npm install jquery). The package manager will read all the dependencies of the target package and install the target along with its dependencies. It also manages a file to keep track of installed packages. This is used for easily uninstalling the package in the future.
Even though Node.js allows require/import of modules directly into the code, browsers do not support require or import functionality to directly import a module. But there are many tools available that can easily mimic this functionality so that we can use import/require inside our browsers. We'll use them for our project in the next chapter.

npm maintains a package.json file to store information regarding a package, such as its name, scripts, dependencies, dev dependencies, repository, author, license, and so on. A package is a folder containing one or more folder or files with a package.json file in its root folder. There are thousands of open source packages available in npm. Visit https://www.npmjs.com/ to explore the available packages. The packages can be modules that are used on the server-side or browser-side and command-line tools that are useful for performing various operations.

npm packages can be installed locally (per project) or globally (entire system). We can specify how we want to install it using different flags, as follows:

  • If we want to install a package globally, we should use the --global or -g flag.
  • If the package should be installed locally for a specific project, use the --save or -S flag.
  • If the package should be installed locally and it is only used for development purposes, use the --save-dev or -D flag.
  • If you run npm install <package-name> without any flags, it will install the package locally but will not update the package.json file. It is not recommended to install packages without the -S or -D flags.

Let's install a command-line tool using npm called http-server: https://www.npmjs.com/package/http-server. It is a simple tool that can be used to serve static files over an http-server just like how files are served in Apache or Nginx. This is useful for testing and developing our web applications, since we can see how our application behaves when it's served through a web server.

Command-line tools are mostly recommended to install globally if they are going to be used only by ourselves and not by any other developer. In our case, we are only going to be using the http-server package. So, let's install it globally. Open your Terminal/command prompt and run the following command:

npm install -g http-server
If you are using Linux, some times you might face errors such as permission denied or unable to access file, and so on. Try running the same command as administrator (prefixed with sudo) for installing the package globally.

Once the installation is complete, navigate to the root folder of our ToDo List app in your terminal and run the following command:

http-server

You will receive two URLs and the server will start running, as follows:

  • To view the ToDo List app on your local device, open the URL starting with 127 in your browser
  • To view the ToDo List app on a different device connected to your local network, open the URL starting with 192 on the device's browser

Every time you open the application, http-server will print the served files in the terminal. There are various options available with http-server, such as -p flag, which can be used to change the default port number 8080 (try http-server -p 8085). Visit the http-server: https://www.npmjs.com/package/http-server, npm page for documentation on all available options. Now that we have a general idea of the npm packages, let's install Babel to transpile our ES6 code to ES5.

We will be using Terminals a lot in our upcoming chapters. If you are using VSCode, it has an inbuilt terminal, which can be opened by pressing Ctrl+` on Mac, Linux, and Windows. It also supports opening multiple terminal sessions at the same time. This can save you lot time on switching between windows.
lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image