# 待辦清單 <!-- - LocalStorage - JSON - init(): Listener (直接在畫面上寫onclick) - renderList() - Template - getData()/setData() - addNewListItem() - changeItemState()/changeListState() - Reset Btn (reset資料,取代setting btn的位置) - 對資料進行操作 - 新增待辦事項: `addNewListItem()` - 完成了一件待辦事項:`changeItemState()` - 取得資料並顯示到畫面上 - 顯示待辦事項:`renderList()` - --> ## JSON * JavaScript Object Notation,是一種資料交換格式 * javascript中的任一資料型態(幾乎)都算在JSON格式中 * 透過JavaScript物件的方式,紀錄各種資料 * 透過`JSON.parse(字串)`將字串轉為JSON檔案,或透過`JSON.stringify(JSON檔案)`將JSON檔案轉為字串 ## Local Storage * localStorage用於瀏覽器用久儲存資料的地方,資料不會因為頁面刪掉或是關機而消失 * 每個網頁的local storage是互相隔離的 * local storage只能儲存string型態的資料 * 每一筆存在localStorage的資料(value)都跟一個鍵(key)來對應 * 檢查localStorage內的資料 英文版。按F12->上面選擇Application->左邊找到local storage->點選對應網頁即可看到 ![](https://hackmd.io/_uploads/ByYK_lZwn.png) 中文版格式。按F12->上面選擇應用程式->左邊找到本機儲存空間->點選對應網頁即可看到 ![](https://hackmd.io/_uploads/r138JWbvh.png) * 儲存資料到localStorage * localStorage.setItem(key, value) * value為要儲存的資料 * key和value都是字串(string) * 取得資料到localStorage * value = localStorage(key) * 透過key來得到value * key和value都是字串(string) * 刪除localStorage的資料 * localStorage.removeItem(key) * 透過key來刪除value * key為字串(string) ## 取得資料:`getData(), setData()` ### setData() :::info 將陣列(由物件組成)作為函式的輸入,將JSON轉換為字串後存入Local Storage,主動更新頁面,將新的待辦事項呈現於頁面上。 ::: ```javascript! function setData(data) { localStorage.setItem(KEY_DATA, JSON.stringify(data)); renderList();//設定完成後,用來渲染(更新)畫面 } ``` * 這裡利用剛剛教的localStorage概念,`localStorage.setItem(KEY_DATa,JSON.stringify(data)`分別對應到localStorage的儲存格式(key和value),而因為localStorage裡面儲存格式是字串,因此利用`JSON.stringify`將JSON轉換為字串。`renderList()`則是用來選染更新好的畫面,之後會提到。 ### getData() :::info 從瀏覽器的Local Storage中取得字串,將字串轉為JSON格式後回傳。 ::: ```javascript function getData() { return JSON.parse(localStorage.getItem(KEY_DATA)) ?? []; } ``` * `localStorage.getItem(KEY_DATA)`我們這裡取得localStorage儲存的資料(裡面的格式是字串),而之後要將字串轉JSON,因此用`JSON.parse()` 這個語法將字串轉為JSON,最後`??[]`是如果取得的東西是NULL,就回傳空陣列。 ## 新增待辦事項: `addNewListItem()` :::info 取得使用者欲增加的事項,並在將其轉換為規定格式後,存入Local Storage。 ::: ```javascript! function addNewListItem() // TODO: 取得input // 透過id(或是其他方式)取得輸入框 let contentInput=?? // 取得輸入框裡面的文字內容,也就是新的待辦事項 let content = ?? // TODO: 清空輸入框(使用者體驗) // TODO: 取得資料並加入新的待辦事項 // 透過getData()拿到待辦事項 let data = ?? // 把新的待辦事項加入Data data.push({ data: content, isFinished: false, }); // 透過setData()儲存新的資料 setData(data); contentInput.value = ''; } ``` :::spoiler ```javascript! function addNewListItem() // TODO: 取得input // 透過id(或是其他方式)取得輸入框 let contentInput = document.getElementById("itemContent"); // 取得輸入框裡面的文字內容,也就是新的待辦事項 let content = contentInput.value; // TODO: 清空輸入框(使用者體驗) // TODO: 取得資料並加入新的待辦事項 // 透過getData()拿到待辦事項 let data = getData(); // 把新的待辦事項加入Data data.push({ data: content, isFinished: false, }); // 透過setData()儲存新的資料 setData(data); contentInput.value = ''; ``` ::: ## 清空所有待辦事項:`resetData()` :::info 透過將空陣列->空字串傳入Local Storage,以達成清空Local Storage所儲存的待辦事項,主動更新頁面。 ::: 這個很重要,會決定我們怎麼處理事件--我們決定新建一個函式,這樣子我們就可以每一個事件觸發都寫onclick了 ```javascript! function resetDate() { // 用setData()函式將localStorage中儲存todo list的部分變成空陣列 renderList(); // 將local storage的值更新到to do list } ``` ## 將資料渲染到畫面上:`renderList()` :::info 主動取得Local Storage中的資料,依據資料對畫面(TodoList)進行更新。 ::: 到目前為止,我們renderList需要做的事情包含: * 先使用`getData()`取得資料... * 取得我們的`to-do-list`,準備把資料加入畫面中 * 清空`to-do-list`裡面的內容 * 透過迴圈將每一筆資料拿出來,並顯示到`to-do-list` * 如果待辦事項已經被完成了,這個迴圈就跳過 * 複製我們藏在網頁當中的template * 複製template底下的`list-item` * 取得`list-item`的文字,放入內容 * 將這個`list-item`加入`to-do-list` 我們已經準備了已經寫好的template給大家參考: ```xml= <!-- d-none 表示display:none,不顯示在畫面上 --> <!-- 我們可以先複製template,再取得template底下的list-item --> <div class="list-item-template d-none"> <div class="list-item d-flex justify-content-center"> <button class="fa btn btn-none p-0 m-0" type="button"> </button> <p class="list-item-content p-0 m-0 ms-2">Lorem ipsum dolor sit amet.</p> </div> </div> ``` ```javascript! function renderList() { // TODO: 取得資料 let data = ... // TODO: 取得to-do-list標籤 let list = ... // TODO: 清空to-do-list裡面的HTML ... // TODO: 使用for迴圈逐一取出每一筆待辦事項,並且待辦事項`to-do-list-item`新增到to-do-list for (let i = 0; i < data.length; i++) { // TODO: 如果這個待辦已經完成了,就要用continue跳過這個迴圈 if(data[i].isFinished) continue; // TODO: 使用cloneNode複製template let template = document.querySelector(".list-item-template div").cloneNode(true); // TODO: 取得tempate底下的Button let itemBtn = template.getElementsByClassName('list-item-button')[0] // TODO: 取得tempate底下的Content(放文字內容的區塊) let itemContent = template.getElementsByClassName('list-item-content')[0]; // 將待辦事項的文字放到Content裡面 itemContent.textContent = data[i].data; // 把新的節點放到to-do-list上 list.appendChild(template); } } ``` :::spoiler ```javascript! function renderList() { // TODO: 取得資料 let data = getData(); // TODO: 取得to-do-list let list = document.querySelector(".to-do-list"); // TODO: 清空to-do-list裡面的HTML list.innerHTML = ''; // TODO: 使用for迴圈逐一取出每一筆待辦事項,並且待辦事項`to-do-list-item`新增到to-do-list for (let i = 0; i < data.length; i++) { // TODO: 如果這個待辦已經完成了,就要用continue跳過這個迴圈 if(data[i].isFinished) continue; // TODO: 使用cloneNode複製template let template = document.querySelector(".list-item-template div").cloneNode(true); // TODO: 取得tempate底下的Button let itemBtn = template.getElementsByClassName('list-item-button')[0] // TODO: 取得tempate底下的Content(放文字內容的區塊) let itemContent = template.getElementsByClassName('list-item-content')[0]; // 將待辦事項的文字放到Content裡面 itemContent.textContent = data[i].data; // 把新的節點放到to-do-list上 list.appendChild(template); } } ``` ::: ## 完成了一件待辦事項:`changeItemState()` 當我們完成一件待辦事項的時候,會點擊按鈕並觸發函式,並且想要把這筆資料的欄位`isFinished`變成`true`。 讓我們思考一個問題:假設我們在這個`to-do-list-item`加上一個`changeItemState()`的function,並且在`onclick`的時候觸發,假設函式這樣子寫: ```javascript! function changeItemState(){ // 準備對某一筆資料進行修改...... } ``` 寫到這裡你會發現,`欸糟糕,我不知道要怎麼寫下去`。因為你觸發這個函式的時候,並不知道究竟是哪一個`to-do-list-item`被點擊了。 要是在點擊這個按鈕的時候,傳入這筆資料在陣列當中的index,那就可以透過這個index找到資料進行修改! ```javascript function changeItemState(i) { // 透過getDate()取得資料 data[i].isFinished = !data[i].isFinished; // 儲存新的資料 setData(data); } ``` 我們在寫html的`onclick`屬性的時候,肯定是不能觸發一個有參數的function,那我們就要用這一種方法來解決問題: ```javascript= function renderList() { let data = getData(); let list = document.querySelector(".to-do-list"); list.innerHTML = ''; ok for (let i = 0; i < data.length; i++) { if (data[i].isFinished) continue; let template = document.querySelector(".list-item-template div").cloneNode(true); let itemBtn = template.getElementsByClassName('list-item-button')[0] let itemContent = template.getElementsByClassName('list-item-content')[0]; if (data[i].isFinished) { itemBtn.classList.add("fa-check"); } else { itemBtn.classList.add("fa-circle-thin"); } itemBtn.addEventListener('click', changeItemState.bind(null, i)); itemContent.textContent = data[i].data; list.appendChild(template); } } ``` 在11行的地方使用了一個`addEventListener方法`,前面的參數是觸發事件的方法`click`(相當於`onclick`),後面的參數是要觸發的事件。 而`changeItemState.bind(null, i)`中,第一個參數解釋起來相當的複雜(解釋在底下),第二個參數就是把這個函式跟這筆資料的index做綁定,當成參數傳入,觸發函式的時候就能取得這個`to-do-list-item`這筆資料在陣列中的index! :::spoiler 精簡的解釋在這裡 第一個參數解釋起來相當的複雜,你暫時知道傳進去的物件,可以在函式當中使用this來取得就可以了。 ::: ## 顯示已經完成的待辦事項:`changeListState()` 先完成原來版本的renderList,好了之後再增加這個功能,修改renderList的邏輯 :::info 這裡是要改itemButton的狀態,如果完成了,將button改為打勾(已完成),還沒完成改為圓圈(未完成) ::: ```javascript! function changeListState() { // 先取得要顯示Button的空間 let showItemBtn = document.getElementById("showItem"); // 點擊按紐狀態相反,因此要把判斷有無完成的情況進行顛倒 showFinished = !showFinished; // Todo:更改是否完成的狀態 // 如果完成了,增加顯示勾勾的class,移除顯示圓圈的class // 如果未完成,增加顯示圓圈的class,移除顯示勾勾的class if (showFinished) { showItemBtn.classList.remove("fa-circle-thin"); showItemBtn.classList.add("fa-check");// ok } else { showItemBtn.classList.remove("fa-check");// ok showItemBtn.classList.add("fa-circle-thin"); } // 最後更新到畫面上 // renderList(); } ``` ## 修改renderList的for迴圈條件 * showFinished是一個boolean值,用來判斷事項是否被完成,剛開始預設未完成,因此預設false ```javascript= let showFinished = false; ``` 我們剛剛完成的renderList()過濾資料的條件是,如果`data[i].isFinished==True`,那for迴圈就要continue,跳過不顯示這筆資料 現在則是要另外考慮showFinished這個變數加進來的時候,什麼時候資料需要被過濾掉 | | `data[i].isFinished == true` | `data[i].isFinished==false` | | ------------------- | -------------------------- | ------------------ | | `showFinished==true` | 顯示 | 顯示 | | `showFinished==false` | 不顯示 | 顯示 | 按照上面的真值表我們得到一個很重要的結論:當`showFinished==false`(不顯示待辦事項)以及`data[i].isFinished == true`(這筆待辦事項已經被完成)同時成立的時候,就可以跳過這個迴圈! 所以我們的結論是: ```javascript for(int i = 0;i < data.length;i++){ // 兩個條件同時成立的時候跳過 if(!showFinished && data[i].isFinished) continue; // 其他情況下,都要把這筆資料顯示到畫面上 } ``` 底下是完整的程式碼: ```javascript= function renderList() { let data = getData(); let list = document.querySelector(".to-do-list"); list.innerHTML = ''; ok for (let i = 0; i < data.length; i++) { if (!showFinished && data[i].isFinished) continue; let template = document.querySelector(".list-item-template div").cloneNode(true); let itemBtn = template.getElementsByClassName('list-item-button')[0] let itemContent = template.getElementsByClassName('list-item-content')[0]; itemBtn.addEventListener('click', changeItemState.bind(null, i)); itemContent.textContent = data[i].data; list.appendChild(template); } } ``` ## 記得把所有按鈕要觸發的函式加上去! 我們寫完每一個按鈕會觸發的事件之後,我們需要在html裡面,按鈕的部分,新增onclick的屬性,寫法如下: ``` ``` 至此,我們就大概完成了To-do-list的功能了! (~~可以休息了~~,嗚並沒有) ## 最終版本 ```xml= <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> <link rel="stylesheet" href="/main.css"> </head> <body> <!-- banner --> <!-- medium --> <!-- container --> <div class="container text-center d-flex flex-column justify-content-center"> <div class="clock"> <!-- <p>This is the current time</p> --> <p id="time"></p> </div> <div class="name"><p>Your name</p></div> <div class="list-item-template d-none"> <div class="list-item d-flex justify-content-center "> <button class="list-item-button fa btn btn-none p-0 m-0" type="button"></button> <p class="list-item-content p-0 m-0 ms-2">Lorem ipsum dolor sit amet.</p> </div> </div> <!-- to-do-list-block --> <div class="to-do-list-block"> <!-- to-do-list --> <div class="to-do-list"> <div class="list-item d-flex justify-content-center"> <button class="fa fa-plus btn btn-none p-0 m-0"></button> <span>示範文字</span> <!-- <p>示範文字</p> --> </div> <div class="list-item d-flex justify-content-center"> <button class="fa fa-circle-thin btn btn-none p-0 m-0"></button> <p class="p-0 m-0">示範文字</p> </div> <div class="list-item d-flex justify-content-center"> <button class="button1 fa fa-plus btn btn-none p-0 m-0"></button> <p class="p-0 m-0">示範文字</p> </div> </div> <!-- to-do-list按鈕 --> <div class="to-do-list-button d-flex justify-content-center align-items-center"> <!-- Button trigger modal --> <button type="button" class="fa fa-plus btn btn-none p-0 m-1" data-bs-toggle="modal" data-bs-target="#exampleModal"></button> <button class="fa fa-circle-thin btn btn-none p-0 m-1 " onclick="changeListState()" aria-hidden="true" id="showItem"></button> <button class="fa fa-times btn btn-none p-0 m-1 " onclick="resetData()" aria-hidden="true"></button> </div> </div> <!-- Modal --> <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h1 class="modal-title fs-5 text-dark" id="exampleModalLabel">新增待辦事項</h1> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div class="input-group mb-3"> <span class="input-group-text" id="basic-addon1">項目內容</span> <input type="text" class="form-control" placeholder="" id="itemContent" aria-label="Username" aria-describedby="basic-addon1"> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> <button type="button" class="btn btn-primary" id="addNewListItem" onclick="addNewListItem()" data-bs-dismiss="modal">保存</button> </div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="/clock.js"></script> <script> KEY_DATA = 'toDoList'; let showFinished = false; function setData(data) { localStorage.setItem(KEY_DATA, JSON.stringify(data)); renderList();//設定完成後,用來渲染(更新)畫面 } function getData() { return JSON.parse(localStorage.getItem(KEY_DATA)) ?? []; } let obj = { data:"寫javascript", isFinished:false } function addNewListItem() { let contentInput = document.getElementById("itemContent"); let content = contentInput.value; let data = getData(); data.push({ data: content, isFinished: false, }); setData(data); contentInput.value = ''; renderList(); } function resetData() { // 用setData()函式將localStorage中儲存todo list的部分變成空陣列 setData([]); renderList(); // 將local storage的值更新到to do list } function changeItemState(i) { // 透過getDate()取得資料 let data = getData(); data[i].isFinished = !data[i].isFinished; // 儲存新的資料 setData(data); } function changeListState() { // 先取得要顯示Button的空間 let showItemBtn = document.getElementById("showItem"); // 點擊按紐狀態相反,因此要把判斷有無完成的情況進行顛倒 showFinished = !showFinished; // Todo:更改是否完成的狀態 // 如果完成了,增加顯示勾勾的class,移除顯示圓圈的class // 如果未完成,增加顯示圓圈的class,移除顯示勾勾的class if (showFinished) { showItemBtn.classList.remove("fa-circle-thin"); showItemBtn.classList.add("fa-check");// ok } else { showItemBtn.classList.remove("fa-check");// ok showItemBtn.classList.add("fa-circle-thin"); } // 最後更新到畫面上 // renderList(); } function renderList() { // TODO: 取得資料 let data = getData(); // TODO: 取得to-do-list標籤 let list = document.getElementsByClassName('to-do-list')[0]; // TODO: 清空to-do-list裡面的HTML list.innerHTML = ''; // TODO: 使用for迴圈逐一取出每一筆待辦事項,並且待辦事項`to-do-list-item`新增到to-do-list for (let i = 0; i < data.length; i++) { // TODO: 如果這個待辦已經完成了,就要用continue跳過這個迴圈 if(!showFinished && data[i].isFinished) continue; // TODO: 使用cloneNode複製template let template = document.querySelector(".list-item-template div").cloneNode(true); // TODO: 取得tempate底下的Button let itemBtn = template.getElementsByClassName('list-item-button')[0]; if (data[i].isFinished) { itemBtn.classList.add("fa-check"); } else { itemBtn.classList.add("fa-circle-thin"); } // TODO: 取得tempate底下的Content(放文字內容的區塊) let itemContent = template.getElementsByClassName('list-item-content')[0]; // 將待辦事項的文字放到Content裡面 itemContent.textContent = data[i].data; itemBtn.addEventListener('click', changeItemState.bind(null, i)); // 把新的節點放到to-do-list上 list.appendChild(template); } } renderList(); </script> </body> </html> ``` ```css= * { padding: 0; margin: 0; } html, body { width: 100%; height: 100%; } body{ background: url("https://picsum.photos/id/16/1920/1080") center / cover no-repeat fixed; } .container { height: 100%; /* color: white; */ /* position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; */ } /* button { min-width: 20px; height: 20px; } */ ```