And sometimes, no matter how many times I do a lesson, I just don't get it. I have to sleep on it. I have to do side research. And I have to break it down into micro-steps.
This is a case where, after going through all my own methods towards understanding (and, candidly, a nearly tearful moment) the lightbulb illuminated and it became my mission to share my understanding in the event that anyone else was as stuck as I was.
My notes, both digital and paper form, for one (in retrospect) little challenge |
So let's start at the beginning. As a JavaScript programmer you have received a 'mock-up' of a basic To Do list. The HTML and CSS are written but there is no functionality. Here is what you have:
From here we know a few things:
<li><inputtype="checkbox"><label>Pay Bills</label><input type="text"><button class="edit">Edit</button><button class="delete">Delete</button></li>
listItem.classList.toggle("editMode");
var makeButtonsWork = function(taskListItem) {
listItem.classList.toggle("editMode");
var makeButtonsWork = function(taskListItem) {
From here we know a few things:
- User should be able to add a new item
- Once item is added, user should be able to edit the item, or delete the item. This should be able to be done if the item resides in the TODO list or the COMPLETED list.
- User should be able to place a check in the checkbox to move an item from the TODO list to the COMPLETED list. Conversely, if the checkbox is unchecked, the item should move from the COMPLETED list back to the TODO list.
Easy enough, right? Well, it's a lot of code to accomplish this and everyone approaches it differently. Here is how I approached it:
First things first. Let's take a look at the HTML code for this To Do list. It's pretty simple:
<!DOCTYPE html>
<html>
<head>
<title>Todo App</title>
<link href='http://fonts.googleapis.com/css?family=Lato:300,400,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="css/style.css" type="text/css" media="screen" charset="utf-8">
</head>
<body>
<div class="container">
<p>
<label for="new-task">Add Item</label><input id="new-task" type="text"><button>Add</button>
</p>
<h3>Todo</h3>
<ul id="incomplete-tasks">
<li><input type="checkbox"><label>Pay Bills</label><input type="text"><button class="edit">Edit</button><button class="delete">Delete</button></li>
<li class="editMode"><input type="checkbox"><label>Go Shopping</label><input type="text" value="Go Shopping"><button class="edit">Edit</button><button class="delete">Delete</button></li>
</ul>
<h3>Completed</h3>
<ul id="completed-tasks">
<li><input type="checkbox" checked><label>See the Doctor</label><input type="text"><button class="edit">Edit</button><button class="delete">Delete</button></li>
</ul>
</div>
<script type="text/javascript" src="js/app.js"></script>
</body>
</html>
It's pretty simple. One <div>, a <p> to hold the new item input <input>, a <ul> for the incomplete list and a <ul> for the completed list, along with <button> and <input> items for all tasks once entered.
After studying this I can come up with 4 elements that we will probably need to be able to reference and/or manipulate inside of our JavaScript code. They are: the task input, the Add button, the list of incompleted tasks and the list of completed tasks. That's where I start. Let's see how to declare variables and assign their value to each of these items
The new task input
The id for this input is "new-task". Using the JavaScript method getElementbyId we can assign this to a new variable.
var taskInput = document.getElementById("new-task");
The addButton
The button doesn't have an id. But it has a tag name and that is "button". JavaScript has a method called getElementsByTagName. If you study that method name you will notice it says Elements, not just Element. That would suggest that it could return more than one item and it's safe to assume that, if there are more than one, the return would be in the form of an Array. After studying the HTML I know that we are looking for the first button and, in an array, grabbing the first button can be done by using the index of [0].
var addButton = document.getElementsByTagName("button")[0];
The two lists - incomplete and complete
These two lists each have an id. We can use the same method we used to get the "new-task" item.
var incompleteTaskList = document.getElementById("incomplete-tasks");
var completedTaskList = document.getElementById("completed-tasks");
In JavaScript these variables should be defined at the very top of your code. You will see, as we go, that it is standard practice to keep your code organized for both you, and the next person who may have to work with it.
So, in summary, here is what the top of your JavaScript code should look like:
var taskInput = document.getElementById("new-task");
var addButton = document.getElementsByTagName("button")[0];
var incompleteTaskList = document.getElementById("incomplete-tasks");
var completedTaskList = document.getElementById("completed-tasks");
We now have 4 variables that we can act upon as we progress through the challenge of creating a To Do list that does what we want it to do! We are on our way.
For me, the logical next step is creating a way to add a new task to the incomplete task list. We can do that by writing a function that takes the information that the user has entered and builds a task that looks like the existing tasks. Let's revisit the HTML code of an existing task:
<li><input type="checkbox"><label>Pay Bills</label><input type="text"><button class="edit">Edit</button><button class="delete">Delete</button></li>
To break it down further we have the following:
1. <li>
2. <input>
3. <label></label>
4. <input>
5. <button></button>
6. <button></button>
</li>
There are six components to a new task and we need to build them each time a user enters a new task. We should build this element inside of a function and that function should return a complete task item element. We call this function createNewTaskElement. Remember, inside of this function all we do is build the task element. It won't do anything with the element (like add it to the incomplete list) until it is asked to do so.
var createNewTaskElement = function(newTaskUserInput) {
var listItem = document.createElement("li");
var checkBox = document.createElement("input");
var userInputLabel = document.createElement("label");
var editableUserInput = document.createElement("input");
var editButton = document.createElement("button");
var deleteButton = document.createElement("button");
}
We've mirrored the HTML for the Task Element and declared variables inside of the formula, each a creation of the 5 components of each To Do <li>, including the <li> itself. As variables we are now able to act upon them, modify them and append them to create a complete <li>.
Next, let's modify the two <input> variables so they reflect their type:
checkBox.type = "checkbox";
editableUserInput.type = "text";
We have buttons that need both labels and classes assigned to them. We can add this information:
editButton.innerText = "Edit";
editButton.className = "edit";
deleteButton.innerText = "Delete";
deleteButton.className = "delete";
And now we need to make the value of the userInput equal to the argument that is passed to this function, the newTaskUserInput:
userInputLabel.innerText = newTaskUserInput;
Now that everything has its values, classes and labels we can chain them together to create a single object. We start with the listItem and add to it:
listItem.appendChild(checkBox);
listItem.appendChild(userInputLabel);
listItem.appendChild(editableUserInput);
listItem.appendChild(editButton);
listItem.appendChild(deleteButton);
And, finally, the last thing the function should do is return the completed object:
return listItem;
Our JavaScript code should now look like this:
var taskInput = document.getElementById("new-task");
var addButton = document.getElementsByTagName("button")[0];
var incompleteTaskList = document.getElementById("incomplete-tasks");
var completedTaskList = document.getElementById("completed-tasks");
var createNewTaskElement = function(newTaskUserInput) {
var listItem = document.createElement("li");
var checkBox = document.createElement("input");
var userInputLabel = document.createElement("label");
var editableUserInput = document.createElement("input");
var editButton = document.createElement("button");
var deleteButton = document.createElement("button");
checkBox.type = "checkbox";
userInputLabel.type = "text";
editButton.innerText = "Edit";
editButton.className = "edit";
deleteButton.innerText = "Delete";
deleteButton.className = "delete";
userInputLabel.innerText = newTaskUserInput;
listItem.appendChild(checkBox);
listItem.appendChild(userInputLabel);
listItem.appendChild(editableUserInput);
listItem.appendChild(editButton);
listItem.appendChild(deleteButton);
return listItem;
}
It's a good time to take a break (and a breath!) and revel in what has been accomplished. A function has been written that builds a complete task item with all the html around it. Exciting!
Onward!
What's next? Well, we have a function but we need to devise a way to call it when the user presses the add button. Let's do that inside of a new function. We will call it addTask.
var addTask = function (){
}
Inside of this function the first thing we should do is create a new variable who's value is equal to the user input. We do this by calling the function defined above and passing the value of the taskInput variable. (Remember, this variable, declared at the top of our JS document, holds the HTML object with the id="new-task" - <label id="new-task> - which is where the user input is placed.) We use the .value JS method to just get the value and not the surrounding HTML.
var listItem = createNewTaskElement(taskInput.value);
Great! We now have a variable that is a complete task element with the value equal to what the user entered.
Logically it now makes sense that we need to move that object to the end of the incompleted tasks list. We have a variable holding that entire list - incompleteTaskList - and we can now add to it by using the .appendChild method.
incompleteTasksHolder.appendChild(listItem);
Ta-da! We have a new function:
var addTask = function (){
var listItem = createNewTaskElement(taskInput.value);
incompleteTaskList.appendChild(listItem);
}
But wait. We know that functions will only do their job when called. Inside of the addTask function we have called the createNewTaskElement function so it has done it's job. But how to we call the addTask function? And when should it be called? Oh yeah - when the Add button has been pressed!
Using an Event Listener method on the addButton variable we can wait patiently for an event and, when it occures (clicking the button) the addTask function will be called.
addButton.addEventListener("click", addTask);
That's it! If you are following along and have this code you should be able to add unlimited To Do items to your list. Have a look and then see what doesn't work or doesn't look right! That's up next.
Alrighty! If you've had a chance to look, this is probably what you are seeing:
And this is a complete picture of your code as it exists now:
var taskInput = document.getElementById("new-task");
var addButton = document.getElementsByTagName("button")[0];
var incompleteTaskList = document.getElementById("incomplete-tasks");
var completedTaskList = document.getElementById("completed-tasks");
var createNewTaskElement = function(newTaskUserInput) {
var listItem = document.createElement("li");
var checkBox = document.createElement("input");
var userInputLabel = document.createElement("label");
var editableUserInput = document.createElement("input");
var editButton = document.createElement("button");
var deleteButton = document.createElement("button");
checkBox.type = "checkbox";
userInputLabel.type = "text";
editButton.innerText = "Edit";
editButton.className = "edit";
deleteButton.innerText = "Delete";
deleteButton.className = "delete";
userInputLabel.innerText = newTaskUserInput;
listItem.appendChild(checkBox);
listItem.appendChild(userInputLabel);
listItem.appendChild(editableUserInput);
listItem.appendChild(editButton);
listItem.appendChild(deleteButton);
return listItem;
}
var addTask = function (){
var listItem = createNewTaskElement(taskInput.value);
incompleteTaskList.appendChild(listItem);
}
addButton.addEventListener("click", addTask);
I personally celebrated the fact that my new task, Buy a Monkey, showed up in the TODO list. Exciting! But there are a couple of things that are broken:
- The edit button doesn't work
- The delete button doesn't work
- The checkbox takes a check, but does nothing
- Our last task remains in the Add Item input box
Because I like small victories I decided to tackle number 4 first. Shouldn't be too hard, should it? Well, it isn't. Simply add code to the end of the addTask function that resets the value of the input box to blank:
taskInput.value = "";
Easy breezy:
Time to tackle those buttons. They don't work. And it seems like we should write a function that tells those buttons what they should do and then that function should be called every time a new list item is created using addTask.
I like to be very literal at this point in my learning. The function we need to write needs to make the buttons work so:
var makeButtonsWork = function() {
}
We need to pass the newly created listItem to this function so that, inside of the function, we can isolate the buttons that belong to that object.
var makeButtonsWork = function(taskListItem) {
}
We have two buttons that we need to work with. We should assign each to a variable to we can call its class (edit button has a class of "edit", delete button has a class of "delete") on it.
var makeButtonsWork = function(taskListItem) {
var editButton = taskListItem.querySelector("button.edit");
var deleteButton = taskListItem.querySelector("button.delete");
}
We've got two variables, one holding the edit button and one holding the delete button. We can now tell the page to go something when each button is pressed by using .onclick.
var makeButtonsWork = function(taskListItem) {
var editButton = taskListItem.querySelector("button.edit");
var deleteButton = taskListItem.querySelector("button.delete");
editButton.onclick();
deleteButton.onclick();
}
Well, we have a placeholder inside of the (), but we haven't yet told the program what to do .onclick. We want a function run! Time for two new functions. Let's start with delete:
var deleteTask = function() {
}
Voila! We've defined a function. Thinking about it logically, when the function is run (i.e, when the delete button is pressed) we should remove the <li> from the <ul>. So, inside of the function we need to create a variable that captures the current list item - the one that belongs to the button that has been pressed. We have a neat tool called this.parentNode. This, in this case, is the button. When we use .parentNode it will return the parent of the button. Let's look at the HTML again:
<li><inputtype="checkbox"><label>Pay Bills</label><input type="text"><button class="edit">Edit</button><button class="delete">Delete</button></li>
What we know is that the <button> is a child of the <li>. So to capture the <li> we just declare a variable equal to this(this button).parentNode(the li):
var deleteTask = function() {
var listItem = this.parentNode;
}
We can then traverse further up the DOM by calling the .parentNode on the listItem, returning the <UL>.
var deleteTask = function() {
var listItem = this.parentNode;
ul = listItem.parentNode
}
We now have the <ul> and the specific <li> identified and assigned to a variable. We can easily remove the <li> from the <ul> using .removeChild().
var deleteTask = function() {
var listItem = this.parentNode;
ul = listItem.parentNode
ul.removeChild(listItem);
}
Add that back into the makeButtonsWork function and you get this:
var makeButtonsWork = function(taskListItem) {
var editButton = taskListItem.querySelector("button.edit");
var deleteButton = taskListItem.querySelector("button.delete");
editButton.onclick = ();
deleteButton.onclick = (deleteTask);
}
We now have a fully functioning delete button that calls a deleteTask function when pressed. Wahoo! Except......we havent't added all of this functionality to the addTask. We want to make those buttons work when the listItem is created. Call this function inside of the addTask, right before we reset the taskInput to "" and pass the listItem to the function.
makeButtonsWork(listItem);
On to the edit button. When that button is pressed we need it to allow us to edit the task. Looking at the HTML we see the existing list item that is in edit mode. For that element, a class of "editMode" is inside of the <li> element: <li class="editMode">. That's a pretty good clue. When we press the edit button we need to add that class and when we press it again, remove it.
As we think about this some more it's probably important to first check the class and see if it is in "editMode" if it is, route a should be taken, if not, route be. Sounds like and if....else statement. Let's see.
First, lets declare a variable with the function attached to it:
var editTask = function() {
}
Like the deleteTask function we need to capture the listItem in a variable so we can act upon it. Let's use this.parentNode again:
var editTask = function() {
var listItem = this.parentNode;
}
The listItem has two properties we need to manipulate, the input element for text and the label element. We need to be able to work with both:
var editTask = function() {
var listItem = this.parentNode;
var editableInput = listItem.querySelector("input[type=text]");
var label = listItem.querySelector("label");
}
Now we need to make some sense out of the "editMode" class on the <li>. After digging into the HTML, CSS and the developer tools in Google Chrome I think I finally made sense of it. Bear with me here:
When we click on the edit mode we want the input[type=text] element to display and the label to hide. When we are not in the edit mode the input[type=text] is hidden and only the label shows.
Said differently, if class="editMode" is not in the <li> we see the label. When class="editMode" in in the <li> we see the input[type=text]. This was discerened from the different .css rules written.
We know, when a new listItem is created using the addTask function, the label has a value but the input[type=text] doesn't.
So, in our editTask function we want to check and see, first, if the editMode class is present. If it is we are seeing the input[type=text]. In this mode we set the label equal to the input[type = text]. Ifhe editTask class is not present we set the input[type]=text value equal to the label.
We can use a cool JavaScript method chain of .classList.contains("editMode') on the listItem variable. Assigning that you a variable, called containsClass, will return either true or false.
In code:
var editTask = function() {
var listItem = this.parentNode;
var editableInput = listItem.querySelector("input[type=text]");
var label = listItem.querySelector("label");
var containsClass = listItem.classList.contains("editMode");
if(containsClass) { //this means if containsClass=true
label.innerText = editableInput.value;
} else
editableInput.value = label.innerText;
}
}
Lastly, we have a method called toggle that we can call on the listItem and its classList passing the value of the class that we have been working with "editMode".
listItem.classList.toggle("editMode");
The editTask code in totality:
var editTask = function() {
var listItem = this.parentNode;
var editableInput = listItem.querySelector("input[type=text]");
var label = listItem.querySelector("label");
var containsClass = listItem.classList.contains("editMode");
if(containsClass) { //this means if containsClass=true
label.innerText = editableInput.value;
} else
editableInput.value = label.innerText;
listItem.classList.toggle("editMode");
}
Remember to call this function back in the makeButtonsWork(taskListItem) function:
var makeButtonsWork = function(taskListItem) {
var editButton = taskListItem.querySelector("button.edit");
var deleteButton = taskListItem.querySelector("button.delete");
editButton.onclick = (editTask);
deleteButton.onclick = (deleteTask);
}
We should now have functional code that will allow us to add items, edit items and deletem items. Pretty cool!
var taskInput = document.getElementById("new-task");
var addButton = document.getElementsByTagName("button")[0];
var incompleteTaskList = document.getElementById("incomplete-tasks");
var completedTaskList = document.getElementById("completed-tasks");
var createNewTaskElement = function(newTaskUserInput) {
var listItem = document.createElement("li");
var checkBox = document.createElement("input");
var userInputLabel = document.createElement("label");
var editableUserInput = document.createElement("input");
var editButton = document.createElement("button");
var deleteButton = document.createElement("button");
checkBox.type = "checkbox";
userInputLabel.type = "text";
editButton.innerText = "Edit";
editButton.className = "edit";
deleteButton.innerText = "Delete";
deleteButton.className = "delete";
userInputLabel.innerText = newTaskUserInput;
listItem.appendChild(checkBox);
listItem.appendChild(userInputLabel);
listItem.appendChild(editableUserInput);
listItem.appendChild(editButton);
listItem.appendChild(deleteButton);
return listItem;
}
var addTask = function (){
var listItem = createNewTaskElement(taskInput.value);
incompleteTaskList.appendChild(listItem);
makeButtonsWork(listItem);
taskInput.value = "";
}
var deleteTask = function() {
var listItem = this.parentNode;
ul = listItem.parentNode
ul.removeChild(listItem);
}
var editTask = function() {
var listItem = this.parentNode;
var editableInput = listItem.querySelector("input[type=text]");
var label = listItem.querySelector("label");
var containsClass = listItem.classList.contains("editMode");
if(containsClass) { //this means if containsClass=true
label.innerText = editableInput.value;
} else
editableInput.value = label.innerText;
listItem.classList.toggle("editMode");
}
var makeButtonsWork = function(taskListItem) {
var editButton = taskListItem.querySelector("button.edit");
var deleteButton = taskListItem.querySelector("button.delete");
editButton.onclick = (editTask);
deleteButton.onclick = (deleteTask);
}
addButton.addEventListener("click", addTask);
We are in the home stretch! All that's left is the checkbox and giving it some functionality.
We can follow the logic of the makeButtonsWork function but, in addition to the taskListItem we need to pass a formula that will mark the task as completed or incomplete and make some events happen like moving the item from one list to another. Instead of onclick we can use onchange.
var makeCheckboxWork = function(taskListItem, completeOrIncomplete) {
var checkBox =
taskListItem.querySelector("input[type=checkbox]");
checkBox.onchange = completeOrIncomplete;
}
Two new functions are necessary. One for when the item is being marked complete and one for when the item is being marked incomplete. Let's start with completed items. We again create a variable for the ListItem equal to the parent element of the button. We can then append this listItem to as a child to the completedTaskList (defined at the very beginning of this exercise). Lastly, we need to call the makeCheckboxWork function and pass both the taskListItem and another function, taskIncomplete which we will write next.
var taskCompleted = function() {
var listItem = this.parentNode;
completedTaskList.appendChild(listItem);
makeCheckboxWork(listItem, taskIncomplete);
}
The opposite of taskCompleted is taskIncomplete. It's nearly identical except that we are appending to the incompleteTaskList and calling the taskComplete function inside of the makeCheckboxWork:
var taskIncomplete = function() {
var listItem = this.parentNode;
incompleteTaskList.appendChild(listItem);
makeCheckboxWork(listItem, taskComplete);
}
Now, the very last thing to do is activate the checkboxes when the listItem is created. Back inside of the addTask call the makeCheckboxWork.
And that should complete our code! Now, a missing element - the placeholder <li> elements have no functionality. You can cycle over those items and make the buttons and checkbox work.
A summary of the code, in it's entirity:
var taskInput = document.getElementById("new-task");
var addButton = document.getElementsByTagName("button")[0];
var incompleteTaskList = document.getElementById("incomplete-tasks");
var completedTaskList = document.getElementById("completed-tasks");
var createNewTaskElement = function(newTaskUserInput) {
var listItem = document.createElement("li");
var checkBox = document.createElement("input");
var userInputLabel = document.createElement("label");
var editableUserInput = document.createElement("input");
var editButton = document.createElement("button");
var deleteButton = document.createElement("button");
checkBox.type = "checkbox";
editableUserInput.type = "text";
editButton.innerText = "Edit";
editButton.className = "edit";
deleteButton.innerText = "Delete";
deleteButton.className = "delete";
userInputLabel.innerText = newTaskUserInput;
listItem.appendChild(checkBox);
listItem.appendChild(userInputLabel);
listItem.appendChild(editableUserInput);
listItem.appendChild(editButton);
listItem.appendChild(deleteButton);
return listItem;
}
var addTask = function (){
var listItem = createNewTaskElement(taskInput.value);
incompleteTaskList.appendChild(listItem);
makeButtonsWork(listItem);
makeCheckboxWork(listItem, taskCompleted);
taskInput.value = "";
}
var deleteTask = function() {
var listItem = this.parentNode;
ul = listItem.parentNode
ul.removeChild(listItem);
}
var editTask = function() {
var listItem = this.parentNode;
var editableInput = listItem.querySelector("input[type=text]");
var label = listItem.querySelector("label");
var containsClass = listItem.classList.contains("editMode");
if(containsClass) { //this means if containsClass=true
label.innerText = editableInput.value;
} else
editableInput.value = label.innerText;
listItem.classList.toggle("editMode");
}
var makeButtonsWork = function(taskListItem) {
var editButton = taskListItem.querySelector("button.edit");
var deleteButton = taskListItem.querySelector("button.delete");
editButton.onclick = (editTask);
deleteButton.onclick = (deleteTask);
}
var taskCompleted = function() {
var listItem = this.parentNode;
completedTaskList.appendChild(listItem);
makeCheckboxWork(listItem, taskIncomplete);
}
var taskIncomplete = function() {
var listItem = this.parentNode;
incompleteTaskList.appendChild(listItem);
makeCheckboxWork(listItem, taskCompleted);
}
var makeCheckboxWork = function(taskListItem, completeOrIncomplete) {
var checkBox = taskListItem.querySelector("input[type=checkbox]");
checkBox.onchange = completeOrIncomplete;
}
addButton.addEventListener("click", addTask);
I hope that helped you as much as it helped me. This was a difficult challenge and I had to approach it a bit differently than the lesson itself. But I think I get it now. And I at least have a place where I can reference back as I continue down this exciting path.
Cheers!