這次要實作的是一個可以把 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
點擊完成時,如果當下是未完成狀態,點擊成為已完成,就做幾件事:
- 把未完成按鈕變成已完成
- 把已完成按鈕加上已完成的 class
- 把已完成按鈕的顏色改掉
- 把 todo 的內容加上已完成的 class,然後透過 CSS 把它劃掉
- 把 todo 加上 已完成的 class
- 隱藏編輯按鈕
如果當下狀態是已完成,點擊成為未完成,就做相反的事:
- 把已完成按鈕變成未完成
- 把已完成按鈕上已完成的 class 移除
- 把已完成按鈕的顏色改回來
- 把 todo 的內容加已完成的 class 移除
- 把 todo 上已完成的 class 移除
- 顯示編輯按鈕
$('.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。