[ 紀錄 ] 實戰練習 - Todo List ( 以 JS 實作前端 + PHP 後端 )


Posted by krebikshaw on 2020-09-01

確認需求

  1. 新增 todo
  2. 編輯 todo
  3. 刪除 todo
  4. 標記完成/未完成
  5. 清空 todo
  6. 篩選 todo(全部、未完成、已完成)
  7. 儲存資料功能(註1)

註1:會有一個「儲存」的按鈕,按下去以後會把目前 todo 的狀態送到 server 去儲存,並且回傳一個獨特的 id,以後使用者如果有帶這個 id,就自動把它的 todo 載入進來。
舉例來說,原本的網址可能是https://example.com/todos.php ,按下儲存以後網址變成:https://example.com/todos.php?id=5 ,下次我用同樣網址進來時,就可以看到我之前儲存好的 todo item。

資料庫規劃

紀錄所需要的 table 及內容

todo : (使用者每按一次儲存,就存進一筆新的資料)

  • id:存檔 id
  • content:todo 的完整內容
  • all:紀錄當下所有 todo 的總量
  • active:紀錄當下待辦事項的數量
  • completed:紀錄當下完成事項數量
  • created_at:建立日期

檔案路由規劃

index.html (首頁)

  • 新增按鈕 -> 執行 createTodo()
  • 完成事項按鈕 -> 執行 completeTodo()
  • 編輯按鈕 -> 執行 updateTodo()
  • 刪除按鈕 -> 執行 deleteTodo()
  • 篩選全部 todo 按鈕 -> 執行 showAllTodo()
  • 篩選待辦 todo 按鈕 -> 執行 showActiveTodo()
  • 篩選已完成 todo 按鈕 -> 執行 showCompleteTodo()
  • 清除已完成 todo 按鈕 -> 執行 clearCompleteTodo()
  • 存檔按鈕 -> 執行 saveTodo() -> 串接後端 save_todo.php

載入頁面

  • url 沒帶 id 不特別處理
  • url 有帶 id,執行 getTodoById() -> 串接後端 get_todo.php
  • 用變數記住當下的篩選狀態(顯示全部、顯示未完成、顯示已完成),預設為顯示全部

功能細部規劃

☞ 前端

在前端利用 todoRepository 物件,將當下的 todo 狀態(編號、內容、是否完成)暫存起來

init():(初始化動作)

  • 建立一個空物件 todoRepository
  • 判斷 url 是否有帶 id:
    • 有帶 id:
      • 執行 getTodoById(),將取得的資料先填入物件裡
      • 執行 showAllTodo() 將 todoRepository 裡的所有 todo 以及狀態顯示出來
    • 沒帶 id:
      • 不做特別處理
  • 將篩選狀態預設為 showAll

createTodo()

  • 將 todo 寫入 todoRepository,狀態設定為「未完成」
  • 將 todo prepend 在畫面上
  • 清空 input 欄位裡的資料

switchTodo()

  • 判斷 todo 的狀態,將狀態(未完成、已完成)做切換
  • 將 todoRepository 裡的狀態,更新
  • 若更新為已完成
    • 將畫面上的 todo 打勾畫線
  • 若更改為未完成
    • 將畫面上的 todo 恢復原樣

updateTodo()

  • 將 todoRepository 裡的內容更新
  • 將畫面上的 todo 改為新的內容

deleteTodo()

  • 將 todoRepository 裡的 todo 刪除
  • 清除畫面上的 todo

showAllTodo()

  • 取得 todoRepository 裡的所有 todo 以及狀態,並顯示出來
  • 將篩選狀態更新為 showAll

showActiveTodo()

  • 取得 todoRepository 裡狀態為「未完成」的所有 todo,並顯示出來
  • 將篩選狀態更新為 showActive

showCompleteTodo()

  • 取得 todoRepository 裡狀態為「已完成」的所有 todo,並顯示出來
  • 將篩選狀態更新為 showComplete

clearCompleteTodo()

  • 取得 todoRepository 裡狀態為「已完成」的所有 todo,並清除
  • 依照當下的篩選狀態,顯示更新後的畫面

saveTodo()

  • 將 todoRepository 裡的內容(編號、內容、是否完成)轉換成 JSON 格式傳到 api_save_todo.php

getTodoById()

  • 將 id 傳到 api_get_todo.php
  • 將取得後的資料轉回物件格式
  • 把資料寫進 todoRepository

☞ 後端

save_todo.php

  • 確認接受到的資料
    • 若資料有問題則回傳錯誤訊息並 die()
  • 將資料寫進資料庫
  • 取得剛剛那筆資料在資料庫裡的 id (可以利用 order by id desc limit 1 來撈資料)
  • 將 id 回傳到前端

get_todo.php

  • 確認接收到的 id
  • 利用 id 去資料庫撈資料
    • 若撈取失敗則回傳錯誤訊息並 die()
  • 將資料回傳到前端

開發流程規劃

  1. index.html 切版
  2. 「設定」todoRepository 物件
  3. 「建立」createTodo() 功能
  4. 「建立」showAllTodo() 功能
  5. 「建立」switchTodo() 功能
  6. 「建立」updateTodo() 功能
  7. 「建立」deleteTodo() 功能
  8. 「建立」showActiveTodo() 功能
  9. 「建立」showCompleteTodo() 功能
  10. 「建立」clearCompleteTodo() 功能
  11. 「建立」saveTodo() 功能
  12. 「建立」save_todo.php
  13. 「建立」getTodoById() 功能
  14. 「建立」get_todo.php
  15. 「建立」init() 功能

Todo 物件導向規劃

class TodoList {
  constructor(id) {
    // 初始化所有數據
    this.id = id;
    this.cb_id = 1;
    this.countAll = 0;
    this.countActive = 0;
    this.countCompleted = 0;
    this.filterKind = 'showAll';
    // 建立 todoRepository 物件
    this.todoRepository = [{
      cb_id: 0,
      content: 'new todo',
      status: 'active'
    }];
    this.init(this.id);
  }
  // 判斷是否需要載入資料庫資料
  init(id) {
    if (id) {
      this.getTodoById(id);
    }
    this.showAllTodo();
  }
  // 跟後端取得資料庫資料,寫進 todoRepository 物件當中
  getTodoById(id) {
    if (!id) return;
    $.ajax({
      type: 'POST',
      url: './get_todo.php',
      data: {
        id: Number(id),
      },
    })
      .done((res) => {
        if (res.status === 'fail') {
          alert(`${res.message}`);
          return;
        }
        let data;
        try {
          data = JSON.parse(res.result.content);
        } catch (e) {
          console.log(e);
        }
        this.countAll = Number(res.result.all);
        this.countActive = Number(res.result.active);
        this.countCompleted = Number(res.result.completed);
        this.todoRepository = data;
        this.render();
      })
      .fail((res) => {
        console.log(res.message);
      });
  }
  // 新增 todo 處理 
  createTodo(str) {
    if (!str) return;
    this.todoRepository.push({
      cb_id: this.cb_id,
      content: str,
      status: 'active'
    });
    this.showTodo(this.cb_id, str, 'active');
    this.cb_id++;
  }
  // 顯示於頁面中
  showTodo(id, content, status) {
    if (!content) return;
    if (status == 'active') {
      $('.list').append(`
      <li id="${id}">
        <div class="view">
          <input type="checkbox" id="cb_${id}"/>
          <label for="cb_${id}"><span class="switch"></span></label>
          <p class="text">${escape(content)}</p>
          <input type="button" class="btn_delete" data-value="${id}">
        </div>
      </li>
    `);
    }
    if (status == 'completed') {
      $('.list').append(`
      <li id="${id}">
        <div class="view">
          <input type="checkbox" id="cb_${id}" class="checked"/>
          <label for="cb_${id}"><span class="switch checked"></span></label>
          <p class="text checked">${escape(content)}</p>
          <input type="button" class="btn_delete" data-value="${id}">
        </div>
      </li>
    `);
    }
  }

  showAllTodo() {
    for (let i = 0; i < this.todoRepository.length; i++) {
      const item = this.todoRepository[i];
      this.showTodo(item.cb_id, item.content, item.status);
    }
    this.calc();
    this.filterKind = 'showAll';
  }

  showActiveTodo() {
    for (let i = 0; i < this.todoRepository.length; i++) {
      const item = this.todoRepository[i];
      if (item.status == 'active') {
        this.showTodo(item.cb_id, item.content, item.status);
      }
    }
    this.calc();
    this.filterKind = 'showActive';
  }

  showCompletedTodo() {
    for (let i = 0; i < this.todoRepository.length; i++) {
      const item = this.todoRepository[i];
      if (item.status == 'completed') {
        this.showTodo(item.cb_id, item.content, item.status);
      }
    }
    this.calc();
    this.filterKind = 'showCompleted';
  }
  // 將 todoRepository 物件整理乾淨,將序號重新編列
  render() {
    const temp_todo = [];
    const length = this.todoRepository.length;
    let num = 0;
    for (let i = 0; i < length; i++) {
      temp_todo.push({
        cb_id: num,
        content: this.todoRepository[i].content,
        status: this.todoRepository[i].status
      });
      num++;
    }
    this.todoRepository = temp_todo;
    this.cb_id = this.todoRepository.length;

    $('.list').html('');
    if (this.filterKind === 'showAll') {
      todo.showAllTodo();
    }
    if (this.filterKind === 'showActive') {
      todo.showActiveTodo();
    }
    if (this.filterKind === 'showCompleted') {
      todo.showCompletedTodo();
    }
  }
  // 處理勾選或取消勾選
  switchTodo(id) {
    if (!id) return;
    for (let i = 0; i < this.todoRepository.length; i++) {
      const item = this.todoRepository[i];
      if (item.cb_id == id) {
        item.status = item.status === 'active' ? 'completed' : 'active';
      }
    }
    $(`#cb_${id}, #cb_${id} + label span, #cb_${id} ~ p`).toggleClass('checked');
  }

  updateTodo(id, value) {
    if (!id || !value) return;
    $(`#${id}`).html(`
      <div class="view">
        <input type="checkbox" id="cb_${id}"/>
        <label for="cb_${id}"><span class="switch"></span></label>
        <input type="text" class="update" value="${value}"/>
        <input type="button" class="btn_delete" data-value="${id}">
      </div>
    `);
    $(`#${id} .update`).keydown((e) => {
      if (e.keyCode === 13 && e.target.value !== '') {
        $(`#${id}`).html(`
          <div class="view">
            <input type="checkbox" id="cb_${id}"/>
            <label for="cb_${id}"><span class="switch"></span></label>
            <p class="text">${escape(e.target.value.trim())}</p>
            <input type="button" class="btn_delete" data-value="${id}">
          </div>
        `);
        for (let i = 0; i < this.todoRepository.length; i++) {
          const item = this.todoRepository[i];
          if (item.cb_id == id) {
            item.content = e.target.value.trim();
            item.status = 'active';
          }
        }
      }
    });
  }

  deleteTodo(id) {
    if (!id) return;
    for (let i = 0; i < this.todoRepository.length; i++) {
      if (this.todoRepository[i].cb_id == id) {
        this.todoRepository.splice(i, 1);
      }
    }
    $(`#${id}`).remove();
  }

  clearCompleted() {
    const temp_todo = [];
    const length = this.todoRepository.length;
    for (let i = 0; i < length; i++) {
      if (this.todoRepository[i].status === 'active') {
        temp_todo.push(this.todoRepository[i]);
      }
    }
    this.todoRepository = temp_todo;
    this.calc();
    this.render();
  }
  // 計算並更新當前數據
  calc() {
    let temp_all = 0;
    let temp_active = 0;
    let temp_completed = 0;
    for (let i = 0; i < this.todoRepository.length; i++) {
      if (this.todoRepository[i].status === 'active') {
        temp_active++;
      }
      if (this.todoRepository[i].status === 'completed') {
        temp_completed++;
      }
      temp_all++;
    }
    this.countAll = temp_all;
    this.countActive = temp_active;
    this.countCompleted = temp_completed;
    $('.todo_count strong').text(`${this.countActive}`);
  }
  // 處理存檔功能
  saveTodo() {
    this.render();
    this.calc();
    const json = JSON.stringify(this.todoRepository) || [];
    const form = {
      content: json,
      all: this.countAll,
      active: this.countActive,
      completed: this.countCompleted,
    };
    $.ajax({
      type: 'POST',
      url: './save_todo.php',
      data: form,
    })
      .done((res) => {
        $('body').append(`
          <div class="modal" tabindex="-1">
            <div class="modal-dialog">
              <div class="modal-content">
                <div class="modal-header">
                  <h5 class="modal-title">儲存成功,您此次存檔的 ID 是 ${res.save_id}</h5>
                    <span aria-hidden="true">&times;</span>
                  </button>
                </div>
                <div class="modal-body">
                  <p>請記下您的 ID,在網址列後面加上 ?userID=${res.save_id} 即可訪問個人的 Todo List~</p>
                </div>
                <div class="modal-footer">
                  <button type="button" class="close_modal btn btn-primary">我知道了</button>
                </div>
              </div>
            </div>
          </div>
        `);
        $('.close_modal').click(() => {
          $('.modal').remove();
        });
      })
      .fail((res) => {
        console.log('失敗: ', res.message);
      });
  }
}
const todo = new TodoList(getUrlVars()['id']);

#Todo-List







Related Posts

2 export / import 元件

2 export / import 元件

DJI Tello 的基礎操作與套件

DJI Tello 的基礎操作與套件

API

API


Comments