# Javascript Exercise: DOM Events - Shopping List ###### tags: `Javascript`, `practice` > 此為 [The Complete Web Developer in 2022: Zero to Mastery](https://www.udemy.com/course/the-complete-web-developer-zero-to-mastery/learn/lecture/8679894#notes) 的課程練習筆記。 ## 課後練習:Shopping List 這次的作業題目: :::info > 前情提要: > 這份作業是接續前一個 shopping list 的練習,前一次主要是實作新增 item,包含: > 1. 建立 user input 欄位 > 2. 當 input 不為空時,透過點擊 button 或是按 enter 都能將 user 輸入的內容建立成 item Using the Shopping List files from the previous videos update the shopping list app to do the following: 1. If you click on the list item, it toggles the .done class on and off. 2. Add buttons next to each list item to delete the item when clicked on its corresponding delete button. 3. BONUS: When adding a new list item, it automatically adds the delete button next to it (hint: be sure to check if new items are clickable too!) ::: 作業成果: :::success - [codepen](https://codepen.io/yachuh/pen/poWKBzX) - github: [source code](https://github.com/yachuh/ztm-shopping-list), [demo](https://yachuh.github.io/ztm-shopping-list/) ::: --- ## 前言 這是一個不小心被 pending 有點久的課後練習,從年前上完 DOM Events 這堂課之後,因為有點卡關加上時間被年底的行程切割得很零碎(掩面),導致每次打開作業大多數的時間都是在複習我前面到底寫了什麼 XD,進度一直裹足不前。 這兩天總算是靜下心來好好回顧將近兩個禮拜前寫到一半的 code ,並且放慢腳步思考如何一步步做出來。 說到「放慢腳步」是因為我發現自己有一些分心的壞習慣: - 邏輯會跳來跳去,沒辦法專注解決眼前的問題:例如寫某個 function 卡住就跳去寫另一個 function,除了整個程式碼看起來很亂之外,要回頭再 catchup 也會很困難,不知道自己到底寫了三小。或是想到這個地方還可以多加些什麼,又跑去查其他資料。功能還沒寫好就一直去在乎頁面的呈現:例如在製作 shopping list 的 delete button 的時候,我的寫法是把 `<button>` append 在 `<li>` 下,但過程中一直覺得 button 樣式太醜而在 CSS 上鑽牛角尖;或是在製作 user input 的時候想讓輸入框跟按鈕的整體排列更好看,一不小心又花太多時間在 CSS 的 display 上(回頭又把 flex 跟 grid 複習了一下)。 - 查解法的時候查到其他不同的寫法或是擴充資料:一度因為查資料產生是不是要改用 `<form>` 來處理 input ,或是乾脆用 table 來包 ul 的項目之類的 overthinking;以及寫到一半覺得 code 改來改去怕忘記改了哪&想要有個儲存點,所以想先 publish 到 github 上但又遇到不知道為何 404 的問題⋯⋯。 諸如此類的分心都導致這次的練習拖得很冗長,很多時候已經不是在解決這個練習想要解決的問題了。 這樣的後果是除了完成的時間比預期久之外,也會因為遲遲無法獲得回饋產生逃避的心態 XD(中間就一度先放棄完成練習,先繼續往下上課)。 也因為有這樣的經驗,所以決定往下應該 :::success 先專注在把功能實作出來,再去 refactor 讓 code 更簡潔、再去調整 CSS 讓畫面更好看」會比較好。 ::: 至於過程中遇到的問題、嘗試的解法,以及做完但還是沒弄得很懂的部分,就乾脆同時記錄在這篇文章裡,之後閒暇時間或是未來有遇到重複問題的時候,可以再撥時間鑽研這些還不熟的部分。 --- ## 實作筆記 以下是每個步驟的一些簡單紀錄,也一邊整理出可能還不是很清楚或還可以再鑽研的地方,方便日後回顧: ### 1. 加上 delete button 1. 為既有 items 加上:用 for loop 為既有的 list item 都加上 delete button。 ```javascript= // Add delete button to existing items for (var i = 0 ; i < listLength; i++){ var delBtn = document.createElement("button"); var t = document.createTextNode("Delete"); delBtn.appendChild(t); delBtn.className = "btn-del"; list[i] = list[i].appendChild(delBtn); } ``` 2. 在原本新增 item 的 function 中加上:copy/paste 上面加上 delete button 的 code 到原本的 function 中 ```javascript= // Function: Add new item with delete button function createListElement() { var li = document.createElement("li"); //create a <li> node li.appendChild(document.createTextNode(input.value)); // add input value to <li> var delBtn = document.createElement("button") // create a <button> node var t = document.createTextNode("Delete"); // create "Delete" text delBtn.appendChild(t); // append text to button delBtn.className = "btn-del"; // add class to button li.appendChild(delBtn);// append delete button to <li> ul.appendChild(li); //append <li> to <ul> input.value = ""; } ``` 由於「加上 delete button」的一段 code 是重複的,當下寫完的時候就想說可以用一個 function 來取代重複的部分。但一個沒搞好導致我自以為是的 refactor 後反而跑不動了,所以又花了比預期還久的時間在處理這塊。後來想想應該要全部功能做完再來做 code refactoring 才是。 ### 2. 點擊清單項目 (li) 來 toggle 該項目 class 的 on and off 1. 加上 click 的事件,並且要明確知道是點擊哪個 `<li>` 2. toggle 該 `<li>` 的 `.done` class on and off ```javascript= // Run functions - click to crossover item // ul.onclick = function(event){ // var target = event.target; // if(target.nodeName === "LI"){ // target.classList.toggle("done"); // } // } ul.addEventListener("click", function(event){ var target = event.target; if(target.nodeName === "LI"){ target.classList.toggle("done"); } }) ``` - `onclick` :一開始用 `.addEventListener("click", function)` 卡了滿久,因為一直不知道要怎麼針對使用者點擊的那個 element。後來參考其他同學的寫法是用 `.onclick` ,套用之後可以 work。但也發現似乎是過時、不建議的做法,最後在[遇到 onclick 兩次失效的問題](https://hackmd.io/WA9-hHmTQuOmLNvO0h9xFA#4-%E8%A7%A3%E6%B1%BA%EF%BC%88%E4%BD%86%E4%B9%9F%E6%B2%92%E8%A7%A3%E6%B1%BA%EF%BC%89-onclick-%E6%B2%92%E8%BE%A6%E6%B3%95%E5%90%8C%E6%99%82%E4%BD%BF%E7%94%A8%E7%9A%84%E5%95%8F%E9%A1%8C)之後還是改寫回 `.addEventListener` 的做法 - `event.target` :如上,對於不知道要怎麼指出使用者點到哪個 element 卡很久,所幸後來查到這個方式實在是太方便了,不過對於 event 還可以接什麼還不是很清楚就是了 - target`.nodeName`:指出 target 的 node name,像是 P 、LI、BUTTON…等。一開始用 console.log 確認到底有哪些 nodeName 才用,也發現**格式要是大寫** ### 3. 點擊該項目的 delete button 可以移除該清單項目 1. 點擊 delete button:會 `event.target` 之後就輕鬆多了。這邊一樣先 cache 點擊的標的 `var target = event.target` ,以及加上標的是否為 delete button 的 if 判斷式:nodeName 為 BUTTON 並且 className 為 btn-del 2. 點擊後希望能移除整條項目,因此得將 (delete button) 所屬的 `<li>` (parent element) 移除:一開始在找移除相關的語法是參考[ HTML DOM removeChild Method (w3schools.com) ](https://www.w3schools.com/jsref/met_node_removechild.asp) 關於 `remove()` method 的介紹,但介紹中只有提到 removeChild ,而我在步驟 1 選取的 element 是 delete button ,並且想要移除的是整個它的 parent element (`<li>`)。 因此後來再查到這篇  [How to remove the parent element using plain Javascript - Stack Overflow](https://stackoverflow.com/questions/2727717/how-to-remove-the-parent-element-using-plain-javascript) 說明用 `.parentElement` 選到 parent 後再 `.remove()` 就可以了。 ```javascript= ul.onclick = function(event){ var target = event.target; if (target.nodeName === "BUTTON" && target.className === "btn-del") { target.parentElement.remove(); } } ``` 加上這段程式碼之後,測試移除的功能沒問題,但此時又遇到另一個問題: > 前一個 onclick event 竟然失效了(抱頭) ### 4. 解決(但也沒解決) onclick 沒辦法同時使用的問題 在查這個問題的時候,找到[這篇](https://)是說 onclick 是可以帶兩個 function 的(但他同時也不建議用 `.onclick` ,所以才會有下面又改用 `.addEventListener` 的做法): ```javascript= onclick="doSomething();doSomethingElse();" ``` 這邊因為要塞 2 個 function ,所以我把原本分開兩個 onclick 各自帶的 function 內容,都另外拉出來先分別建立好兩個 function。但不確定是哪裡沒寫好,在把建立第一個 function 並且用 onclick 只帶一個 function 的時候就無效了(頭好痛): ```javascript= //Function declaration: toggle .done when LI is clicked function itemDone (){ var target = event.target; if(target.nodeName === "LI"){ target.classList.toggle("done"); } // onclick function ul.onclick = itemDone ``` 此時山不轉路轉,路不轉人轉,前面看到作者建議不要用 `.onclick` ,所以靈機一動想說不然改試試回歸到原本課堂上教的 `.addEventListner` 好了,於是改寫了一下發現馬欸通!而且還沒遇到上述 onclick 兩次前者失效的問題,就這麼意外地寫好了! ```javascript= // Run functions - click to crossover item // ul.onclick = function(event){ // var target = event.target; // if(target.nodeName === "LI"){ // target.classList.toggle("done"); // } // } ul.addEventListener("click", function(event){ var target = event.target; if(target.nodeName === "LI"){ target.classList.toggle("done"); } }) //Run functions - click Delete button to delete item // ul.onclick = function(event){ // var target = event.target; // if (target.nodeName === "BUTTON" && target.className === "btn-del") { // target.parentElement.remove(); // } // } ul.addEventListener("click", function(event){ var target = event.target; if (target.nodeName === "BUTTON" && target.className === "btn-del") { target.parentElement.remove(); } }) ``` ### 5. 自己加了清除所有 items 的 “Clear All” button 最後的最後,想說都有各 item 的 delete 了,那麼一鍵清除全部應該也不困難吧!於是很「假會」地又加了最後這個功能。 ```javascript= //Run function: clear all items var clearBtn = document.getElementById("clear") clearBtn.addEventListener("click", function(){ var list = document.querySelectorAll("li"); var listLength = list.length; for (var i = 0 ; i < listLength; i++){ list[i] = list[i].remove(); } }) ``` 這邊值得紀錄的是,一開始我沒有再次定義這兩個變數: ```javascript= var list = document.querySelectorAll("li"); var listLength = list.length; ``` 因為想說開頭就有了,應該沒必要重複,結果: :::info 操作步驟: 1. 新增一個 item 2. 按下 Clear All ::: :::success 預期結果:清除所有 item(s)。 ::: :::danger 實際結果:只刪除既有的、不會刪除新增的 item(s)。 ::: 我自己猜想是因為步驟 1 先用了 add items 的 function,所以此時變數 `list` 跟 `listLength` 是 cache 到當下的。因此如果沒有重新再定義一次,那麼步驟 2 執行的 clear all function 就不會再抓到新增後的值。 這邊也衍生出下面「還沒弄清楚的」部分的第 5 點,對於哪些 variable 應該要定義在 root scope 的、哪些是適合定義在 function 內的(或許這次的 `list` 跟 `listLength` 就比較適合後者),還不是很清楚。 --- ## 還沒弄清楚的 1. 哪些東西應該要用 `<div>` 包起來比較合適?(或也不一定是用 div 包,可能有其他方式?) 主要是這次寫到一半有參考到一位同學的 solution 是將每一個 `<li>` 都用 `<div>` 包起來然後 assign class 。後續 toggle 跟 crossover 就直接在 div 上做變化,似乎也是不錯的寫法。在某一次重寫中是有嘗試想乾脆砍掉重練整個架構 (create new item 要再多一層 div),但寫到一半覺得太麻煩了就放棄 XD 1. 對於想要呈現的排版以及如何用 flex 跟 grid 做到還不熟,這次是直接看 cheatsheet 複製貼上試看看效果才知道 1. 這次有用到 `event.target` ,但對於 event 還有哪些 method 還不是很熟 1. 有待搞懂 `.onclick` 跟 `.addEvenListener` 的差別與使用時機 1. 命名 variables 的時候其實很怕踩到 JS 專有名詞或是已經使用過的命名(是否只命名在 function 內),這部分應該是靠多練習就可以有比較好的命名系統(說不定也有什麼建議的命名 guidline 之類的,有空可以再查) 1. Scope: 哪些 variable 適合放在 function 內,哪些適合放在 root? ## 還可以再優化的 關於這個 shopping list 邊想到可以再做的優化(PM 魂不小心跑出來),就當作是額外的 bonus 挑戰,先放到 backlog 有空再做 XD: 1. 將 item 改成 checkbox (input type = checkbox),透過 check & uncheck 來 toggle `.done` 2. 點擊 delete button 之後、刪除 item 之前,中間多一步跳確認視窗,讓使用者確認是否要刪除。 3. 全部的 item 都 done 之後,恭喜 user 全部完成&詢問是否清除所有 item。 4. 如果 item 為空,可以有空狀態的提示文字在下方,或是在 input 框加上 tooltip 提醒使用者輸入內容。