確認需求
- 新增 todo
- 編輯 todo
- 刪除 todo
- 標記完成/未完成
- 清空 todo
- 篩選 todo(全部、未完成、已完成)
- 儲存資料功能(註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:
- 不做特別處理
- 有帶 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()
- 將資料回傳到前端
開發流程規劃
- index.html 切版
- 「設定」todoRepository 物件
- 「建立」createTodo() 功能
- 「建立」showAllTodo() 功能
- 「建立」switchTodo() 功能
- 「建立」updateTodo() 功能
- 「建立」deleteTodo() 功能
- 「建立」showActiveTodo() 功能
- 「建立」showCompleteTodo() 功能
- 「建立」clearCompleteTodo() 功能
- 「建立」saveTodo() 功能
- 「建立」save_todo.php
- 「建立」getTodoById() 功能
- 「建立」get_todo.php
- 「建立」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">×</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']);