利用 JavaScript 實作簡易 TodoList


Posted by 小小碼農 on 2021-05-30

從初期規劃,一直到後期實作功能部分的紀錄。利用之前學的透過 JavaScript 操控 DOM 元素,事件傳遞機制與監聽等

主要功能:

  • 新增 todo
  • 刪除 todo
  • 紀錄 todo 完成與否

1. 先大概規劃好 UI,同時內心想好巢狀結構大概會怎麼寫

UI

2. 切版,一邊想著之後要做功能,父子結構要寫好

程式碼如下:

<div class="wrapper">
      <h1>What's Next 🤔</h1>
      <form class="todo__input-block">
        <input
          class="todo__input"
          type="text"
          placeholder="Let's Do Something Cool..."
          minlength="1"
          maxlength="48"
        />
        <button class="todo__add"></button>
      </form>
      <div class="todo__hr"></div>
      <ul class="todo__list">
        <li class="todo">
          <label class="todo__title">
            <input type="checkbox" class="todo__check" />
            <p>eat an apple</p>
          </label>
          <button class="btn-delete"></button>
        </li>

        <li class="todo">
          <label class="todo__title">
            <input type="checkbox" class="todo__check" />
            <p>write code</p>
          </label>
          <button class="btn-delete"></button>
        </li>

        <li class="todo">
          <label class="todo__title">
            <input type="checkbox" class="todo__check" checked />
            <p>work out</p>
          </label>
          <button class="btn-delete"></button>
        </li>

        <li class="todo">
          <label class="todo__title">
            <input type="checkbox" class="todo__check" checked />
            <p>feed dog</p>
          </label>
          <button class="btn-delete"></button>
        </li>

        <li class="todo">
          <label class="todo__title">
            <input type="checkbox" class="todo__check" />
            <p>send postcards</p>
          </label>
          <button class="btn-delete"></button>
        </li>
      </ul>
    </div>

加入 CSS 的部分:

* {
  margin: 0;
  padding: 0;
  font-family: "Rubik", MicrosoftJhengHei, sans-serif;
  /* border: 1px solid red; */
}
.wrapper {
  margin: 50px auto;
  max-width: 450px;
  padding: 20px;
  border-radius: 10px;
  font-size: 22px;
  letter-spacing: 1.2px;
  box-shadow: 0 0 20px rgb(199 197 197);
}
h1,
h2,
h3,
h4,
p {
  margin: 0;
  padding: 0;
}
h1 {
  text-align: center;
  margin-bottom: 10px;
  color: #6c6c6c;
  font-size: 30px;
}
.todo__input-block {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.todo__input {
  width: 350px;
  padding: 8px 12px;
  font-size: 18px;
  border: 1px solid #ddd;
  border-radius: 50px;
  letter-spacing: 1.2px;
  outline: transparent;
  transition: 0.2s ease-in-out;
}
::placeholder {
  padding-left: 10px;
  color: #b4b4b4;
}
.todo__input:hover {
  border: 1px solid #686868;
}
.todo__add {
  border: 1px solid #ddd;
  height: 40px;
  width: 40px;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  margin-right: 5px;
  outline: none;
  transition: 0.2s ease-in-out;
}
.todo__add:hover {
  background: #868686;
}
.todo__add:before,
.todo__add:after {
  content: "";
  position: absolute;
  width: 20px;
  height: 1px;
  background: #6c6c6c;
  transition: 0.2s ease-in-out;
}
.todo__add:hover:before,
.todo__add:hover:after {
  background: white;
}
.todo__add:before {
  transform: translate(-50%, -50%) rotate(-90deg);
}
.todo__add:after {
  transform: translate(-50%, -50%);
}
.todo__hr {
  border-bottom: 1px solid #ddd;
  margin-top: 20px;
}
.todo__list {
  list-style-type: none;
  margin: 30px 20px 10px 20px;
}
.todo__title {
  display: flex;
  align-items: center;
}
.todo__check {
  cursor: pointer;
  margin-right: 12px;
  min-width: 20px;
  height: 20px;
  border-radius: 50%;
}
.todo {
  position: relative;
  display: flex;
  margin-bottom: 10px;
  padding: 10px;
  border-radius: 10px;
  transition: 0.2s ease-in-out;
}
.todo__title {
  flex: 1;
  cursor: pointer;
  padding: 0 10px;
}
.todo:hover {
  background: rgb(235, 235, 235);
}
.todo__title p {
  font-weight: 200;
}
/* 做與沒做  */
input:checked + p {
  text-decoration: line-through #da4141 2px;
}
.btn-delete {
  border: none;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  outline: none;
  transition: 0.2s ease-in-out;
}
.btn-delete:hover {
  background: #868686;
}
.btn-delete:hover:before,
.btn-delete:hover:after {
  background: #e0e0e0;
  transition: 0.2s ease-in-out;
}
.btn-delete:before,
.btn-delete:after {
  content: "";
  background: #6c6c6c;
  width: 20px;
  height: 1px;
  position: absolute;
}
.btn-delete:before {
  transform: translate(-50%, -50%) rotate(-45deg);
}
.btn-delete:after {
  transform: translate(-50%, -50%) rotate(45deg);
}

最後長這樣:
final

3. 實作功能

const addTodos = () => {
        const inputValue = document.querySelector(".todo__input").value;
        // 前後空格去掉
        if (inputValue.trim().length !== 0) {
          const newTodo = document.createElement("li");
          newTodo.classList.add("todo");
          newTodo.innerHTML = `
            <label class="todo__title">
              <input type="checkbox" class="todo__check" />
              <p>${escapeHtml(inputValue)}</p>
            </label>
            <button class="btn-delete"></button>
          `;
          document.querySelector(".todo__list").appendChild(newTodo);
          // 清空輸入欄
          document.querySelector(".todo__input").value = "";
        } else return;
      };

      // 處理特殊字元
      function escapeHtml(unsafe) {
        return unsafe
          .replace(/&/g, "&amp;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;")
          .replace(/"/g, "&quot;")
          .replace(/'/g, "&#039;");
      }

      // click 會新增事件 document.querySelector(".todo__add").addEventListener("click", (e) => {
        addTodos();
        // 預防頁面更新
        e.preventDefault();
      });
      // Enter 會新增事件
      document
        .querySelector(".todo__input")
        .addEventListener("keypress", (e) => {
          // 做不同瀏覽器的判斷
          if (e.which === 13) addTodos();
        });

      // 刪除功能,使用事件代理
      document.querySelector(".todo__list").addEventListener("click", (e) => {
        const target = e.target;
        // 移除 li
        if (target.classList.contains("btn-delete")) {
          target.parentNode.remove();
        }
      });
  • 這邊注意利用跳脫函式 escapeHtml 處理特殊字元,防止程式碼無法正常顯示
    // 處理特殊字元
    function escapeHtml(unsafe) {
      return unsafe
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
    }
    
  • 注意事件代理部分,刪除 todo,點擊右側叉叉圖示,可刪除 todo

    • 遇到問題:動態新增
      由於 todo 是「動態新增」,若直接選取 .btn-delete,將無法對 後來新增的按鈕進行監聽。

    • 解決辦法:事件代理
      我們可透過事件傳遞機制,改成對父元素(事件代理)進行事件監聽,如此即可對底下的所有 .btn-delete 進行監聽。

4.測試功能

final

恩,一切看起來很合理,沒有太誇張的部分,之後有機會就補充關於儲存在 local storage 的部分


#todolist







Related Posts

CH 12 檔案系統與處理(File)

CH 12 檔案系統與處理(File)

利用 Docker Compose 管理多個容器

利用 Docker Compose 管理多個容器

去除陣列中的黑名單(以物件屬性檢查)

去除陣列中的黑名單(以物件屬性檢查)


Comments