實作 To do List SPA(上):前端部份


Posted by Nicolakacha on 2020-09-12

這次要實作的是一個可以把 Todos 存到資料庫的 To do List ,每次按下儲存,To do List 就會給我們一個獨特的 userID,下次只要在網址列上加上這個 userID,就能訪問自己之前存下的 Todos。

而基本的功能會有:新增、刪除、編輯、篩選

本篇會使用 JQuery 和 Bootstrap 這兩個工具來幫助開發,當然使用原生的 JavaScript 也是可以達成的,可參考 You might not need JQuery

要做 SPA,就要來複習一下什麼是 SPA,Single Page Application 顧名思義,只會有一個頁面,所有神奇的事情和故事都在這個頁面上發生。SPA 做到前後端分離,後端只負責和資料相關的部份,前端用 ajax 和後端互動拿到資料之後,再透過 JavaScript 來處理資料,決定資料要怎麼渲染到畫面上。

先來思考一下需要什麼檔案來做哪些事:

  • HTML, CSS 來做基本的 To do List 架構和外觀:index.html, style.css
  • JavaScript 幫我們操控 DOM,得以在畫面上做到 CRUD:script.js
  • 讀取 todos 的 API:api_todos.php
  • 儲存 todos 的 API:api_add_todos.php
  • 連線資料庫:conn.php
  • 準備 MySQL 資料庫

我們可以做一個純前端版本的 To do list,再來做資料庫和 API 的部份,最後用 JavaScript 串 API。

先來看一下程式碼吧,首先是 HTML 的部份,用 Bootstrap 很快建立起我們的頁面,兩個 modal 的作用是顯示編輯框框和儲存時告訴使用者 UserId 的框框:

  <body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4 d-flex justify-content-between">
      <a class="navbar-brand" href="#">Todo List</a>
      <button class="save btn btn-primary" data-toggle="modal" data-target="#save">儲存</button>
    </nav>

    <div class="main container">

      <div class="new-todo-input row">
        <div class="col-12 col-md-8">
          <div class="form-group">
            <input maxlength="40" class="todo-input form-control" placeholder="Please add something here">
          </div>
        </div>
        <div class="col-12 col-md-4">
          <button type="submit" class="submit btn btn-primary btn-block">新增</button>
        </div>
      </div>

      <div class="filter mt-1 mb-2"> 
        <button class="all btn btn-primary">全部</button>
        <button class="filter-ongoing btn btn-primary">未完成</button>
        <button class="filter-completed btn btn-primary">已完成</button>  
      </div>

      <div>
        <ul class="todo-list list-group list-group-flush"></ul>
      </div>

      <div class="modal fade" id="edit-content" tabindex="-1" aria-hidden="true">
      ...
      </div>

      <div class="modal fade" id="save" tabindex="-1" aria-hidden="true">
      ...
      </div>
    </div>
  </body>

Script.js
首先先準備一個模板,是為了新增 todo 的時候用的,也需要一個提醒訊息,這是輸入框為空卻按下送出時就出現的提醒:

const template = `
  <li class="className todo list-group-item d-flex justify-content-between align-items-center">
    <div class="todo-content">
      <span class="content">xxxxx</span>
      <input type="text" class="d-none">
    </div>
    <div class="functional-btn">
      <button class="check btn btn-primary">已完成</button>
      <button class="edit btn btn-success" data-toggle="modal" data-target="#edit-content">編輯</button>
      <button class="delete btn btn-danger">刪除</button>  
    </div>
  </li>`;

const reminder = `
  <div class="reminder alert alert-danger text-center" role="alert">
    請輸入資料哦!
  </div>`;

列出我們想透過 JQuery 操控 DOM 來做的事:

$(document).ready(() => {
  // 新增 todo
  // 刪除 todo
  // 完成 todo
  // 篩選所有 todos
  // 篩選未完成 todos
  // 篩選已完成 todos
  // 編輯 todo
});

接著就是把每個功能做出來啦:

新增 todo
當 submit 按鈕被點擊的時候,就把 .todo-input 的 value 存起來替換掉模板內放內容的位置,接著清空輸入框,如果這個 todo 不為空,就把這則新留言 prepend 到 .todo-list 中,如果為空,就顯示提醒訊息。

  $('.submit').click(() => {
    const value = encodeHTML($('.todo-input').val());
    const newTodo = template.replace(/xxxxx/gi, value).replace(/className/gi, 'ongoing');
    $('.todo-input').val('');
    if (value.trim() !== '') {
      $('.reminder').remove();
      $('.todo-list').prepend(newTodo);
    } else {
      $('.main').prepend(reminder);
    }
  });

刪除 todo
使用事件代理監聽 .todo-list 區塊,當 .delete.btn 被點擊的時候,我們就把那筆 todo 移除掉:

  $('.todo-list').on('click', '.delete.btn', (e) => {
    $(e.target).parent().parent().remove();
  });

完成 todo
點擊完成時,如果當下是未完成狀態,點擊成為已完成,就做幾件事:

  1. 把未完成按鈕變成已完成
  2. 把已完成按鈕加上已完成的 class
  3. 把已完成按鈕的顏色改掉
  4. 把 todo 的內容加上已完成的 class,然後透過 CSS 把它劃掉
  5. 把 todo 加上 已完成的 class
  6. 隱藏編輯按鈕

如果當下狀態是已完成,點擊成為未完成,就做相反的事:

  1. 把已完成按鈕變成未完成
  2. 把已完成按鈕上已完成的 class 移除
  3. 把已完成按鈕的顏色改回來
  4. 把 todo 的內容加已完成的 class 移除
  5. 把 todo 上已完成的 class 移除
  6. 顯示編輯按鈕

    $('.todo-list').on('click', '.check.btn', (e) => {
     const btn = $(e.target);
     const todoForCheck = $(e.target).parent().parent();
     // 變成沒完成
     if (btn.hasClass('completed')) {
       todoForCheck.removeClass('completed');
       todoForCheck.find('.content').removeClass('completed-item');
       btn.removeClass('completed');
       btn.removeClass('btn-secondary').addClass('btn-primary');
       btn.text('已完成');
       btn.parent().find('.edit').show();
     // 變成已完成
     } else {
       todoForCheck.addClass('completed');
       todoForCheck.find('.content').addClass('completed-item');
       btn.addClass('completed');
       btn.removeClass('btn-primary').addClass('btn-secondary');
       btn.text('未完成');
       btn.parent().find('.edit').hide();
     }
    });
    

篩選全部、未完成、已完成 todos
這三個可以一起做,就是依 class 決定要 hide() 或是 show() 而已,要注意的是,如果使用了 bootstrap 的 d-flex class,display 會一直是 flex,是沒辦法用 hide() 讓 display 變成 none 的。

   // 篩選所有 todos
  $('.main').on('click', '.all', () => {
    $('.todo').addClass('d-flex').show();
  });

  // 篩選未完成 todos
  $('.main').on('click', '.filter-ongoing', () => {
    $('.todo.completed').removeClass('d-flex').hide();
    $('.todo:not(.completed)').addClass('d-flex').show();
  });

  // 篩選已完成 todos
  $('.main').on('click', '.filter-completed', () => {
    $('.todo').removeClass('d-flex').hide();
    $('.todo.completed').addClass('d-flex').show();
  });

編輯 todo
把原來 todo 的值存在變數裡,放到我們準備好的編輯框,編輯完按下確定之後,寫回去剛才的 todo 裡,要注意的是每次都要把 .confirm 的事件監聽停止,不然下次在編輯別的 todo 時,這個事件還是在持續著,就會一起改到別的留言惹,當初自己實作的時候發現的小 bug,後來才解決。

  $('.main').on('click', '.edit', (e) => {
    const originTodo = $(e.target).parent().parent().find('.content').text();
    $('.edit-todo-input').val(originTodo);
    $('.confirm').click(()=> {
      let newTodo = $('.edit-todo-input').val();
      $(e.target).parent().parent().find('.content').text(newTodo);
      $('.confirm').off();
    })
  });

小結

到這裡我們 To do List 的前端版本就完成啦,只是這個留言板還不能真的儲存起來,每次重新整理就清空了,所以我們下一篇要來實作後端 API 並把資料串接起來,讓它成為可以真的儲存的 To do List。

下篇:實作 To do List SPA(下):後端及 API 串接部份


#todo list #API #PHP #javascript







Related Posts

30-Day LeetCoding Challenge 2020 April Week 5 || Leetcode 解題

30-Day LeetCoding Challenge 2020 April Week 5 || Leetcode 解題

MTR04_1115

MTR04_1115

如何在瀏覽器上儲存資料?

如何在瀏覽器上儲存資料?


Comments