Node.js+Express — 製作CRUD簡易待辦清單(下)
到目前為止,我們已經製作簡易版的TODO 待辦清單應用程式建立資料(C)與讀取資料庫資料(R)的部分,接下來我們要繼續進行修改資料(U)與刪除資料(D)的說明。
U修改資料
還記得我們之前寫的使用情境嗎?
情境說明
讀取:進入頁面時,應用程式會自行從資料庫中取出所有資料,並顯示在列表中。
新增:頁面中有一個input輸入框,讓使用者輸入待辦清單相關的文字,一旦輸入完畢按下輸入框旁邊的『新增項目』按鈕,就可以將文字輸入到資料庫中,並且同時顯示在下面的列表中。
修改:每個列表資料旁邊都有Edit(編輯)與Delete(刪除)按鈕。按下Edit(編輯)後,可以修改文字,並且存回資料庫。回存資料庫的同時,頁面不需重新整理,直接顯示修改後的文字。
刪除:按下Delete(刪除)後,該筆資料直接消失於畫面,並且相對應的資料也從資料庫中刪除。
其中:
修改:…按下Edit(編輯)後,可以修改文字,並且存回資料庫。回存資料庫的同時,頁面不需重新整理,直接顯示修改後的文字。
修改資料可分為下面幾個步驟:
- 監聽按鈕。
- 取得使用者修改的文字
- 將該文字加進資料庫中
- 網頁顯示更新後的內容
這裏,我們需要靜態檔案的協助。首先,在專案的根目錄建立一個public資料夾。
為了達到這個結果,我們要在app.js 裡面加上express內建的static
middleware:
app.use(express.static('public'))
加上middleware之後,在public資料夾裡面新增一個 browser.js 檔案。並且在index.ejs的</body>前面加上一行:
<script src="/browser.js"></script>
我們要利用這個browser.js檔案來寫事件的監聽程式。
監聽
為了讓使用者按下Edit(編輯)按鈕,就可以編輯的程式。必須監聽Edit(編輯)按鈕的click事件:
document.addEventListener('click', function () {})
觀察下面HTML區段,尋找鉤子勾住Edit。
<button class="edit-me btn btn-secondary btn-sm mr-1">Edit</button>
由於Edit(編輯)按鈕(<button>…</button>)上面有一個edit-me的class,我們可以用它來勾住程式。也就是說,按下去的<button>按鈕有edit-me的css class的話就可以執行某項指令,其條件可以寫成:
e.target.classList.contains("edit-me")
由於DOM裡的每個節點上都有一個 classList物件,我們可以使用裡面的方法新增、刪除、修改節點上的CSS類別。使用classList,程式設計師還可以用它來判斷某個節點是否被賦予了某個CSS類別。classList.contains 就是在檢查是否含有某個CSS類別。我們要檢查看看被按下去的按鈕是否包含一個edit-me
的CSS類別,程式可以寫成下面的樣子:
document.addEventListener('click', function (e) {if (e.target.classList.contains("edit-me")) {console.log("You click the edit button")}})
測試上面的程式,應該可以在console區看到You click the edit button
字樣。點擊得越多次,出現越多次。
取得使用者修改的文字
既然,我們已經可以選定edit按鈕了,我們希望近一步點選按鈕之後,可以輸入文字並且進而修改資料庫的內容。先從輸入文字開始。
prompt() 方法可用於顯示對話框讓使用者輸入資料。在這裡我們要使用它,程式可以寫成下面的樣子:
document.addEventListener('click', function (e) {if (e.target.classList.contains("edit-me")) {let userInput = prompt("請修改待辦事項")console.log(userInput)}});
使用者輸入的結果會顯示在網頁的console裡面。
將該文字存入資料庫
接下來,要把使用者輸入的資料存入資料庫中,來取代原來已存在資料庫中的內容。
存入資料庫的方法有很多種,我們在這裡要使用axios的方式。想暸解axios的使用方式,可以網頁上去看看如何使用:https://github.com/axios/axios
在這裡我們選擇使用axios 的CDN。請拷貝下面程式碼到index.ejs裡面。
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
注意,axios 的CDN要放在browser.js的上面一行。
然後,我們要依照 axios.post(a,b).then().catch() 來寫,這裏涉及到promise的概念。其中 a為update-item,b 為物件。
程式可以寫成下面的樣子:
document.addEventListener('click', function (e) {if (e.target.classList.contains("edit-me")) {let userInput = prompt("Enter your desired new text")axios.post('/update-item', {text: userInput}).then(function () {//do something}).catch(err => {console.log(err)})};});
其中text會儲存使用者輸入的文字字串。(text: userInput
)
再來,要切換到app.js。我們需要在上方加上另外一個中介軟體以及路由。
app.use(express.json())
update-item路由,提供給更新資料時使用。先測試看看:
app.post('/update-item', function (req, res) {console.log(req.body.text)res.send("Success")})
這裡有個問題,如何知道要更新的是哪一筆資料?
程式該如何知道使用者想要更新哪一筆資料?為了達成這個目的,我們需要在「按鈕」的部分加上某個「標籤」來區別。我們可以使用 data-id 來解決這個問題。由於data-id是HTML5的屬性,只要有data-開頭的都可以自由的命名。data-id的值,可以去資料庫抓取每一筆資料的key值放入,由於每個key值都是獨一無二、不會重複的。該屬性值在這裡是<%- list %>,透過它就可以區分出每一筆的差異。
data-id="<%- list %>"
我們可以把上面的程式碼加到按鈕上面:
<button data-id="<%- list %>" class="edit-me btn btn-secondary btn-sm mr-1">Edit</button>
這樣子每一筆按鈕上面就存在不同的data-id值。
加上下面程式碼,讓按下「編輯按鈕」時,取得該筆資料的id值。
let _id = e.target.getAttribute("data-id");
可以用console.log測試看看:
console.log(_id)
測試完畢後,要真正來連接資料庫修改裡面的資料。
我們把目光移到app.js檔案中app.post('/update-item',…
的程式碼。為了要連接資料庫中特定的一筆資料,我們需要知道它的 id,這個部分已經在前面取得了id值。只是我們需要把這個id值與目前這個路由連在一起。於是我們要加上id與dbRef兩個變數。其中,id就是前面browser.js檔案裡面透過axios來POST的部分: (id: _id)。我們可以透過req.body.id來取得。
var id = req.body.id
而dbRef是firebase資料庫的路徑。我們想要取得todos下面的某一筆紀錄,可以使用「todos/紀錄的id」的格式來取得。於是,就可以寫成下面式子:
var dbRef = db.ref('todos/' + id)
取得該筆資料的路徑後,就可以進行更新的命令了。在這裏我們可以使用firebase裡面的update方法來進行這項工作。
dbRef.update({item: req.body.text})
因為dbRef已經是指特定的資料,所以我們在後面直接加上update()方法,並且告訴它哪個部分需要更新,這裏要更新的是item。我們一樣使用req.body.text來取得。
你或許不知道req.body.text是什麼。其實這也是從前面browser.js檔案裡面透過axios來POST的部分。下面是browser.js檔案,你可以看到有兩個分別是text與id,而text的值是從userInput而來。如果你還記得的話,userInput是使用者輸入的更新文字。
axios.post(‘/update-item’, {text: userInput,id: _id})
把這一切串起來:
app.post('/update-item', function (req, res) {var id = req.body.idvar dbRef = db.ref('todos/' + id)dbRef.update({item: req.body.text})res.send("Updated Success")})
可以試試看,資料庫的資料有沒有改變?
網頁顯示更新後的內容
最後一個步驟就是要把資料庫更新的結果顯示在網頁上。你會發現目前為止,雖然我們按下編輯,也修改了文字並且也確認資料庫裡的資料已經改變,但是畫面上仍然沒有任何的改變,除非我們重新整理頁面,畫面上的文字才會被更新。
因此,這部分還需要進行一些程式上的修改。首先我們要去找尋文字所在HTML元素的位置,並且找到可以「鉤住它」的東西。經過尋找,我們發現的目標是帶有 item-text 的<span> 標籤。
<span class="item-text">
我們可以給它一個變數,由於這是原有的文字。就命名為 originalText 吧:
let originalText = e.target.parentElement.parentElement.querySelector(".item-text");
這裡我們使用「兩次」parentElement,是因為我們要從e.target實際被點擊的<button>標籤(第30行)向上巡迴兩層DOM到達<li>(第27行),再從<li>向下尋找含有 item-text的<span>標籤(第28行)。
在按鈕按下的時候,我們可以透過 .innerHTML 來讓原來的文字(originalText)變成使用者新加入的文字(userInput)
.then(function () {originalText.innerHTML = userInput})
完成前美化
目前已經算是完成了70%的更新功能,我們覺得還有一些地方可以優化的。
例如當按下更新對話視窗時,裡面是空的沒有任何文字,很可能會不小心按錯,如果把舊有的文字顯示在修改框裡面,不是比較方便?如果要修正這個問題的話,可以在prompt後面再加上:
let userInput = prompt("請修改待辦事項", originalText.innerHTML)
又如,當我們按下編輯,而實際上沒有改變而放棄時,原來的資料反而不見了。
這時候,如果要修正這個問題的話,可以在整段axios.post前面加上一個判斷式,以確認使用者有輸入新的文字,才更新。程式碼可以改成下面:
if (userInput) {axios.post('/update-item', {text: userInput,id: _id}).then(function () {originalText.innerHTML = userInput}).catch(err => {console.log(err)})}
如此修改,更新功能就算是大功告成了。最後列出與更新相關的程式碼:
app.js部分
app.post('/update-item', function (req, res) {var id = req.body.idvar dbRef = db.ref('todos/' + id)dbRef.update({item: req.body.text})res.send("Updated Success")})
browser.js部分
document.addEventListener('click', function (e) {//update featureif (e.target.classList.contains("edit-me")) {let originalText = e.target.parentElement.parentElement.querySelector(".item-text");//文字輸入框 + 顯示原來的文字let userInput = prompt("請修改待辦事項", originalText.innerHTML)//Key data-idlet _id = e.target.getAttribute("data-id");if (userInput) {axios.post('/update-item', {text: userInput,id: _id}).then(function (result) {originalText.innerHTML = userInput}).catch(err => {console.log(err)})}};});
D刪除資料
刪除跟更新的邏輯差不多,在刪除資料的時候我們需要明確的知道到底要刪除哪一筆。因此一樣要在刪除的該筆資料上面加上一個id綁定。
data-id="<%- list %>"
跟編輯一樣,我們把上面的data-id放在「刪除」按鈕上面。
<button data-id="<%- list %>" class="delete-me btn btn-danger btn-sm">Delete</button>
設定監聽事件,監聽Delete按鈕
接下來就是要監聽使用者的點擊事件。當使用者點擊下面HTML裡面的「Delete 刪除」按鈕,就會觸發事件。
我們先前已經在刪除鈕上面加了一個delete-me的CSS Class。這時候可以利用這個Class來跟事件勾在一起。因此,我們可以試著寫在browser.js裡面一個監聽事件:
document.addEventListener('click', function (e) {if (e.target.classList.contains("delete-me")) {//Key data-idlet _id = e.target.getAttribute("data-id");console.log('going to delete:' + _id)};});
當程式間聽到使用者按下「Delete 刪除」按鈕時,就會在console裡面顯示這筆資料的識別碼(_id),這個id是先前所提到的data-id。
刪除firebase資料
使用remove()方法可以把被指定的firebase路徑下資料全部刪除。remove方法有兩個參數,一個是該筆資料的id,另一個是個回呼函數。當資料被刪除後,可以執行的動作。如果你還想要保留資料裡面的id,而只想把id裡面的值刪除的話,可以改用set()方法,把資料取代為null。
remove(data, callback)
接著,仿照更新的方式,我們使用axios來刪除資料,並且回傳刪除後的資料結果。
axios.post('/delete-item', {id: _id}).then(function () {e.target.parentElement.parentElement.remove()}).catch(err => {console.log(err)});
增加delete-item路由
我們一樣需要一個路由:delete-item。請在app.js加上一個路由。這個路由大致上與更新差不多。唯一的差別在於找到資料路徑後,直接使用dbRef.remove() 刪除資料。
app.post('/delete-item', function (req, res) {var id = req.body.idvar dbRef = db.ref('todos/' + id)dbRef.remove();res.send("Delete Success")});
如此就可以刪除資料了。
完成前美化
我們一樣可以加上一些控制條件,讓整個刪除的流程更加順暢。
例如:在刪除前,添加一個判斷式,讓使用者確認該筆資料是不是確實想要刪除的資料。
if (confirm("確定要刪除這筆資料嗎 ? [" + originalText.innerHTML + "]")){
// do something}
結果:
if (e.target.classList.contains("delete-me")) { let originalText = e.target.parentElement.parentElement.querySelector(".item-text"); //Key data-id let _id = e.target.getAttribute("data-id"); console.log('going to delete:' + _id) if (confirm("確定要刪除這筆資料嗎 ? [" + originalText.innerHTML + "]")) { //delete data axios.post('/delete-item', { id: _id }).then(function () { e.target.parentElement.parentElement.remove() }).catch(err => { console.log(err) }); };};
我們還可以把整個刪除的功能與更新的功能放在同一個監聽下:
document.addEventListener('click', function (e) {//update featureif (e.target.classList.contains("edit-me")){...}//delete featureif (e.target.classList.contains("delete-me")) {...}});
結語
以上就是簡單的CRUD待辦清單。你可以繼續思考看看,還有什麼別的方式可以使用?還有什麼地方可以優化?