---
# System prepended metadata

title: 0428 購物車實作練習
tags: [JavaScript]

---

# 0428 購物車實作練習 
###### tags: `JavaScript`

## 環境設定
- [龍哥做的貓貓購物車架構 GitHub 連結](https://github.com/kaochenlong/shopping-cart-v2)
- 拿到新專案時第一件事：看 `README`
  - make sure yarn is installed.
  - run yarn install to install all packages.
  - run yarn run dev or yarn dev to run a development server on your local machine.
  - and write all implements in src/main.js.
- 給用 ubuntu 的朋友們：
  - 如果遇到 node 版本太舊的問題可以參考下面網站
  - [NodeSource Node.js Binary Distributions](https://github.com/nodesource/distributions/blob/master/README.md)

## 讓每個按鈕都可以動
```javascript=
document.addEventListener('DOMContentLoaded', () => {
  const buttons = document.querySelectorAll('.card .btn')
  buttons.forEach(btn => {
    btn.addEventListener('click', () => {
      console.log('hi');
    })
  })
})
```
## 把功能拉出來
- 把固定使用的 function 抓到外面
```javascript=
const addToCart = btn => {
  btn.addEventListener('click', () => {
    console.log('hi');
  })
}

document.addEventListener('DOMContentLoaded', () => {
  const buttons = document.querySelectorAll('.card .btn')
  buttons.forEach(btn => addToCart(btn))
  // buttons.forEach(addToCart)    這兩行道理一樣
})
```
## 修改 addToCart 功能
### 抓取商品名跟價格
  - 觀察 HTML 架構設定
```javascript=
const addToCart = btn => {
  btn.addEventListener('click', (e) => {
    const card = e.currentTarget.parentElement.parentElement
    const title = card.querySelector('.card-title').textContent
    const price = parseFloat(card.querySelector('.price')
                                 .textContent
                                 .replace('$', ''))
  })
}
```
### 相關運算式
#### e.target & e.currentTarget
- [Event.target](https://developer.mozilla.org/zh-TW/docs/Web/API/Event/target)
- [Event.currentTarget](https://developer.mozilla.org/zh-TW/docs/Web/API/Event/currentTarget)
- e.target 是指向按到的物件
- e.currentTarget 是指向你裝 eventListener 的物件

#### replace
- [String.prototype.replace()](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/String/replace)
- 預設使用 const 宣告，若值會有變化使用 let
```=javascript
  const price = card.querySelector('.price').textContent.replace('$', '')
 //將 $ 取代成 ''
```


#### parseFloat 轉成小數點
- [parseFloat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat)
- 把一個東西換成小數點


#### parseInt 轉成整數
- [parseInt](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/parseInt)

### constructor 建構子
- [constructor](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Classes/constructor)
- 隨著 class 一同建立並初始化物件的特殊方法；一個 class 只能有一個
```javascript=
class Monster {
}

class Hero {
  constructor(name) {
    this.name = name
  }
}

const h1 = new Hero('悟空')
console.log(h1)
// Hero {name: "悟空"}
```
```javascript=
class Monster {
  constructor(hp) {
    this.hp = hp
  }
}

class Hero {
  constructor(name) {
    this.name = name
  }
  
  hit(monster) {
    if (monster.hp > 0) {
      console.log('hit!!!')
      moster.hp -= 10
    } else {
      console.log('booooom!')
    }
  }
}

const h1 = new Hero('悟空')
const m1 = new Monster(50)
hi.hit(m1)
// 先偵測 hp 然後再對 m1 扣 hp
```
物件導向程式設計
把邏輯包在物件裡面，讓物件變成彼此的參數傳來傳去

現階段可以將constructor 跟 initialize 看作一樣的功能
但實際還是有點不一樣

## 把一些建構式拆出來
- 在 scripts 中建立新檔案 `cart.js` `cart-item.js`

cart.js
```javascript=
class Cart {
}

export default Cart
```
cart-item.js
```javascript=
class CartItem {
}

export default CartItem
```
main.js
```javascript=
import Cart from './cart'
import CartItem from './cart-item'
```
## 建構購買物品
/cart-item.js
```javascript=
class CartItem {
  constructor(title, price, quantity = 1) {
    this.title = title
    this.price = price
    this.quantity = quantity
  }
}
```
/main.js
```javascript=
const addToCart = btn => {
  btn.addEventListener('click', (e) => {
    const card = e.currentTarget.parentElement.parentElement
    const title = card.querySelector('.card-title').textContent
    const price = parseFloat(card.querySelector('.price')
                                 .textContent
                                 .replace('$', ''))
    // 加到購物車
    const item = new CartItem(title, price)
    console.log(item)  // 檢查一下
  })
}
```
- 解構
  - 因為物件本身沒有順序問題
  - 所以直接以名字去指定
  - 上一個作法會有順序問題，如果打反了就會出問題

/cart-item.js
```javascript=
class CartItem {
  constructor({ title, price, quantity = 1 }) {
    this.title = title
    this.price = price
    this.quantity = quantity
  }
}
```
/main.js
```javascript=
const addToCart = btn => {
  btn.addEventListener('click', (e) => {
    const card = e.currentTarget.parentElement.parentElement
    const title = card.querySelector('.card-title').textContent
    const price = parseFloat(card.querySelector('.price')
                                 .textContent
                                 .replace('$', ''))
    // 加到購物車
    const item = new CartItem({ title, price })
    // const item = new CartItem({title, price}){
    //   title = title
    //   price = price
    // }
    // 如果 key 跟 value 是一樣的，可以只寫一個
    console.log(item)
  })
}
```
## 建構購物車
/cart.js
```javascript=
class Cart {
  constructor() {
    this.items = []
  }
  // constructor(items = []) {
  //   this.items = items
  // }

  add(item) {
    this.items.push(item)
// this.items為空陣列，將 item 塞進去
    console.log(this.items);
  }
}

export default Cart
// 預設匯出Cart
```
/main.js
- 可以不用放在 DOMContentLoaded 裡面
```javascript=
const cart = new Cart()
```
## 解決同一商品重複選取問題
- 去抓 id 然後檢查購物車裡面是不是有相同的 id
  - 有相同的 => 增加數量
  - 沒有相同的 => 增加品項，數量預設 1

/cart.js
```javascript=
add(item) {
  const foundItem = this.items.find(t => t.id == item.id)
 // 檢查有沒有重複 
  if (foundItem) {
    // 增加數量
    foundItem.increment(n)
  } else {
    this.items.push(item)  // 增加品項，數量預設 1
  }
}
```
/main.js
```javascript=
const addToCart = btn => {
  btn.addEventListener('click', (e) => {
    const card = e.currentTarget.parentElement.parentElement
    const title = card.querySelector('.card-title').textContent
    const price = parseFloat(card.querySelector('.price')
                                 .textContent
                                 .replace('$', ''))
    const id = card.dataset['productId']}
    // 加到購物車
    const item = new CartItem({ id, title, price })
```
/cart-item.js
```javascript=
class CartItem {
  constructor({id, title, price, quantity = 1}) {
    this.title = title
    this.price = price
    this.quantity = quantity
    this.id = id
  }
  
  increment(n = 1){
    this.quantity += n
  }
}
```
## 渲染購物車內容
- 去看 html 檔案，分析 table 裡的架構
- 建立一個 ui 的檔案

/ui.js
```javascript=
                    //接到     回傳
const buildItemList = cart =>{
  return `
  <tr>
    <td>老大</td>
    <td><input type="number" class="quantity" value="1"></td>
    <td>$20</td>
    <td>$20</td>
    <td><button class="remove-item-btn btn btn-danger btn-sm"><i class="fas         fa-trash-alt"></i></button></td>
  </tr>         
  `
}

export { buildItemList }
```
/main.js
```javascript=
import { buildItemList } from './ui' 

const addToCart = btn => {
  btn.addEventListener('click', (e) => {
    const card = e.currentTarget.parentElement.parentElement
    const title = card.querySelector('.card-title').textContent
    const price = parseFloat(card.querySelector('.price')
                                 .textContent
                                 .replace('$', ''))
    const id = card.dataset['productId']
      // 加到購物車
    const item = new CartItem({ id, title, price })
    cart.add(item)
    
    const result = buildItemList(cart)
    document.querySelector('.cart tbody').innerHTML = result
  })
}
```
- 把功能拉到外面

/main.js
```javascript=
const renderUI = () => {
  const result = buildItemList(cart)
  document.querySelector('.cart tbody').innerHTML = result
}

const addToCart = btn => {
  btn.addEventListener('click', (e) => {
    const card = e.currentTarget.parentElement.parentElement
    const title = card.querySelector('.card-title').textContent
    const price = parseFloat(card.querySelector('.price')
                                 .textContent
                                 .replace('$', ''))
    const id = card.dataset['productId']
      // 加到購物車
    const item = new CartItem({ id, title, price })
    cart.add(item)
    
    renderUI()
  })
}
```

- 讓購物車選到不同商品時會有不同品項

/ui.js
```javascript=
const buildItemList = cart =>{
  const r = cart.items.map(item => {
  return `
  <tr>
    <td>${item.title}</td>
    <td><input type="number" class="quantity" value="1"></td>
    <td>$${item.price}</td>
    <td>$20</td>
    <td><button class="remove-item-btn btn btn-danger btn-sm"><i class="fas          fa-trash-alt"></i></button></td>
  </tr>         
  `
  })
  
  return r.join('')  // 因為 map 出來是一個陣列，他直接渲染中間會有一個逗號
}
```
## 清空購物車
/main.js
```javascript=
document.addEventListener('DOMContentLoaded', () => {
  const buttons = document.querySelectorAll('.card .btn')
  buttons.forEach(addToCart)

  document.querySelector('.empty-cart').addEventListener('click', () => {
    cart.empty()   // 把購物車清空
    renderUI()  // 把購物車畫面重新渲染
  })
})
```
/cart.js
```javascript=
class Cart {
  empty() {
    this.items = []    // 把購物車清空的功能做出來
  }
}
```
## 計算小計
- 額外加功能的好處
  - 增加可讀性
  - 降低維護難度

/cart-item.js
```javascript=
class CartItem {
  totalPrice() {
    return this.price * this.quantity
}}
```
/ui.js (把小計換掉)
```javascript=
const buildItemList = (cart) => {
  const list = cart.items.map(item => {
    return `<tr>
      <td>${item.title}</td>
      <td><input type="number" class="quantity" value="${item.quantity}"></td>
      <td>$${item.price}</td>
      <td>$${item.totalPrice()}</td>
      <td><button class="remove-item-btn btn btn-danger btn-sm"><i class="fas fa-trash-alt"></i></button></td>
    </tr>`
  })

  return list.join('')
}
```
## 計算總計
- 把每一個品項的小計都加起來

/cart.js
```javascript=
class Cart {
  totalPrice() {
    let total = 0

    this.items.forEach(item => {
      total += item.totalPrice()
    })

    return total
  }
}
```
- 比較高級的做法
- [reduce 用法的 MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)
```javascript=
class Cart {
  totalPrice() {
    return Math.round(this.items.reduce(
      (total, currentItem) => total + currentItem.totalPrice(),
      0
    )* 100) / 100
}
//  如果品項多的話會有浮點數計算問題，所以加四捨五入功能避免
```
- 把結果渲染上去

/main.js
```javascript=
const renderUI = () => {
  const result = buildItemList(cart)
  document.querySelector('.cart tbody').innerHTML = result

  document.querySelector('.cart .total-price').textContent = '$' + cart.totalPrice()
}
```
## 一進網頁就清空購物車
- 不動 HTML 的話可以直接 RenderUI()
- 其實應該直接把 HTML 不需要的部分刪掉
  - 檢視原始碼的話還是會在
## 刪除購物車內品項
- 偷塞 id 進去做出來的 innerHTML

/ui.js
```javascript=
const buildItemList = (cart) => {
  const list = cart.items.map(item => {
    return `<tr>
      <td>${item.title}</td>
      <td><input type="number" class="quantity" value="${item.quantity}"></td>
      <td>$${item.price}</td>
      <td>$${item.totalPrice()}</td>
      <td><button data-id="${item.id}" class="remove-item-btn btn btn-danger btn-sm"><i class="fas fa-trash-alt"></i></button></td>
    </tr>`
  })

  return list.join('')
}
```
- 直接在渲染出東西的時候裝監聽器

/main.js
```javascript=
const renderUI = () => {
  document.querySelectorAll('.remove-item-btn').forEach(btn => {
    btn.addEventListener('click', (e) => {
      cart.removeItemId(e.currentTarget.dataset['id'])
      renderUI()
    })
  })
}
```
- 在購物車裡裝上清除品項的功能
- [filter 的 MDN 說明](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array/filter)

/cart.js
```javascript=
  removeItemId(id) {
    this.items = this.items.filter(item => item.id != id)
  }  // 過濾符合條件的東西（只要不是選到 ID 的品項就留下來 => 刪除被選到的）
```
* filter 在 ruby 中的用法
* [filter api](https://apidock.com/ruby/Array/filter)
```ruby=
a = [1, 2, 3, 4, 5]

a.filter{|n| n < 2}
a.select{|n| n < 2}
```

## 防止購物車數量小於 0
- 設定最小值

/ui.js
```javascript=
const buildItemList = (cart) => {
  const list = cart.items.map(item => {
    return `<tr>
      <td>${item.title}</td>
      <td><input type="number" min="1" class="quantity" value="${item.quantity}"></td>
      <td>$${item.price}</td>
      <td>$${item.totalPrice()}</td>
      <td><button data-id="${item.id}" class="remove-item-btn btn btn-danger btn-sm"><i class="fas fa-trash-alt"></i></button></td>
    </tr>`
  })

  return list.join('')
}
```
## 打包
- 使用 yarn 裡面的 parcel 套件
- 到 `package.json` 裡面檢查 `"script"` 裡面的東西
- parcel 裡有建立一個 build 功能可直接使用
- 在 terminal 裡面輸入 `$ yarn build` 就可以打包進 `/dist` 內
- 類似 rails 的 webpack 功能
* scripts 裡面會是一個物件
  * Key 就是可以輸入的指令
  * Value 就是對到的指令
  * 都使用字串
  * 有點類似簡寫的概念


## Vue
起手式
```javascript=
new Vue({

  el: document.querySelectoy('#hello')
  el: '#hello' 上下等同
  //綁定物件↑
  data: {
  name: kk
  }
})

<div id='hello'>hello {{ name }}</div>
```


## 佈署網站
- [netlify](https://www.netlify.com/)
  - 把 `/dist` 裡面的東西丟上去就佈署完畢
  - 一堆 react 的程式碼，丟上去會自動編譯
  - 拖拉上去就有作品集或履歷...，還可以有網址
---




## 題外話
### webpack & parcel
- webpacker: 是一個 gem，用來打包 webpack 功能
- 瀏覽器無法讀取scss檔案
- parcel 幾乎不用設定 自動編譯
- 上線使用 `parcel build`

### 物件解構 常用
- 只要等號右邊是物件，就可以用解構
- [參考資料：解構賦值](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)

```javascript=
const obj = {name: 'kk', age: 123}

const name = obj.name
const age = obj.age
// 上面兩行等於下面一行
const { name, age } = obj
```

- 物件所有屬性的預設值是undefined
```javascript=
const obj = {name: 'kk', age: 123}
const { name, age, hello} = obj
console.log(hello)  // undefined
```
- 解構賦值
```javascript=
// 方法 1
function a(words){
  const {name, age} = words
  console.log(name)
  console.log(age)
}
// 方法 2
function  a({ name, age }){
  console.log(name) // kk
  console.log(age) // 18
}
// 方法 3
function a({ name, age }){
  console.log(name) // kk
  console.log(age) // 18
}

a({ name: 'kk', age: '18' })
```
### 其實不用所有 function 都放在 DOMContentLoaded 裡面
通常放裡面只是為了集大成，裝 eventListener... 等等
其他的功能可以先在外面設定，

### includes()
- 是否包含
```javascript=
const li = ['a', 'b' , 3, 'c']
console.log(li.includes(4))  // false
console.log(li.includes(3))  // true
```
### 是否有 + 條件式
```javascript=
const li = [1, 2, 3, 4, 8]

for(n of li) {
  if(n >= 5) {
    console.log(true)
    break;
  }
}
// break
```

```javascript=
const li = [1, 2, 3, 4, 8]

const r = li.some(n => n >= 5)
// 上下兩行相同，是否有些元素有符合條件
const r = li.some(function(n) {return n>5))
console.log(r)


```
```javascript=
const li = [1, 2, 3, 4, 8]

const r = li.every(function(n) {return n > 0})
// 是否全部元素都符合條件
console.log(r)
```

```javascript=
const li = [1, 2, 3, 4, 8]
const greaterThanZero = n => n >= 0
const r = li.every(greaterThanZero)
console.log(r)
```

```javascript=
const li = [1, 2, 3, 4, 8]
const greaterThanZero = n => n >= 0
const r = li.some(greaterThanZero)
console.log(r)
```
## 找陣列裡的物件裡的東西
- 回傳找到的第一筆物件
```javascript=
const list = [
  {name: 'kk', age: 18}
  {name: 'aa', age: 12}
  {name: 'bb', age: 20}
  {name: 'cc', age: 30}
]
const found = list.find(n=>{
  return n.age < 18
})
// const found = list.find(n => n.age < 18)
console.log(found)
// {name: 'aa', age: 12}
```
- 在 ruby 內的相似方法
```ruby=
list = [
  {name: 'kk', age: 18}
  {name: 'cc', age: 30}
]

p list.find{ |n| n[:age] > 18 }
```

  