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.