iT邦幫忙

2023 iThome 鐵人賽

DAY 18
0
Modern Web

給前端新手的圖文故事系列 第 18

了解 JSON 以及應用 LocalStorage

  • 分享至 

  • xImage
  •  

JSON 概念

資料的儲存以及傳輸,一直以來都是網路技術所發展的一個課題,如何方便且高效率的傳輸,並在其中涵蓋安全與可操作性等,各種需求使得我們應接不暇,而在過去的時間裡,XML 也是其中的一名優秀的佼佼者,但很可惜的是他過於複雜的資料儲存方式,使得使用者還是傾向了更加簡單的 JSON,並且因為 JSON 嚴格來說是從 JS 所衍生過來的,因此優秀的相容性也使得他成為了網站開發者的最愛。

XML - https://www.w3schools.com/xml/
JSON - https://www.json.org/json-en.html

JSON(JavaScript Object Notation) 是什麼?

JavaScript Object Notation(JSON)是一種資料交換格式。JSON 的語法非常接近 JavaScript,但嚴格上來說 JSON 並不是 JavaScript 的一個子集(因為說法不一所以存在模糊地帶,但嚴格來說只能算是資料儲存格式)。許多程式語言都支援 JSON,不過 JSON 在基於 JavaScript 的應用程式(包含網站和瀏覽器擴充功能)中特別方便使用。

JSON 可以表示數字、布林值、字串、null、陣列(一些有順序的數值),以及由這些數值所組成的物件(字串和數值的對應表)。
JSON的基本資料類型:

  • 數值:十進位數,不能有前導0,可以為負數,可以有小數部分。還可以用e或者E表示指數部分。不能包含非數,如NaN。不區分整數與浮點數。JavaScript用雙精度浮點數表示所有數值。
  • 字串:以雙引號""括起來的零個或多個Unicode碼位。支援反斜槓開始的跳脫字元序列。
  • 布林值:表示為true或者false
  • 陣列:有序的零個或者多個值。每個值可以為任意類型。序列表使用方括號[,]括起來。元素之間用逗號,分割。形如:[value, value]
  • 物件:若干無序的「鍵-值對」(key-value pairs),其中鍵是數值或字串。建議但不強制要求物件中的鍵是獨一無二的。物件以花括號{開始,並以}結束。鍵-值對之間使用逗號分隔。鍵與值之間用冒號:分割。
  • 空值:值寫為null

JSON 的資料範例

{
'type': 'book',
'pages': 220,
'title': 'My Book',
'author': {
	'name': 'Alex',
	'age': '33',
	'email': 'test@foo.com',
	'directions': 'Lorem ipsum dolor sit amet consectetur adipisicing elit.',
    }
}

JSON 的轉換語法

以下語法的操作將會把輸入值轉換為 JSON 的格式

操作方式

JSON.stringify(content);

實際範例

var book = {
  type: 'book',
  pages: 220,
  title: 'My Book',
  author: {
    name: 'Alex',
    age: '33',
    email: 'test@foo.com',
    directions: 'Lorem ipsum dolor sit amet consectetur adipisicing elit.',
  },
};

console.log(JSON.stringify(book));
// 輸出:{"type":"book","pages":220,"title":"My Book","author":{"name":"Alex","age":"33","email":"test@foo.com","directions":"Lorem ipsum dolor sit amet consectetur adipisicing elit."}}

操作方式

JSON.parse(content);

實際範例

var jsonData = {"type":"book","pages":220,"title":"My Book","author":{"name":"Alex","age":"33","email":"test@foo.com","directions":"Lorem ipsum dolor sit amet consectetur adipisicing elit."}}

console.log(JSON.parse(jsonData));
// 輸出:{type: 'book', pages: 220, title: 'My Book', author: {…}}

操作 Window.localStorage

localStorage 允許使用這在同一個網址底下,進行資料的儲存與放置,並且在同一個網址這一前提下,資料將不會在短時間內被移除,需要注意的是,localStorage 使用的是 JSON 的資料結構,因此我們在操作上需要先將資料傳喚成 JSON,才可以完成上傳的操作

儲存資料

localStorage.setItem('myCat', 'Tom');

取得報復資料

let cat = localStorage.getItem('myCat');

刪除個別的資料

localStorage.removeItem('myCat');

清除整個 localStorage

localStorage.clear();

實際範例操作

Todo List 第一版

範例連結

project
│   index.html
│
└───assets
│   │
│   └───javascript
│   │   │   script.js
│   │      
│   └───style
│       │   style.css

HTML 樣板

<!DOCTYPE html>
<html lang="zh">
  <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>
    <!-- 引入 Material Icon 庫 -->
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
    />
    <link rel="stylesheet" href="assets/style/style.css" />
  </head>
  <body>
    <article class="todo-list">
      <form class="todo-list__form">
        <input type="text" placeholder="輸入代辦內容" />
        <button><span class="material-symbols-outlined"> add </span></button>
      </form>
      <ol class="todo-list__tabs">
        <li class="active">全部</li>
        <li>代辦</li>
        <li>已完成</li>
      </ol>
      <ul class="todo-list__items"></ul>
    </article>
    <script src="assets/javascript/script.js"></script>
  </body>
</html>

CSS樣式

* {
  margin: 0;
  padding: 0;
}

body {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  padding: 40px;
  min-height: 100vh;
  background-color: aliceblue;
}

.todo-list {
  width: 400px;
}

.todo-list__form {
  display: flex;
}

.todo-list__form input {
  flex: 1;
  padding: 15px;
  outline: none;
  border: none;
  font-size: 1rem;
}

.todo-list__form button {
  padding: 15px;
  border: none;
  background-color: lightslategray;
  color: white;
}

.todo-list__tabs {
  display: flex;
  margin: 10px 0;
  background-color: white;
  list-style-type: none;
}

.todo-list__tabs > li {
  padding: 15px 20px;
  letter-spacing: 2px;
  cursor: pointer;
  transition: background-color 300ms, color 300ms;
}

.todo-list__tabs > li.active,
.todo-list__tabs > li:hover {
  background-color: lightslategray;
  color: white;
}

.todo-list__items {
  display: flex;
  flex-direction: column;
  list-style-type: none;
}

.todo-list__items li {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  padding: 10px 20px;
  background-color: white;
}

.todo-list__not-found {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 200px;
  background-color: lightslategray;
  color: white;
}

JavaScript 操作

// 去取得 localStorage 上方的資料,若沒有結果的話會回傳結果 null
// 原本使用兩個陣列是指空值合併運算符,但這個操作其實比較新,因此我們可以改用三元運算式進行操作
const todoListData = JSON.parse(localStorage.getItem('todoList'))
  ? JSON.parse(localStorage.getItem('todoList'))
  : [];

// 去取得 DOM 上面 class 名稱是 todo-list 的節點
const todoList = document.querySelector('.todo-list');

// 去取得 todoList 中 class 名稱是 todo-list__form 的節點
const todoListFrom = todoList.querySelector('.todo-list__form');
// 去取得 todoListFrom 中的第一個 button 節點
const todoListFromButton = todoListFrom.querySelector('button');

// 去取得 todoList 中 class 名稱是 todo-list__tabs 的節點
const todoListTabs = todoList.querySelector('.todo-list__tabs');
// 去取得 todoList 中 class 名稱是 todo-list__items 的節點
const todoListItems = todoList.querySelector('.todo-list__items');

// 建立一個協助我們宣染頁面 todo list 的函式
const renderTodoList = () => {
  // 在此處先宣告一個將要被宣染到 html 的變數,沒有跟賦值一起操作是為了讓過程比較好修改與理解
  let todoListUI;

  // 讓要被宣染到 html 的物件,指定成一個 map 出來的新陣列,此處將會回傳一個 html 模板
  todoListUI = todoListData.map(
    (element) =>
      `<li>
          <h2>${element.title}</h2>
          <span>${element.status}</span>
      </li>`,
  );
  // 這裡可以個別將 todoListUI 與 todoListData 列印出來,並觀察其中的差異
  console.log(todoListData); // 陣列資料
  console.log(todoListUI); // 要被宣染到 html 的內容

  // 這裡會將 todoListItems 這個 node 節點所在的 html,置換為下方的其中一種方式
  // 這裡藉由三元運算式的方式,去判斷目前陣列是否有內容(用長度判斷),若沒有內容的話新增一個提示區塊
  todoListItems.innerHTML = todoListUI.length
    ? todoListUI.join('')
    : '<div class="todo-list__not-found">目前沒有內容</div>';
};

// 幫 todoListFromButton 這個節點新增按鈕點擊事件
todoListFromButton.addEventListener('click', function (event) {
  // 因為我們的 button 是放在 form 中,因此點擊時會直接觸發表單送出操作,這裡是將這一預設操作給停止
  event.preventDefault();
  // 新增一筆資料到我們的 todoListData 這個 array 上面
  todoListData.push({ title: 'ssss', status: '代辦' });
  // 將已經新增資料的陣列,轉成 JSON 格式之後上傳到 localStorage 上面,這裡的機碼(儲存位子)使用 todoList
  localStorage.setItem('todoList', JSON.stringify(todoListData));
  // 觸發畫面更新的函式,因為此時他所倚賴的陣列已經更新了內容,因此會在畫面上增加一筆項目
  renderTodoList();
});

// 此處算是初始化的操作,沒有執行的話畫面預設會是空的內容
renderTodoList();

Todo List 第二版

範例連結

project
│   index.html
│
└───assets
│   │
│   └───javascript
│   │   │   script.js
│   │      
│   └───style
│       │   style.css

HTML 樣板

<!DOCTYPE html>
<html lang="zh">
  <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>
    <!-- 引入 Material Icon 庫 -->
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
    />
    <link rel="stylesheet" href="assets/style/style.css" />
  </head>
  <body>
    <article class="todo-list">
      <form id="search-todo-list" class="todo-list__form">
        <input type="text" id="search" placeholder="請輸入搜尋關鍵字" />
        <button><span class="material-symbols-outlined"> search </span></button>
      </form>
      <form id="create-todo-list" class="todo-list__form">
        <input type="text" placeholder="輸入代辦內容" />
        <button><span class="material-symbols-outlined"> add </span></button>
      </form>
      <ol class="todo-list__tabs">
        <li class="active">全部</li>
        <li>代辦</li>
        <li>已完成</li>
      </ol>
      <ul class="todo-list__items"></ul>
      <p class="todo-list__info"></p>
    </article>
    <script src="assets/javascript/script.js"></script>
  </body>
</html>


CSS樣式

* {
  margin: 0;
  padding: 0;
}

body {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  padding: 40px;
  min-height: 100vh;
  background-color: aliceblue;
}

.todo-list {
  width: 400px;
}

.todo-list__form {
  display: flex;
}

.todo-list__form input {
  flex: 1;
  padding: 15px;
  outline: none;
  border: none;
  font-size: 1rem;
}

.todo-list__form button {
  padding: 15px;
  border: none;
  background-color: lightslategray;
  color: white;
}

.todo-list__tabs {
  display: flex;
  margin: 10px 0;
  background-color: white;
  list-style-type: none;
}

.todo-list__tabs > li {
  padding: 15px 20px;
  letter-spacing: 2px;
  cursor: pointer;
  transition: background-color 300ms, color 300ms;
}

.todo-list__tabs > li.active,
.todo-list__tabs > li:hover {
  background-color: lightslategray;
  color: white;
}

.todo-list__items {
  display: flex;
  flex-direction: column;
  list-style-type: none;
}

.todo-list__items li {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  padding: 10px 20px;
  background-color: white;
}

.todo-list__items > li > label {
  display: inline-flex;
  align-items: center;
  flex: 1;
}

.todo-list__items > li > label > input {
  margin-right: 10px;
}

.todo-list__not-found {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 200px;
  background-color: lightslategray;
  color: white;
}


JavaScript 操作

// 去取得 DOM 上面 class 名稱是 todo-list 的節點
const todoList = document.querySelector('.todo-list');

// 去取得 todoList 中 class 名稱是 todo-list__form 的節點
const todoListFrom = todoList.querySelector('#create-todo-list');
// 去取得 todoListFrom 中的第一個 button 節點
const todoListFromButton = todoListFrom.querySelector('button');
// 去取得 todoListFrom 中的第一個 input 節點
const todoListFromInput = todoListFrom.querySelector('input');

// 去取得 todoList 中 id 名稱是 search-todo-list 的節點
const todoListSearch = todoList.querySelector('#search-todo-list');
// 去取得 todoListSearch 中的第一個 button 節點
const todoListSearchButton = todoListSearch.querySelector('button');
// 去取得 todoListSearch 中的第一個 input 節點
const todoListSearchInput = todoListSearch.querySelector('input');

// 去取得 todoList 中 class 名稱是 todo-list__tabs 底下的 li 節點
const todoListTabs = todoList.querySelectorAll('.todo-list__tabs li');

// 去取得 todoList 中 class 名稱是 todo-list__items 的節點
const todoListItems = todoList.querySelector('.todo-list__items');

// 去取得 todoList 中 class 名稱是 todo-list__info 的節點
const todoListInfo = todoList.querySelector('.todo-list__info');

// 去取得 localStorage 上方的資料,若沒有結果的話會回傳結果 null
// 使用短路求值的技巧做操作
let todoListData = JSON.parse(localStorage.getItem('todoList')) || [];

// 定義整個 todo list 的初始狀態
let currentStatus = '全部';

// 定義一個關鍵字
let keyword = '';

// 建立一個協助我們宣染頁面 todo list 的函式
const renderTodoList = () => {
  // 在此處先宣告一個將要被宣染到 html 的變數,沒有跟賦值一起操作是為了讓過程比較好修改與理解
  let todoListUI;

  todoListUI =
    currentStatus === '全部'
      ? todoListData
      : todoListData.filter((element) => element.status === currentStatus);

  todoListUI = todoListUI.filter((element) => element.title.includes(keyword));

  // 讓要被宣染到 html 的物件,指定成一個 map 出來的新陣列,此處將會回傳一個 html 模板
  todoListUI = todoListUI.map(
    (element, index) =>
      `<li>
        <label for="check_${index}">
          <input type="checkbox"
            onclick="editTodoItemStatus(${element.id})"
            ${element.status !== '代辦' ? 'checked' : ''}
            id="check_${index}">
          <h2>${element.title}</h2>
        </label>
        <span>${element.status}</span>
        <span class="material-symbols-outlined"
              onclick="deleteTodoItem(${element.id})">
            delete
        </span>
      </li>`,
  );

  // 這裡可以個別將 todoListUI 與 todoListData 列印出來,並觀察其中的差異
  console.log(todoListData); // 陣列資料
  console.log(todoListUI.join('')); // 要被宣染到 html 的內容

  // 這裡會將 todoListItems 這個 node 節點所在的 html,置換為下方的其中一種方式
  // 這裡藉由三元運算`式的方式,去判斷目前陣列是否有內容(用長度判斷),若沒有內容的話新增一個提示區塊
  todoListItems.innerHTML = todoListUI.length
    ? todoListUI.join('')
    : '<div class="todo-list__not-found">目前沒有內容</div>';

  todoListInfo.innerHTML = `${
    keyword ? '關鍵字:' + keyword : ''
  }目前的${currentStatus}共: ${todoListUI.length} 筆`;
};

const updateLocalStorage = () => {
  // 將已經新增資料的陣列,轉成 JSON 格式之後上傳到 localStorage 上面,這裡的機碼(儲存位子)使用 todoList
  localStorage.setItem('todoList', JSON.stringify(todoListData));
};

const editTodoItemStatus = (id) => {
  // 對原始資料的陣列進行搜尋,並取得符合輸入 id 的資料結果
  const itemData = todoListData.find((element) => element.id === id);

  // 對資料的狀態進行狀態上的判斷,隨後再將判斷出的結果進行賦值操作
  itemData.status = itemData.status !== '代辦' ? '代辦' : '已完成';

  // 觸發畫面繪製的函式
  renderTodoList();

  // 觸發 updateLocalStorage 的函式操作
  updateLocalStorage();
};

const deleteTodoItem = (id) => {
  // 將 todoListData 重新給予一個沒有包括傳入 id 物件的陣列
  todoListData = todoListData.filter((element) => element.id !== id);

  // 觸發畫面繪製的函式
  renderTodoList();

  // 觸發 updateLocalStorage 的函式操作
  updateLocalStorage();
};

// 幫 todoListFromButton 這個節點新增按鈕點擊事件
todoListFromButton.addEventListener('click', function (event) {
  // 因為我們的 button 是放在 form 中,因此點擊時會直接觸發表單送出操作,這裡是將這一預設操作給停止
  event.preventDefault();

  // 從 todoListFormInput 上方取得他的數值
  const inputValue = todoListFromInput.value;

  // 判斷 inputValue 在被剪裁去空格之後,是否還有內容值,若變為空字串則會被轉換為 false
  if (!inputValue.trim()) {
    // 將 todoListFromInput 的數值改為空字串
    todoListFromInput.value = '';
    // 將函示進行返回操作,當一個函式 return 時後面的程式將不會運作
    return;
  }

  // 將 todoListFromInput 的數值改為空字串
  todoListFromInput.value = '';

  // 新增一筆資料到我們的 todoListData 這個 array 上面
  todoListData.push({
    // 幫資料使用目前時間建立一個 id
    id: new Date().getTime(),
    title: inputValue,
    status: '代辦',
  });

  // 觸發 updateLocalStorage 的函式操作
  updateLocalStorage();

  // 觸發畫面更新的函式,因為此時他所倚賴的陣列已經更新了內容,因此會在畫面上增加一筆項目
  renderTodoList();
});

// 幫 todoListFromButton 這個節點新增按鈕點擊事件
todoListSearchButton.addEventListener('click', function (event) {
  // 因為我們的 button 是放在 form 中,因此點擊時會直接觸發表單送出操作,這裡是將這一預設操作給停止
  event.preventDefault();

  // 設定關鍵字為 input 的 value
  keyword = todoListSearchInput.value;

  // 觸發畫面更新的函式,因為此時他所倚賴的陣列已經更新了內容,因此會在畫面上增加一筆項目
  renderTodoList();
});

// 對 todoListTabs 進行迴圈操作,幫每一個項目都執行一次函式
todoListTabs.forEach((element) => {
  // 對目前被執行的項目新增一個點擊事件
  element.addEventListener('click', () => {
    // 對 todoListTabs 進行迴圈操作,將每一個項目的 class 上的 active 做刪除
    todoListTabs.forEach((element) => element.classList.remove('active'));
    // 對目前被點擊的項目新增一個 active 的 class
    element.classList.add('active');
    // 將當前的定義設定為點擊的文字內容
    currentStatus = element.innerHTML;

    // 觸發畫面更新的函式,
    renderTodoList();
  });
});

// 此處算是初始化的操作,沒有執行的話畫面預設會是空的內容
renderTodoList();


Todo List 第三版

範例連結

project
│   index.html
│
└───assets
│   │
│   └───javascript
│   │   │   script.js
│   │      
│   └───style
│       │   style.css

HTML 樣板

<!DOCTYPE html>
<html lang="zh">
  <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>
    <!-- 引入 Material Icon 庫 -->
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
    />
    <link rel="stylesheet" href="assets/style/style.css" />
  </head>
  <body>
    <article class="todo-list">
      <form class="todo-list__form">
        <input type="text" placeholder="輸入代辦內容" />
        <button><span class="material-symbols-outlined"> add </span></button>
      </form>
      <ol class="todo-list__tabs">
        <li class="active">全部</li>
        <li>代辦</li>
        <li>已完成</li>
        <li>已刪除</li>
      </ol>
      <ul class="todo-list__items"></ul>
    </article>
    <script src="assets/javascript/script.js"></script>
  </body>
</html>


CSS樣式

* {
  margin: 0;
  padding: 0;
}

body {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  padding: 40px;
  min-height: 100vh;
  background-color: aliceblue;
}

.todo-list {
  width: 400px;
}

.todo-list__form {
  display: flex;
}

.todo-list__form input {
  flex: 1;
  padding: 15px;
  outline: none;
  border: none;
  font-size: 1rem;
}

.todo-list__form button {
  padding: 15px;
  border: none;
  background-color: lightslategray;
  color: white;
  cursor: pointer;
}

.todo-list__tabs {
  display: flex;
  margin: 10px 0;
  background-color: white;
  list-style-type: none;
}

.todo-list__tabs > li {
  padding: 15px 20px;
  letter-spacing: 2px;
  cursor: pointer;
  transition: background-color 300ms, color 300ms;
}

.todo-list__tabs > li.active,
.todo-list__tabs > li:hover {
  background-color: lightslategray;
  color: white;
}

.todo-list__items {
  display: flex;
  flex-direction: column;
  list-style-type: none;

  perspective: 500pc;
}

.todo-list__items > li {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  padding: 10px 20px;
  background-color: white;
}

.todo-list__items > li:hover > .todo-list__item-actions {
  opacity: 1;
  transform: rotateY(0deg);
}

.todo-list__item-actions {
  position: absolute;
  left: -100px;
  display: flex;
  width: 100px;
  height: 100%;
  opacity: 0;
  transition: transform 400ms, opacity 400ms;
  transform: rotateY(90deg);
  transform-origin: right;
}

.todo-list__item-actions > li {
  display: inline-flex;
  align-items: center;
  flex: 1;
  justify-content: center;
  height: 100%;
  color: white;
  cursor: pointer;
}

.todo-list__item-actions > li:nth-child(1) {
  background-color: #f0a48c;
}

.todo-list__item-actions > li:nth-child(2) {
  background-color: #70c670;
}

.todo-list__not-found {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 200px;
  background-color: lightslategray;
  color: white;
}


JavaScript 操作

const todoList = document.querySelector('.todo-list');

const todoListFrom = todoList.querySelector('.todo-list__form');
const todoListFromButton = todoListFrom.querySelector('button');
const todoListFromInput = todoListFrom.querySelector('input');

const todoListTabs = todoList.querySelectorAll('.todo-list__tabs li');
const todoListItems = todoList.querySelector('.todo-list__items');

const todoListSourceData = JSON.parse(localStorage.getItem('todoList'))
  ? JSON.parse(localStorage.getItem('todoList'))
  : [];

let currentList = todoListSourceData;
let currentStatus = '全部';

const renderTodoList = () => {
  let renderList;
  if (currentStatus !== '全部') {
    currentList = todoListSourceData.filter(
      (element) => element.status === currentStatus,
    );
  } else {
    currentList = todoListSourceData;
  }

  renderList = currentList.map(
    (element) =>
      `<li>
          <ol class="todo-list__item-actions">
            <li onClick="editTodo(${element.id},'已刪除')"><span class="material-symbols-outlined"> delete </span></li>
            <li onClick="editTodo(${element.id},'已完成')"><span class="material-symbols-outlined"> check </span></li>
          </ol>
          <h2>${element.title}</h2>
          <span>${element.status}</span>
      </li>`,
  );

  todoListItems.innerHTML = renderList.length
    ? renderList.join('')
    : '<div class="todo-list__not-found">目前沒有內容</div>';
};

const editTodo = (id, status) => {
  todoListSourceData.find((e) => e.id === id).status = status;
  updateLocalStorage();
  renderTodoList();
};

const updateLocalStorage = (newTodo = null) => {
  if (newTodo !== null) {
    currentList.push(newTodo);
  }
  localStorage.setItem('todoList', JSON.stringify(todoListSourceData));
};

todoListTabs.forEach((element) => {
  element.addEventListener('click', function () {
    todoListTabs.forEach((typeItem) => typeItem.classList.remove('active'));
    element.classList.add('active');
    currentStatus = element.innerHTML;
    renderTodoList();
  });
});

todoListFromButton.addEventListener('click', function (event) {
  event.preventDefault();
  const inputValue = todoListFromInput.value;

  if (!inputValue.trim()) {
    todoListFromInput.value = '';
    alert('請輸入空格以外的內容');
    return;
  }

  updateLocalStorage({
    id: new Date().getTime(),
    title: inputValue,
    status: '代辦',
  });
  renderTodoList();

  todoListFromInput.value = '';
});

renderTodoList();


簡易購物車設計

範例連結

project
│   index.html
│
└───assets
│   │
│   └───javascript
│   │   │   script.js
│   │      
│   └───scss
│   │   │   style.scss
│   │      
│   └───css
│       │   style.css
│       │   style.css.map

HTML 樣板

<!DOCTYPE html>
<html lang="zh">
  <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>購物車練習</title>
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0"
    />

    <link rel="stylesheet" href="assets/css/style.css" />
  </head>
  <body>
    <main class="main">
      <article class="main__container">
        <ul class="product-items">
          <li>
            <header class="product-items__cover">
              <img
                src="https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
              />
            </header>
            <article class="product-items__info">
              <h2>包包</h2>
              <b>123</b>
              <section class="product-items__number">
                <span class="material-symbols-outlined"> remove </span>
                <input type="number" value="1" />
                <span class="material-symbols-outlined"> add </span>
              </section>
              <button class="product-items__button">新增商品</button>
            </article>
          </li>
          <li>
            <header class="product-items__cover">
              <img
                src="https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg"
              />
            </header>
            <article class="product-items__info">
              <h2>衣服</h2>
              <b>213</b>
              <section class="product-items__number">
                <span class="material-symbols-outlined"> remove </span>
                <input type="number" value="1" />
                <span class="material-symbols-outlined"> add </span>
              </section>
              <button class="product-items__button">新增商品</button>
            </article>
          </li>
          <li>
            <header class="product-items__cover">
              <img
                src="https://fakestoreapi.com/img/71li-ujtlUL._AC_UX679_.jpg"
              />
            </header>
            <article class="product-items__info">
              <h2>大衣</h2>
              <b>321</b>
              <section class="product-items__number">
                <span class="material-symbols-outlined"> remove </span>
                <input type="number" value="1" />
                <span class="material-symbols-outlined"> add </span>
              </section>
              <button class="product-items__button">新增商品</button>
            </article>
          </li>
        </ul>
      </article>
      <aside class="main__aside">
        <header class="main__aside-header"></header>
        <ul class="cart-items"></ul>
      </aside>
    </main>
    <script src="assets/javascript/script.js"></script>
  </body>
</html>

CSS 樣板

* {
  margin: 0;
  padding: 0;
}

ul {
  list-style-type: none;
}

.main {
  display: flex;

  &__aside {
    display: inline-flex;
    flex-basis: 280px;
    flex-direction: column;
    padding: 10px;
  }

  &__container {
    display: inline-flex;
    flex: 1;
    padding: 10px;
  }

  &__aside-header {
    padding: 20px;
    background-color: bisque;
  }
}

.product-items {
  display: grid;
  align-items: flex-start;
  width: 100%;

  gap: 10px;
  grid-template-columns: repeat(3, 1fr);

  img {
    max-width: 100%;
    max-height: 100%;
  }

  > li {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    padding: 10px;
    border: #e2dfdf solid 1px;
  }

  &__cover {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 200px;
  }
  &__info {
    display: inline-flex;
    flex-direction: column;
  }
  &__number {
    display: inline-flex;
    margin: 10px 0;

    input {
      width: 100%;
    }

    > span {
      display: flex;
      width: 24px;
      height: 24px;
      background-color: rgb(82, 82, 144);
      color: white;
    }
  }
  &__button {
    padding: 10px;
    background-color: burlywood;
  }
}

.cart-items {
  img {
    width: 100%;
  }
  > li {
    display: inline-flex;
    align-items: center;
    margin-top: 10px;
    padding: 10px;
    border: #e2dfdf solid 1px;
  }
  &__cover {
    flex: 1;
    padding: 10px;
  }

  &__info {
    flex: 2;
  }

  &__number {
    display: inline-flex;

    input {
      width: 100%;
    }

    > span {
      display: flex;
      width: 24px;
      height: 24px;
      background-color: rgb(82, 82, 144);
      color: white;
    }
  }

  &__button {
    padding: 10px;
    width: 100%;
    background-color: rgb(161, 53, 53);
    color: white;
  }
}

JavaScirpt 樣板

const productItems = document.querySelectorAll('.product-items li');
const asideHeader = document.querySelector('.main__aside-header');
const cart = document.querySelector('.cart-items');
let cartItemsData = [];

productItems.forEach((element) => {
  const elementButton = element.querySelector('button');
  elementButton.addEventListener('click', () => {
    const itemName = element.querySelector('h2').innerHTML;
    const itemNumber = Number(element.querySelector('input').value);
    const cartItem = cartItemsData.find((item) => item.name === itemName);
    if (cartItem) {
      cartItem.number += itemNumber;
    } else {
      const newItem = {
        name: itemName,
        number: itemNumber,
        price: element.querySelector('b').innerHTML,
        imageSrc: element.querySelector('img').src,
      };
      cartItemsData.push(newItem);
    }

    renderCartItems();
  });
});

const renderCartItems = () => {
  let todoListUI;

  todoListUI = cartItemsData.map(
    (element) =>
      `<li>
			<header class="cart-items__cover">
				<img
					src="${element.imageSrc}"
				/>
			</header>
			<article class="cart-items__info">
				<h2>${element.name}</h2>
				<b>單價:${element.price}</b>
				<section class="cart-items__number">
					數量:${element.number}
				</section>
				<button class="cart-items__button">刪除項目</button>
			</article>
		</li>`,
  );
  cart.innerHTML = todoListUI.length
    ? todoListUI.join('')
    : '<li class="todo-list__not-found">目前沒有內容</li>';
  renderCartInfo();
  createNumberBoxAction();
  createItemDeleteAction();
};

const renderCartInfo = () => {
  const countNumber = cartItemsData.reduce(
    (prev, current) => prev + current.number,
    0,
  );
  const countPrice = cartItemsData.reduce(
    (prev, current) => prev + current.number * current.price,
    0,
  );
  asideHeader.innerHTML = `您總計選擇了 ${countNumber} 項商品,共計 NT.${countPrice}元 `;
};

const createNumberBoxAction = () => {
  const numberBox = document.querySelectorAll('.product-items__number');
  numberBox.forEach((element) => {
    let subtractBtn = element.querySelector('span');
    let addBtn = element.querySelector('span:nth-child(3)');
    let input = element.querySelector('input');

    subtractBtn.onclick = () => {
      input.value = Number(input.value) - 1;
      if (input.value <= 0) {
        input.value = 0;
      }
    };

    addBtn.onclick = () => {
      input.value = Number(input.value) + 1;
    };

    input.onkeyup = () => {
      if (Number(input.value) < 0) {
        input.value = 0;
      }
    };
  });
};

const createItemDeleteAction = () => {
  if (!cartItemsData.length) return;
  const cartItems = document.querySelectorAll('.cart-items li');

  cartItems.forEach((element, index) => {
    let deleteButton = element.querySelector('button');
    deleteButton.onclick = () => {
      cartItemsData.splice(index, 1);
      renderCartItems();
    };
  });
};

renderCartItems();


上一篇
數組與對象更多內容頗析
下一篇
學習 API 請求與陳諾的概念
系列文
給前端新手的圖文故事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言