WooCommerce Cat 外掛開發 (五) – 修改購物車

前情提要:上一篇我們實作了 Alpine.js 的迴圈,也認識了 x-bind 綁定屬性、x-text/x-html 輸出文字,以及對於 JavaScript 非同步事件的理解,知道該如何輸出資料後,接下來我們進行輸入的部分,也就是修改購物車的商品數量以及刪除功能。

商品數量與刪除功能的介面如下:

https://oberonlai.blog/wp-content/uploads/woocommerce-cat-5/woocommerce-cat-01.jpg

我們希望完成的功能如下:

  • 當使用者點擊數量的加號按鈕時,數量會加一
  • 當使用者點擊數量的加號按鈕時,如果數量超過該商品的庫存數量,則顯示剩餘庫存數量,並提示庫存上限
  • 當使用者點擊數量的減號按鈕時,數量會減一
  • 當使用者點擊數量的減號按鈕時,如果數量小於一時,則顯示 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() 方法,它帶有五個參數:

  1. type – 修改類型,decre 是減少、incre 是增加、change 是用文字框輸入修改數量。
  2. itemKey – 商品的 key,這是 Store API 用來修改購物車內容的必要參數
  3. quantity – 目前的商品數量
  4. quantity_limits – 商品庫存數量
  5. 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,最後把 itemKeyqty 作為參數,以 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 也會跟著變動,以下圖來看會比較好理解:

https://oberonlai.blog/wp-content/uploads/woocommerce-cat-5/woocommerce-cat-02.jpg

因此遇到表單的需求時,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 })
    }

}

我們看 typeincre 也就是點擊增加按鈕的寫法:

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 顯示

回到庫存的部分,我們希望在輸入數量超過該商品的庫存時,會顯示如下的文字提示:

https://oberonlai.blog/wp-content/uploads/woocommerce-cat-5/woocommerce-cat-03.jpg

我們先把提示文字的 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

目錄

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料

Picture of 賴俊吾 / Oberon Lai
賴俊吾 / Oberon Lai

現為全職 WordPress 工程師,網站開發經歷 11 年,專攻前端工程與 WordPress 佈景主題、外掛客製化開發

訂閱電子報

Hi,我是 Oberon,我會固定在每週五早上發送接案心得以及與 WordPress 相關的電子報,同時也會分享一些實用的開發知識,讓你在 WordPress 的接案路上不孤單!

專注於分享 WordPress 開發、接案技巧、專案管理等自由工作者必備知識與心得

© 2024 想點創意科技有限公司

想點創意科技有限公司 | 統一編號 90516823
Designed by Hend Design | 隱私權政策

訂閱電子報

Hi,我是 Oberon,我會固定在每週五早上發送接案心得以及與 WordPress 相關的電子報,同時也會分享一些實用的開發知識,讓你在 WordPress 的接案路上不孤單!