# 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 }
```