🛠️ Todolist: Creating tasks with JavaScript

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ Todolist: Creating tasks with JavaScript

We want to add a task to the taskList when a user clicks on the “add task” button.

Added task to the Todolist with a click

We also want to add a task to the taskList if the user presses the “Enter” button.

Added task to the Todolist by hitting the enter key after typing

The best way to listen to both events (click and enter) is to listen to the form’s submit event.

const todolist = document.querySelector('.todolist')

todolist.addEventListener('submit', event => {
  // Add a new task
})

Note: By default, the submit event navigates a user to the url mentioned it the action attribute. We want to prevent this behavior since we’re building custom functionality.

const todolist = document.querySelector('.todolist')
todolist.addEventListener('submit', event => {
  event.preventDefault()

  // Add a new task
})

There are three steps to creating a task:

  1. Get what the user typed
  2. Create the task element
  3. Add the task element into the DOM

Getting what the user typed

First, we need to know what the user typed into the new task field. We can get this information through the input’s value property.

When we get the value, we also want to trim the contents so we don’t get any extra whitespaces.

todolist.addEventListener('submit', event => {
  // ...
  // Get value of task
  const newTaskField = todolist.querySelector('input')
  const inputValue = newTaskField.value.trim()
  console.log(inputValue)
})
Logging inputValue into the console

Creating a task element

To create a task, we need to create a <li> element. This element should have the .task class.

todolist.addEventListener('submit', event => {
  // ...
  // Create task
  const taskElement = document.createElement('li')
  taskElement.classList.add('task')
})

We need to populate the task’s innerHTML with the checkbox, label, the task name, and the delete button. Here, we can copy-paste the code from the HTML we wrote.

const taskElement = document.createElement('li')
taskElement.classList.add('task')
taskElement.innerHTML = `
  <input type="checkbox" id="task-1" />
  <label for="task-1"> ... </label>
  <span class="task__name">Drink water</span>
  <button type="button" class="task__delete-button">
    <svg viewBox="0 0 20 20"> ... </svg>
  </button>`

The code above would produce a task that says “Drink water”.

We don’t want a task that says “Drink water”. We want a task that’s entered by the user. To do this, we can change the task name to inputValue.

taskElement.innerHTML = `
  <input type="checkbox" id="task-1" />
  <label for="task-1"> ... </label>
  <span class="task__name">${inputValue}</span>
  <button type="button" class="task__delete-button">
    <svg viewBox="0 0 20 20"> ... </svg>
  </button>`

When we use a user-entered value in innerHTML, we must sanitize it. This prevents our website from being susceptible to cross-site-scripting attacks.

<body>
  <form class="todolist"> ... </form>

  <!-- Adds DOMPurify library -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/1.0.7/purify.min.js"></script>
</body>
// Sanitize the innerHTML
taskElement.innerHTML = DOMPurify.sanitize(`
  <input type="checkbox" id="task" />
  <label for="task"> ... </label>
  <span class="task__name">${inputValue}</span>
  <button type="button" class="task__delete-button">
    <svg viewBox="0 0 20 20"> ... </svg>
  </button>
`)

Changing the id

The checkbox in each task needs to have a unique id. And the <label> in each task needs a for attribute that links to the checkbox’s id. (Otherwise, you can’t check the correct checkbox).

The best way to create a unique id is to use generateUniqueString. We talked about this in “Generating unique IDs”.

const generateUniqueString = length =>
  Math.random().toString(36).substring(2, 2 + length)
const uniqueID = generateUniqueString(10)
const taskElement = document.createElement('li')
taskElement.classList.add('task')
taskElement.innerHTML = DOMPurify.sanitize(`
  <input type="checkbox" id="${uniqueID}" />
  <label for="${uniqueID}"> ... </label>
  <span class="task__name">${inputValue}</span>
  <button type="button" class="task__delete-button">
    <svg viewBox="0 0 20 20"> ... </svg>
  </button>
`)

Adding the task element to the DOM

We can add the taskElement to the DOM with appendChild.

const taskList = todolist.querySelector('.todolist__tasks')

todolist.addEventListener('submit', event => {
  // ...
  // Add task element to the DOM
  taskList.appendChild(taskElement)
})

Here’s what you should have:

Task added to DOM

UX Improvements

Notice the task remains in the new task field after we added it to the DOM? This makes it hard for users to enter a second task. They have to delete the first task manually before they can enter a new one.

We can make it easy for them by removing text from the new task field.

todolist.addEventListener('submit', event => {
  event.preventDefault()
  // Get what the user typed

  // Clear the new task field
  newTaskField.value = ''

  // Create task
  // Append to the DOM
})

Next, notice input loses focus if you click on the “add task” button? Again, this makes it hard for users to enter a second task. They have to click on the input again before they can type.

We can put focus back onto the new task form to make it easy for users to enter a second task.

todolist.addEventListener('submit', event => {
  event.preventDefault()
  // Get what the user typed
  // Clear the new task field

  // Bring focus back to input field
  newTaskField.focus()

  // Create task
  // Append to the DOM
})
Focused returned to the input. User is now able to type task, hit enter, then type second task immediately after

Another thing.

Right now, users can add an empty task to the taskList.

User hits enter twice without typing anything into the input. Two new tasks get added to the DOM, but both tasks are empty.

We want to prevent empty tasks from getting into the taskList. We can do this by aborting the listener (and not do anything) unless inputValue is truthy.

todolist.addEventListener('submit', event => {
  event.preventDefault()
  // Get what the user typed
  // Clear the new task field
  // Bring focus back to input field

  // Prevent adding of empty task
  if (!inputValue) return

  // Create task
  // Append to the DOM
})

Cleaning up

Our event listener is pretty long now. We can make it easier to understand by abstracting the part that makes the task into a separate function.

Here’s what it looks like:

const makeTaskElement = taskname => {
  const uniqueID = generateUniqueString(10)
  const taskElement = document.createElement('li')
  taskElement.classList.add('task')
  taskElement.innerHTML = DOMPurify.sanitize(`
    <input type="checkbox" id="${uniqueID}" />
    <label for="${uniqueID}">
      <svg viewBox="0 0 20 15">
        <path d="M0 8l2-2 5 5L18 0l2 2L7 15z" fill-rule="nonzero" />
      </svg>
    </label>
    <span class="task__name">${taskname}</span>
    <button type="button" class="task__delete-button">
      <svg viewBox="0 0 20 20">
        <path d="M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z" />
      </svg>
    </button>
  `)
  return taskElement
}

Using it:

todolist.addEventListener('submit', event => {
  ev.preventDefault()
  // ...
  const taskElement = makeTaskElement(inputValue)
  // ...
})

That’s it!