Simple JavaScript TodoList

by Ayo Isaiah

HTML

<main>
  <div class="container">
    <h1 class="app-title">todos</h1>
    <ul class="todo-list js-todo-list"></ul>
    <!-- add the empty state here -->
    <div class="empty-state">
      <svg class="checklist-icon"><use href="#checklist-icon"></use></svg>
      <h2 class="empty-state__title">Add your first todo</h2>
      <p class="empty-state__description">What do you want to get done today?</p>
    </div>
    <!-- end -->
    <form class="js-form">
      <input autofocus type="text" aria-label="Enter a new todo item" placeholder="E.g. Build a web app" class="js-todo-input">
    </form>
  </div>
</main>

<svg>
  <defs>
    <symbol id="checklist-icon" viewBox="-52 0 512 512"><path d="m217.140625 47.226562c0 7.1875-5.832031 13.015626-13.019531 13.015626-7.191406 0-13.019532-5.828126-13.019532-13.015626 0-7.191406 5.828126-13.019531 13.019532-13.019531 7.1875 0 13.019531 5.828125 13.019531 13.019531zm0 0"/><path d="m140.917969 98.820312c4.144531 0 7.5-3.355468 7.5-7.5v-11.4375c0-4.140624-3.355469-7.5-7.5-7.5-4.140625 0-7.5 3.359376-7.5 7.5v11.4375c0 4.144532 3.359375 7.5 7.5 7.5zm0 0"/><path d="m267.320312 98.820312c4.144532 0 7.5-3.355468 7.5-7.5v-11.4375c0-4.140624-3.355468-7.5-7.5-7.5-4.140624 0-7.5 3.359376-7.5 7.5v11.4375c0 4.144532 3.359376 7.5 7.5 7.5zm0 0"/><path d="m204.121094 104.925781c8.242187 0 15.984375-3.449219 21.246094-9.464843 2.726562-3.117188 2.40625-7.855469-.710938-10.582032-3.121094-2.726562-7.855469-2.410156-10.582031.710938-2.410157 2.753906-6.039063 4.335937-9.953125 4.335937-3.917969 0-7.542969-1.582031-9.953125-4.335937-2.726563-3.121094-7.464844-3.4375-10.582031-.710938-3.121094 2.726563-3.4375 7.464844-.710938 10.582032 5.257812 6.015624 13 9.464843 21.246094 9.464843zm0 0"/><path d="m162.492188 184.390625c-1.308594-3.929687-5.558594-6.054687-9.484376-4.742187-21.402343 7.132812-37.015624 17.507812-46.878906 25.582031v-9.789063c0-4.140625-3.359375-7.5-7.5-7.5s-7.5 3.359375-7.5 7.5v12.710938c0 11.421875 13.816406 17.8125 22.5...

CSS

html {
  box-sizing: border-box;
}

*, *::before, *::after {
  box-sizing: inherit;
  margin: 0;
  padding: 0;
}

body {
 font-family:
 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans,
  Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
 line-height: 1.4;
}

.container {
  width: 100%;
  max-width: 500px;
  margin: 0 auto;
  padding-left: 10px;
  padding-right: 10px;
  color: #333;
  height: 90vh;
  margin-top: 5vh;
  margin-bottom: 5vh;
  overflow-y: auto;
}

.app-title {
  text-align: center;
  margin-bottom: 20px;
  font-size: 80px;
  opacity: 0.5;
}

svg {
  width: 64px;
  height: 64px;
}

.todo-list {
  list-style: none;
  margin-bottom: 20px;
}

.todo-item {
  margin-bottom: 10px;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.todo-item span {
  flex-grow: 1;
  margin-left: 10px;
  margin-right: 10px;
  font-size: 22px;
}

.done span {
  text-decoration: line-through;
}

input[type="checkbox"] {
  display: none;
}

.tick {
  width: 30px;
  height: 30px;
  border: 3px solid #333;
  border-radius: 50%;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
}

.tick::before {
  content: '✓';
  font-size: 20px;
  display: none;
}

.done .tick::before {
  display: inline;
}

.delete-todo {
  border: none;
  background-color: transparent;
  outline: none;
  cursor: pointer;
}

.delete-todo svg {
  width: 30px;
  height: 30px;
  pointer-events: none;
}

form {
  width: 100%;
  display: flex;
  justify-content: space-between;
}

input[type="text"] {
  width: 100%;
  padding: 10px;
  border-radius: 4px;
  border: 3px solid #333;
}

/* Add this below all the other styles */

.empty-state {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  display: none;
}

.checklist-icon {
  margin-bottom: 20px;
}

.empty-state__title, .empty-state__description {
  margin-bottom: 20px;
}

/* Add this below the other styles */
.todo-list:empty {
  display:...

JavaScript

let todoItems = [];

function renderTodo(todo) {
  localStorage.setItem('todoItems', JSON.stringify(todoItems));

  const list = document.querySelector('.js-todo-list');
  const item = document.querySelector(`[data-key='${todo.id}']`);
  
  if (todo.deleted) {
    item.remove();
    if (todoItems.length === 0) list.innerHTML = '';
    return
  }

  const isChecked = todo.checked ? 'done': '';
  const node = document.createElement("li");
  node.setAttribute('class', `todo-item ${isChecked}`);
  node.setAttribute('data-key', todo.id);
  node.innerHTML = `
    <input id="${todo.id}" type="checkbox"/>
    <label for="${todo.id}" class="tick js-tick"></label>
    <span>${todo.text}</span>
    <button class="delete-todo js-delete-todo">
    <svg><use href="#delete-icon"></use></svg>
    </button>
  `;

  if (item) {
    list.replaceChild(node, item);
  } else {
    list.append(node);
  }
}

function addTodo(text) {
  const todo = {
    text,
    checked: false,
    id: Date.now(),
  };

  todoItems.push(todo);
  renderTodo(todo);
}

function toggleDone(key) {
  const index = todoItems.findIndex(item => item.id === Number(key));
  todoItems[index].checked = !todoItems[index].checked;
  renderTodo(todoItems[index]);
}

function deleteTodo(key) {
  const index = todoItems.findIndex(item => item.id === Number(key));
  const todo = {
    deleted: true,
    ...todoItems[index]
  };
  todoItems = todoItems.filter(item => item.id !== Number(key));
  renderTodo(todo);
}

const form = document.querySelector('.js-form');
form.addEventListener('submit', event => {
  event.preventDefault();
  const input = document.querySelector('.js-todo-input');

  const text = input.value.trim();
  if (text !== '') {
    addTodo(text);
    input.value = '';
    input.focus();
  }
});

const list = document.querySelector('.js-todo-list');
list.addEventListener('click', event => {
  if (event.target.classList.contains('js-tick')) {
    const itemKey = event.target.parentElement.dataset.key;
   ...