前情提要:上一篇我們實作了 Alpine.js 的迴圈,也認識了
x-bind
綁定屬性、x-text/x-html
輸出文字,以及對於 JavaScript 非同步事件的理解,知道該如何輸出資料後,接下來我們進行輸入的部分,也就是修改購物車的商品數量以及刪除功能。
商品數量與刪除功能的介面如下:
我們希望完成的功能如下:
- 當使用者點擊數量的加號按鈕時,數量會加一
- 當使用者點擊數量的加號按鈕時,如果數量超過該商品的庫存數量,則顯示剩餘庫存數量,並提示庫存上限
- 當使用者點擊數量的減號按鈕時,數量會減一
- 當使用者點擊數量的減號按鈕時,如果數量小於一時,則顯示 1
- 當使用者修改數量時,只能輸入正整數
- 當使用者點擊刪除按鈕時,購物車會將該商品移除
需要處理的事件有點多,我們一個一個步驟來吧~
購物車物件
四、x-on 事件綁定
先回顧目前的 HTML:
<ul>
<template x-for="item in cart.items" :key="item.id">
<li>
<!--商品圖片-->
<img :src="item.images[0].src" />
<!--商品名稱-->
<h3 x-text="item.name"></h3>
<!--單價-->
<p x-text="item.prices.currency_symbol+new Intl.NumberFormat().format(item.prices.price)"></p>
<!--數量-->
<div>...</div>
<!--小計-->
<div class="float:right f:24 f:red-50 float:none@sm mt:10 mt:0@sm" x-text="item.prices.currency_symbol+new Intl.NumberFormat().format((item.prices.price*item.quantity))"></div>
<!--刪除-->
<div>...</div>
</li>
</template>
</ul>
新增減少數量的按鈕,加入以下程式碼:
// 略
<!--數量-->
<div>
<!--減少數量按鈕-->
<div type="button" class="bg:fade-90:hover" :class="item.quantity === 1 ? 'pointer-events:none opacity:.5':''" @click.prevent="updateQty('decre',item.key,item.quantity,'',$el)">-</div>
</div>
首先可以看到在這個按鈕裡面,有兩個 CSS 的 class 屬性,差別在於一個有冒號一個沒有,這是前篇文章提到的 x-bind
的用法,沒有冒號的是原生的寫法,有冒號的則是 Alpine.js 的動態綁定,也就是我可以在裡面使用 JS 根據條件動態產生 class 名稱。
<div :class="item.quantity === 1 ? 'pointer-events:none opacity:.5':''"></div>
這邊使用三元運算子的寫法,也就是當 item.quantity
商品數量等於 1 的時候,降低按鈕透明度以及讓滑鼠點不到的 CSS 屬性,藉此讓減少數量按鈕在商品數量為 1 時不可點擊,以避免變成負數,然後在商品數量不等於 1 的情況下不輸出任何 CSS 屬性,原生 class 裡面的 CSS 屬性則不會受到商品數量的影響。
接下來看到我們的主角 x-on
事件綁定:
<div type="button" @click.prevent="updateQty('decre',item.key,item.quantity,item.quantity_limits.maximum,$el)">-</div>
x-on
的簡寫使用 @
這個符號,所以滑擊就是 @click
、按下鍵盤就是 @keyup
,JS 內建的事件都可以使用,至於 @click\.prevent
後面的 prevent 則是避免觸發預設的事件,也就是 preventDefault()
的縮寫,點擊按鈕後我們觸發 updateQty()
方法,它帶有五個參數:
- type – 修改類型,
decre
是減少、incre
是增加、change
是用文字框輸入修改數量。 - itemKey – 商品的 key,這是 Store API 用來修改購物車內容的必要參數
- quantity – 目前的商品數量
- quantity_limits – 商品庫存數量
- el – 觸發事件的對象,在這邊的話就是減少按鈕本身
本來是打算把減少、增加、修改數量用三個方法來處理,但因為處理的任務內容差不多,因此我把它設計在一起,updateQty()
實作如下:
updateQty(type, itemKey, quantity, quantity_limits = null, el = null) {
let qty;
quantity = parseInt(quantity)
quantity_limits = parseInt(quantity_limits)
switch (type) {
case 'decre':
qty = (quantity === 1) ? 1 : quantity - 1
break;
default:
break;
}
// 實際請求 Store API 修改購物車內容
if (qty) {
this.reqJSON(`${this.routeCart}/update-item`, 'POST', {
'key': itemKey,
'quantity': qty,
}).then(data => { this.cart = data })
}
}
在實際進行商品數量的修改前,記得要先確保傳入數量的型別是整數 Integer,不然後續在計算上會發生錯誤,接下來根據 type
參數來修改變數 qty
,最後把 itemKey
跟 qty
作為參數,以 POST 方法請求路徑 wp-json/wc/store/v1/cart/update-item
就能更新購物車資料。
接下來當點擊減少按鈕後,就可以更新購物車商品的數量,但我們在畫面上並沒有看到數量的改變,是哪裡發生了問題呢?
五、x-model 資料綁定
Alpine.js 的資料綁定是我最喜歡的功能,以往在製作表單的時候,必須要去接聽欄位的值是否有更新,有的話要取得該欄位的值,然後存到變數裡面以利後續傳送,有了 x-model
之後可以省下許多步驟,它能幫我們把接聽、取值、存值一次搞定。
我們先把數量的輸入框以及增加按鈕的 HTML 補上:
// 略
<!--數量-->
<div>
<!--減少數量按鈕-->
<div type="button" class="bg:fade-90:hover" :class="item.quantity === 1 ? 'pointer-events:none opacity:.5':''" @click.prevent="updateQty('decre',item.key,item.quantity,'',$el)">-</div>
<!--數量輸入框-->
<input type="text" x-model="item.quantity" @keyup="updateQty('change',item.key,$el.value,item.quantity_limits.maximum,$el);" />
<!--增加數量按鈕-->
<div type="button" class="bg:fade-90:hover" :class="item.quantity >= item.quantity_limits.maximum ? 'pointer-events:none opacity:.5':''" @click.prevent="updateQty('incre',item.key,item.quantity,item.quantity_limits.maximum,$el)">+</div>
</div>
在數量輸入框的地方,除了常見的 type="text"
以及前面提過的 @keyup="updateQty()"
事件綁定外,還多了一個 x-model="item.quantity"
,多了這個資料綁定代表只要修改 input
裡面的值,在物件 cart.items
裡面的 item.quantity
也會跟著變動,以下圖來看會比較好理解:
因此遇到表單的需求時,x-model
可以很方便的儲存使用者輸入的資料。回到點選加減按鈕沒有更新數量顯示的問題,原因在於我們是把 x-model
綁定在 input
上面,因此還需要多一個給值的動作來更新數量顯示,updateQty()
方法更新如下:
updateQty(type, itemKey, quantity, quantity_limits = null, el = null) {
let qty;
quantity = parseInt(quantity)
quantity_limits = parseInt(quantity_limits)
switch (type) {
case 'decre':
qty = (quantity === 1) ? 1 : quantity - 1
el.parentElement.querySelector('input')._x_model.set(qty)
break;
case 'incre':
qty = (quantity >= quantity_limits) ? quantity_limits : quantity + 1
el.parentElement.querySelector('input')._x_model.set(qty)
break;
case 'change':
qty = (quantity >= quantity_limits) ? quantity_limits : quantity;
(qty) ? el._x_model.set(qty) : el._x_model.set(1)
break;
default:
break;
}
// 實際請求 Store API 修改購物車內容
if (qty) {
this.reqJSON(`${this.routeCart}/update-item`, 'POST', {
'key': itemKey,
'quantity': qty,
}).then(data => { this.cart.items_count = data.items_count })
}
}
我們看 type
為 incre
也就是點擊增加按鈕的寫法:
qty = (quantity >= quantity_limits) ? quantity_limits : quantity + 1
el.parentElement.querySelector('input')._x_model.set(qty)
先判斷目前數量是否大於庫存數量,是的話 qty
就等於庫存數量,否則就是用傳入的數量加一,第二行我們選到數量的輸入框,由於該輸入框已經設定 x-model
,因此它會自動增加 _x_model
屬性,並且用 set()
方法來設定值,這邊只要帶入變數 qty
就能在點擊增加按鈕後看到數量的改變了。
六、x-mask 過濾輸入資料
在預設情況下 input
可以輸入任何字元以及字數,為了避免使用者不小心輸入到非數字的字元,或是輸入太大的數字,我們有必要來進行輸入資料的過濾。Alpine.js 官方提供一支外掛來協助我們處理這個問題,可以用 x-mask
來設定輸入的格式限制。
安裝方法可以直接使用 CDN 引入或是用 NPM 進行安裝:https://alpinejs.dev/plugins/mask
接下來我們在數量輸入框的地方多增加一個 x-mask
屬性:
<!--數量輸入框-->
<input type="text" x-model="item.quantity" @keyup="updateQty('change',item.key,$el.value,item.quantity_limits.maximum,$el);" x-mask="999" />
新增 x-mask="999"
代表的意思是只能輸入三位數的數字,就能讓使用者不能輸入英文或是其他符號,這還可以應用在其他地方,像是限制手機格式為 x-mask="9999-999-999"
、限制信用卡卡號 x-mask="9999 9999 9999 9999"
、限制日期 x-mask="9999/99/99"
等等,也可以用 x-mask.dynamic
設定動態的格式規則。
七、x-show / x-if 控制 HTML 顯示
回到庫存的部分,我們希望在輸入數量超過該商品的庫存時,會顯示如下的文字提示:
我們先把提示文字的 HTML 準備好,而它只會在商品有設定庫存時才會出現:
// 略
<!--數量-->
<div>
<!--減少數量按鈕-->
// 略
<!--數量輸入框-->
// 略
<!--增加數量按鈕-->
// 略
<!--庫存提示文字-->
<template x-if="item.quantity_limits.maximum!==9999">
<p>庫存上限 <span x-text="item.quantity_limits.maximum"></span></p>
</template>
</div>
這邊可以看到我們用了一個 <template>
來包住庫存提示,並且裡面用了 x-if
這個屬性來判斷該商品是否有設定庫存,當 x-if
裡面的參數為 true
時,瀏覽器會輸出裡面的 <p>...</p>
,false
的話則不輸出,判斷該商品是否有設定庫存則用 quantity_limits.maximum
決定,如果是 9999 的話代表沒有設定庫存。
因此我們判斷當 quantity_limits.maximum
不等於 9999 的時候才會輸出庫存提示文字,這邊要注意的是 x-if
只能用在 <template>
標籤身上,如果直接用在其他標籤如 <div>
是沒有作用的。
與 x-if
相似的還有一個叫做 x-show
的屬性,邏輯跟 x-if
一樣,都是當裡面回傳 true 時才會顯示,但差別在於 x-show
會將 HTML 輸出,控制顯示與否的原理是用 CSS 的 display
屬性,也就是說如果 x-show
為 true 時,該標籤會增加 style="display:block"
,反之為 style="display:none"
。
我們把 <p>
標籤加入 x-show
來控制顯示隱藏:
<!--庫存提示文字-->
<template x-if="item.quantity_limits.maximum!==9999">
<p x-show="item.quantity>=item.quantity_limits.maximum">庫存上限 <span x-text="item.quantity_limits.maximum"></span></p>
</template>
當商品數量大於等於商品庫存時,就會顯示「庫存上限 XX」的文字,這樣就完成庫存提示的功能了。
刪除購物車商品
最後我們來設計移除購物車內商品的功能,需要用到上面介紹過的 x-on
點擊事件,我們設計了一個 removeItem()
方法,傳入 item.key
以作為 Store API 刪除商品的必要參數,HTML 新增刪除按鈕:
<ul>
<template x-for="item in cart.items" :key="item.id">
<li>
// 略
<!--刪除-->
<div class="cursor:pointer p:0! abs top:0 right:5 rel@sm" @click.prevent="removeItem(item.key)"></div>
</li>
</template>
</ul>
removeItem()
方法實作如下:
removeItem(itemKey) {
this.reqJSON(`${this.routeCart}/remove-item`, 'POST', {
'key': itemKey,
}).then(data => this.cart = data)
},
具體內容就是帶參數 key
以 POST 方法請求路徑 wp-json/wc/store/v1/cart/remove-item
,並將回傳結果存回 cart
變數裡面,在顯示的部分我們不需要處理元素的移除,Alpine.js 會自動接聽 cart
變數的異動來重新輸出 HTML,因此當刪除的請求完成後該商品的 <li>
隨即移除,比以往要用 remove()
方便許多。
最後我們處理當購物車內沒有加入任何商品時,會顯示「購物車內沒有商品」的文字提示,用的一樣是 x-show
屬性,只要判斷 items_count
為 0 時顯示即可:
<div x-data="cart">
<ul>
<template x-for="item in cart.items" :key="item.id">
<li>
// 略
</li>
</template>
</ul>
<!--購物車內沒有商品的狀態-->
<div x-show="cart.items_count===0">購物車內沒有商品</div>
</div>
結語
購物車的基本功能到這邊告一段落,但以 WooCommerce 來說還有一堆功能可以整合,包含稅率的計算、依商品數量進行折扣、可變商品的規格顯示等等,以及當 API 請求錯誤時的一些狀態處理,這些就等日後再逐一完善了。
下一個部分我預計處理會員的註冊以及登入功能,我們下一篇繼續吧!
存放庫連結:https://github.com/oberonlai/woocommerce-cat