One of the projects in the Treehouse Front End Web Development course is to wire up a To-Do List using JavaScript. I was able to make the to-do list fully functional as well as add some additional drag-and-drop functionality using the Sortable jQuery plugin. The full project files are up on GitHub, but I wanted to walk through the JavaScript used to get this project up and going as well as my thought process behind some of the trickier areas.
The initial step was to define some variables including the incompleteTaskHolder and completedTaskHolder that I could reference later on.
[code language=”javascript”]
var newTaskField = document.getElementById("new-task")
var addButton = document.getElementById("add");
var listItem = document.getElementsByTagName("li");
var newTask;
var incompleteTaskHolder = document.getElementById("incomplete-tasks");
var completedTasksHolder = document.getElementById("completed-tasks");
var noTasks = ‘<div id="emptyToDo">Nothing to do today!</div>’;
var noMoreTasks = ‘<div id="emptyToDo">Nothing else to do today!</div>’;
var noCompleted = ‘<div id="emptyToDo">You haven\’t done anything!</div>’;
[/code]
Next, I defined some functions I could use to display empty messages for both the incompleteTaskHolder and completedTaskHolder. I specified a different message if the user had no incomplete tasks but did have complete tasks (“Nothing else to do today!”) versus if both sections were empty (“Nothing to do today!”).
[code language=”javascript”]
function hideEmptyToDo () {
if (incompleteTaskHolder.getElementsByTagName("li")[0] === undefined && completedTasksHolder.getElementsByTagName("li")[0] !== undefined) {
incompleteTaskHolder.innerHTML = noMoreTasks;
} else if (incompleteTaskHolder.getElementsByTagName("li")[0] === undefined ) {
incompleteTaskHolder.innerHTML = noTasks;
}
};
function hideEmptyCompleted () {
if (completedTasksHolder.getElementsByTagName("li")[0] === undefined ) {
completedTasksHolder.innerHTML = noCompleted;
}
};
[/code]
Next, I created a function I could use to build a new task when the user pressed the “Add” button. The function accepts an argument that will represent the user’s input into the newTaskField.
[code language=”javascript”]
function buildTask (a) {
// Create list item
var taskItem = document.createElement("li");
// Create checkbox
var checkbox = document.createElement("input");
checkbox.type = "checkbox";
// Create label
var label = document.createElement("label");
label.innerText = a;
// Create checkbox
var textField = document.createElement("input");
textField.type = "text";
// Create Edit button
var editButton = document.createElement("button");
editButton.classList.add("edit");
editButton.innerText = "Edit";
// Create Delete button
var deleteButton = document.createElement("button");
deleteButton.classList.add("delete");
deleteButton.innerText = "Delete";
// Append everything
taskItem.appendChild(checkbox);
taskItem.appendChild(label);
taskItem.appendChild(textField);
taskItem.appendChild(editButton);
taskItem.appendChild(deleteButton);
return taskItem;
};
[/code]
Now, it was time to make the “Add” button fully functional. The function assigns the input field value to the variable newTask. If the incompleteTaskHolder is currently empty, the innerHTML is set to newTask. If the incompleteTaskHolder is not empty, newTask is appended to the existing items. I also added a check to ensure the user didn’t input a series of spaces as a task. The newTaskField value also was reset to being blank at the end.
[code language=”javascript”]
addButton.onclick = function () {
var newTask = newTaskField.value;
if (newTaskField.value === "" || $.trim(newTask) === "") {
alert("You have to enter a task!");
} else {
if (incompleteTaskHolder.innerHTML === noTasks || incompleteTaskHolder.innerHTML === noMoreTasks ) {
// If it’s the first item, replace innerHTML
incompleteTaskHolder.innerHTML = "";
incompleteTaskHolder.appendChild(buildTask(newTask));
} else {
// Add item to ToDo
incompleteTaskHolder.appendChild(buildTask(newTask));
}
// Clear Add Item field
newTaskField.value = "";
};
bindEvents();
};
[/code]
With the Add function mostly working, it was time to take a look at the Edit function. The editTask function would run when the Edit/Save button was pressed. When the task was being edited, the class “editMode” would be applied and the button would read “Save.” When the item was not being edited, the class “editMode” would not be applied, and the button would read “Edit.”
I needed a way to actually grab the task that was being edited so I used this.parentNode and assigned it to the editedTask variable. Then, I used an if/then statement to look at the editedTask and determine if it was already in editMode. If the task was already in editMode and someone pressed the Save button, the function would set the label to the text input, remove the editMode class, and set the button text back to “Edit.” If the task wasn’t in editMode, the class was added, innerText of the button set to “Save,” and the textField value was set to the innerText of the label as a starting point so someone could easily modify an existing task.
[code language=”javascript”]
function editTask() {
editedTask = this.parentNode;
label = editedTask.getElementsByTagName(‘label’);
textField = editedTask.querySelectorAll(‘input[type=text]’);
if (editedTask.className === "editMode") {
if (textField[0].value === "" || $.trim(textField[0].value) === "") {
// Don’t allow blank tasks
alert ("You have to enter a task");
} else {
// Set label to newly entered value
label[0].innerText = textField[0].value;
// Remove the class editMode
editedTask.classList.remove("editMode");
// Set button back to "Edit"
this.innerText = "Edit";
};
} else {
// Add class editMode
editedTask.classList.add("editMode");
// Set button to "Save"
this.innerText = "Save";
// Set text field to previous label value
textField[0].value = label[0].innerText;
}
};
[/code]
The Delete function was relatively painless. The function was setup so that if the parentNode of the specific “Delete” button was in the incompleteTaskHolder or completedTaskHolder, the removeChild method was called removing the specific task. Then, I ran hideEmptyToDo again to check if either task holder was empty.
[code language=”javascript”]
function deleteTask() {
deletedTask = this.parentNode;
if (deletedTask.parentNode === incompleteTaskHolder) {
incompleteTaskHolder.removeChild(deletedTask);
hideEmptyToDo();
} else if (deletedTask.parentNode === completedTasksHolder) {
completedTasksHolder.removeChild(deletedTask);
hideEmptyCompleted();
}
};
[/code]
The next task was to address the checking/unchecking of a task. If a task was checked off, the item was removed from the incompleteTaskHolder and appended to the completedTasksHolder. Vice versa for unchecking a task.
[code language=”javascript”]
function checkTask() {
checkedTask = this.parentNode;
if (this.checked) {
if (completedTasksHolder.innerHTML === noCompleted) {
// Set innerHTML to blank and append checked task
completedTasksHolder.innerHTML = "";
completedTasksHolder.appendChild(checkedTask);
} else {
// Item is added to Completed list
// Item is crossed off
incompleteTaskHolder.removeChild(checkedTask);
completedTasksHolder.appendChild(checkedTask);
};
hideEmptyToDo();
} else {
// Item is not crossed off
// Item is added to TODO
completedTasksHolder.removeChild(checkedTask);
if (incompleteTaskHolder.innerHTML === noTasks || incompleteTaskHolder.innerHTML === noMoreTasks){
incompleteTaskHolder.innerHTML = "";
incompleteTaskHolder.appendChild(checkedTask);
} else {
incompleteTaskHolder.appendChild(checkedTask);
};
hideEmptyCompleted();
}
};
[/code]
Finally, I had to add an event listener to the individual buttons that would fire the corresponding events. Since there could be an infinite number of tasks, I used a for loop with a counter to loop through the list items. The individual buttons were defined as childNodes of the list item. Then, a click listener was added to the button and the appropriate function was specified for each button. For the last step, I called the bindEvents function to kick things off.
[code language=”javascript”]
// Loop over list items and bind events to click
function bindEvents () {
for (var i = 0; i < listItem.length; i++ ) {
var editButton = listItem[i].childNodes[3];
var deleteButton = listItem[i].childNodes[4];
var checkbox = listItem[i].firstChild;
editButton.addEventListener("click", editTask);
deleteButton.addEventListener("click", deleteTask);
checkbox.addEventListener("click", checkTask);
}
};
// Call binding event
bindEvents();
[/code]