<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Todo List</title>
<!--  引入 Bootstrap  -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
    integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<!-- 引入 jQuery -->
  <script src="http://code.jquery.com/jquery-latest.js"></script>
</head>
<body>
  <div class="container">
    <div class="card mx-auto mt-5 w-50">
      <div class="card-body">
        <h2 class="text-center">Todo List</h2>
        <!-- Dump: 輸出資料 -->
        <button class="btn btn-outline-dark btn-dump mb-2 float-right" type="button">Dump</button>
        <div class="input-group mb-3">
          <input type="text" class="form-control input-todo" placeholder="新增待辦事項...">
          <div class="input-group-append">
            <button class="btn btn-dark btn-add" type="button">新增</button>
          </div>
        </div>
        <ul class="list-group todos">
          <!-- todo -->
        </ul>
      </div>
    </div>
  </div>
</body>
</html>
let id = 0;
// 設把 todos 放到 state 物件中
let state = {
  todos: []
}

// 更新 state
function updateState(newState) {
  state = newState;
  render();
}

// state => UI
function render() {
  // 先把畫面清空
  $('.todos').empty();
  $('.todos').append(
    // 把每個 todo 的 HTML 集合起來放到畫面上
    state.todos.map(todo => Todo(todo)).join('')
  );
}

// Todo component
function Todo({id, content, isDone}) {
  return `
    <li class="todo list-group-item d-flex justify-content-between align-items-center" data-id="${id}">
      <div class="todo-title ">${content}</div>
      <div class="btn-group">
        ${Button({
          // 根據不同狀態回傳不同 UI
          className: isDone ? 'btn-done btn-outline-success' : 'btn-undone btn-outline-secondary',
          content: isDone ? '已完成' : '未完成'
          })}
        ${Button({
          className: 'btn-delete btn-outline-danger',
          content: '刪除'
          })}
      </div>
    </li>
  `
}

// Button component
function Button(props) {
  return `
    <button class="btn ${props.className}" type="button">${props.content}</button>
  `
}

// 新增 todo
$('.btn-add').click(() => {
  const content = $('.input-todo').val();
  if (!content) return;
  $('.input-todo').val('');
  // 更新 state
  updateState({
    todos: [...state.todos, {
      id,
      content,
      isDone: false
    }]
  });
  id++;
});

// 刪除 todo
$('.todos').on('click', '.btn-delete', e => {
  const id = Number($(e.target).parents('.todo').attr('data-id'));
  updateState({
    todos: state.todos = state.todos.filter(todo => todo.id !== id)
  });
});

// 未完成 -> 已完成
$('.todos').on('click', '.btn-undone', e => {
  const id = Number($(e.target).parents('.todo').attr('data-id'));
  updateState({
    todos: state.todos.map(todo => {
      if (todo.id !== id) return todo;
      return {
        ...todo,
        isDone: true
      }
    })
  });
}); 

// 已完成 -> 未完成
$('.todos').on('click', '.btn-done', e => {
  const id = Number($(e.target).parents('.todo').attr('data-id'));
  updateState({
    todos: state.todos.map(todo => {
      if (todo.id !== id) return todo;
      return {
        ...todo,
        isDone: false
      }
    })
  });
});

// 印出 todo 資料 
$('.btn-dump').click(() => {
  console.log(state);
});
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.