Node.js+Express — 製作CRUD簡易待辦清單(下)

Sean Yeh
Web Design Zone
Published in
17 min readApr 28, 2020

--

到目前為止,我們已經製作簡易版的TODO 待辦清單應用程式建立資料(C)與讀取資料庫資料(R)的部分,接下來我們要繼續進行修改資料(U)與刪除資料(D)的說明。

U修改資料

還記得我們之前寫的使用情境嗎?

情境說明

讀取:進入頁面時,應用程式會自行從資料庫中取出所有資料,並顯示在列表中。

新增:頁面中有一個input輸入框,讓使用者輸入待辦清單相關的文字,一旦輸入完畢按下輸入框旁邊的『新增項目』按鈕,就可以將文字輸入到資料庫中,並且同時顯示在下面的列表中。

修改:每個列表資料旁邊都有Edit(編輯)與Delete(刪除)按鈕。按下Edit(編輯)後,可以修改文字,並且存回資料庫。回存資料庫的同時,頁面不需重新整理,直接顯示修改後的文字。

刪除:按下Delete(刪除)後,該筆資料直接消失於畫面,並且相對應的資料也從資料庫中刪除。

其中:

修改:…按下Edit(編輯)後,可以修改文字,並且存回資料庫。回存資料庫的同時,頁面不需重新整理,直接顯示修改後的文字。

修改資料可分為下面幾個步驟:

  1. 監聽按鈕。
  2. 取得使用者修改的文字
  3. 將該文字加進資料庫中
  4. 網頁顯示更新後的內容

這裏,我們需要靜態檔案的協助。首先,在專案的根目錄建立一個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待辦清單。你可以繼續思考看看,還有什麼別的方式可以使用?還有什麼地方可以優化?

相關文章::Node.js+Express — 製作CRUD簡易待辦清單(上)

--

--

Sean Yeh
Web Design Zone

# Taipei, Internet Digital Advertising,透過寫作讓我們回想過去、理解現在並思考未來。並樂於分享,這才是最大贏家。