# 待辦清單
<!-- - 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->點選對應網頁即可看到

中文版格式。按F12->上面選擇應用程式->左邊找到本機儲存空間->點選對應網頁即可看到

* 儲存資料到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;
} */
```