Writing in the front

In e-commerce platforms, SKU attribute selection is a common problem in product modules. In fact, it is not difficult to solve this problem, the key is to clarify their thinking, the big problem is divided into several small problems, and then beat it one by one. To write this article is to make a summary of the small program SKU attribute selection some time ago, and to put on the Internet to help you. Nuggets on the content are my original, if you need to reprint please indicate the source, thank you!

Requirement analysis and solution

I personally prefer to break this down into three small questions:

Requirement 1: RENDERING of SKU pages

On the product list page, click a different product, and the product details interface will be requested according to different product IDS to jump to the corresponding product details page. On the product details page, click the “Add to cart” button, and the SKU page of the product will pop up.

The result of a product details request for a product is as follows:

{data: {id: 246.name: Sling Cosmetic Bags DSFSAD test,... }}data: {id: 246.name: Sling Cosmetic Bags DSFSAD test,... }color_image: "https://assets.forucdn.com/basics/DD-W/j5tB2xyie4maeMWKboYDuOseOgz2Jar8kIZQE1u1.jpeg"
description: "

* EVA Flexibility  soles with  cross    performance  design  for sneaker shoes   

* Mesh  -knit fabric  upper lining construction with EVA padded insoles

* Complete with 4 eyelets and a lace up closure for a classic look

* Perfect for every season, wear them all year round

"
id: 246 images: ["https://appfiles.forucdn.com/samples/1/0/Z8-1-20210428091333-X41yFkML.jpg",... ]lowest_price: "9.99" merchant: {id: 1.code: "HCFW".name: Dimo Information Co., LTD..address: "Xifeng District, Lhasa",... }address: "Xifeng District, Lhasa" code: "HCFW" id: 1 name: Dimo Information Co., LTD. thumb: "https://appfiles.forucdn.com/avatars/5/20210426151158-T6UCoTjYJ6.jpeg" name: Sling Cosmetic Bags DSFSAD test size_image: "https://appfiles.forucdn.com/testing/admin/basics/Z8/OyrsoiMFpUNYeP8eWhZdHfCsQqPsKYDHGtYi2KYb.jpg" status: "active" type: "public" variants: [{id: 3795.price: "9.99".color: "White".gender: "General".size: "General-all code"},... ]0: {id: 3795.price: "9.99".color: "White".gender: "General".size: "General-all code"} 1: {id: 3796.price: "66.66".color: "Rich color".gender: "Man".size: "Man - 38"} 2: {id: 3797.price: "34.56".color: "Yellow".gender: "General".size: "Gm - 41"} 3: {id: 3798.price: "34.56".color: "Yellow".gender: "General".size: "Gm - 42"} 4: {id: 3799.price: "34.56".color: "Green".gender: "General".size: "Gm - 41"} 5: {id: 3800.price: "34.56".color: "Green".gender: "General".size: "Gm - 42"} 6: {id: 3801.price: "12.34".color: "Test color".gender: "General".size: "General-test equalization code"} 7: {id: 3802.price: "100.00".color: "Black".gender: "Man".size: "Men to 37"} 8: {id: 3803.price: "31.23".color: "dsad".gender: "Man".size: "Man - 323"} 9: {id: 3804.price: "99.99".color: "Test".gender: "General".size: "Gm - 41"} 10: {id: 3805.price: "99.99".color: "Test".gender: "General".size: "Gm - 42"} 11: {id: 3806.price: "99.99".color: "Test".gender: "General".size: "Gm - 43"} Copy the code

Below is the detailed page of the corresponding product:

         

The following figure shows the SKU page of the corresponding product rendered according to the request result of the product detail interface:

          

Solution:

  • Technical selection: MINA framework + Vant Syndrome

  • Development idea of componentization:

1. The product details page is divided into two components: the product details component and the bottom navigation component. The bottom navigation component is divided into bottom navigation tool component and product SKU component.

2. In the bottom navigation component, add the click event for the Add shopping cart button, write the product SKU component using the Popup pop-up layer component provided by the likes, and use a variable to control the show and hide of the pop-up layer. You can also do some initialization in the close callback of the pop-up layer component, which will be detailed in subsequent requirements.

  • Rendering of product SKU components:

1. Based on the product details interface request results, use versatile Flex and the stepper component layout provided by Yozan.

2. Special attention should be paid to button rendering under color and size classification. If you look at the variants data returned by the Details interface, you will find that some of the colour and size data returned will overlap. Therefore, the returned data cannot be looping directly. The returned data needs to be reprocessed, otherwise part of the data displayed in color and size categories will have data overlap, which is obviously not reasonable. For example, the test for the colour will be repeated twice. Variants’ figures 4, 6 and 11 show that the size “-42” will be repeated twice.

<view class="attribute-warp">color</view>
<view class="flex-button-warp stepper-warp">
    <van-button  
        wx:for="{{colors}}"
        wx:key="id" 
        custom-class="color {{selectedColorId === item.id ? 'selected' : '' }}"
        bindtap="handleColorClicked"
        data-color="{{item}}"
        disabled = "{{! m1.hasTag(availableColorArray, item.color)}}"
    >
        {{item.color}}
    </van-button>       
</view> 
Copy the code
// Listen for the incoming product data. If the request succeeds and the details are available, filter the repeated color classification attributes with the set
  properties: {
    product: {
      type: Object.observer: function (data) {
        if(Object.keys(data).length > 0) {
          this.onClose()
          const colors = Array.from(new Set(this.data.product.variants.map(item= > item.color))).map((color, index) = > ({
            color,
            id: index
          }))
          this.setData({
            colors
          })
        }
      }
    }
  }
Copy the code

Requirement 2: Display properties for selected products

This little requirement actually consists of three parts, all of which are triggered when clicked:

1. Display the classification attributes of selected products (triggered when clicked) :

If no product classification attribute is selected, the prompt is please select product attribute. If a product category attribute is selected, the selected product category attribute is displayed in the product SKU component.

          


          

Solution:

Since the classification attributes of selected products are displayed under different conditions, the most obvious ones to think of are wx:if and Wx :else in MINA framework. So you can set the judgment condition, as long as the two classification attributes are not selected, it prompts you to select the product attributes, otherwise the selected product classification attributes will be displayed. So how do you get the value inside the button? In fact, in the click event of product classification attribute, pass in the current click object through the custom attribute, and then get the value inside to display on the product SKU.

<view class="message">
    <text wx:if="{{ selectedColor === '' && selectedSize === ''}}">Please select product properties</text>
    <text wx:else>Selected properties: {{selectedColor}} {{selectedSize}}</text>
</view>
Copy the code

2. Display the price attribute of the selected product (triggered when clicking) :

If no classification attribute of the product is selected or only color attribute or size attribute is selected, the displayed product price is the LOwest_price field in the returned product details interface. If the selected category attributes include colour attributes and size attributes, the categories will be looked up in the variants object array in the returned product details interface. If the price of the corresponding classification attribute can be found, the corresponding price is returned; otherwise, an error is reported indicating that the price field does not exist. Therefore, it is necessary to do the attribute association of click classification attribute, which will be solved in Requirement 3.

Solution:

The logic goes like this: listen for incoming product data and as soon as it is received or the pop-up layer is closed, initialize currentPrice and assign the value of product.lowest_price to currentPrice. In the click event of product classification attribute, judge whether the product color classification attribute and size classification attribute are selected. If not, the lowest_price field in the returned product details interface is displayed; Variants will find the price of the selected category attribute if they do not find it and will report an error that the price field does not exist. Therefore, it is necessary to do the attribute association of click classification attribute, which will be solved in Requirement 3.

<view class="product-price-warp">
    <view>RMB</view>
    <view class="product-price">{{ currentPrice }}</view>
</view>
Copy the code

3. Switch the logic of different classification attributes (triggered when clicked) :

Take click color classification properties for example. Before clicking on the color classification attribute, there are actually three cases (there are two cases that have the same result and can be merged into one category) :

  • No color category attribute has been selected, clicking on it will directly activate the style of the selected color category attribute.

          

  • If a color classification attribute is selected and the selected color classification attribute is consistent with the color classification attribute to be clicked, clicking the color classification attribute style will be deactivated.

          

  • If a color category attribute is selected and the selected color category attribute is not consistent with the color category attribute to be clicked, clicking this button will cancel the previously activated color category attribute style and activate the currently selected color category attribute style.

          

For example, a variable can be used to determine whether the current color classification attribute is selected, and the id of the currently selected color classification attribute. This allows comparison with the id of the currently clicked object to handle different judgment logic. This is why when you reconstruct the data returned by the detail interface in Requirement 1, you assign them ids in addition to the classification attribute names. Of course, in the final callback that closes the pop-up layer, you need to reset the text and ID of the selected color category attribute and the text and ID of the selected size category attribute.

handleColorClicked(e) {
  // If no if condition is used, the button can still be clicked
  if (!this.data.availableColorArray.includes(e.currentTarget.dataset.color.color)) return
  // Compare the id of the currently selected object with the id of the currently clicked object
  //1. If selectedSizeId === -1, no color class attribute is currently selected. At this time, the click needs to transfer the ID and value of the current click object. The id transfer is for the next comparison, and the value transfer is to display the classification attributes of the selected products.
  //2. If the selected color category attribute ID is inconsistent with the id of the currently clicked object. Clicking at this point will not only cancel the selected color category attribute, but also activate the style of the currently selected color category (the first case is the same as the second case).
  //3. If the id of the selected color category attribute is the same as the ID of the currently clicked object. Click this button to cancel the selected color classification attribute, which is equivalent to clearing the selected color classification attribute.
  if(this.data.selectedColorId.length === 0 || this.data.selectedColorId ! == e.currentTarget.dataset.color.id) {const availableSizeArray = this.data.product.variants.filter(item= > item.color === e.currentTarget.dataset.color.color).map(item= > item.size)
    this.setData({
      selectedColorId: e.currentTarget.dataset.color.id,
      selectedColor: e.currentTarget.dataset.color.color,
      availableSizeArray
    })
  } else if(this.data.selectedColorId === e.currentTarget.dataset.color.id) {
    const availableSizeArray = this.data.sizes.map(item= > item.size)
    this.setData({
      selectedColorId: -1.selectedColor: ' ',
      availableSizeArray
    })
  }
  this.setSelectedPrice()
}
Copy the code

Requirement 3: Deal with property associations for clicking different categories buttons

Click the color classification attribute of the product, the size classification attribute of the product will be screened, available display, disabled gray display and can not be clicked; After deselecting the corresponding color classification attribute, all the product size classification attribute will be restored to the available state. The same is true for clicking the product’s size classification attribute, so it is necessary to make attribute association for clicking different classification buttons.

Solution:

Click color classification attribute for example, talk about the product size classification attribute screening principle. After clicking the color category attribute, you will need to find the variant size that includes the current color category attribute in the variants data of the product. One or more sizes is the available size attribute set after clicking on the color classification attribute, and the negative one is the disabled size attribute set. Because the code logic for the click event was pasted in Requirement 2, it will not be pasted here.

There are two things to note here: 1. After iterating through the classification data using van-button, the disabled attribute is disabled by filtering out the available attribute set and then inverting it. If the current click color attribute is inverted first, and then select the disabled size attribute set from the available color attribute set, it may cause that the disabled size attribute set contains the current click color attribute corresponding to the available size attribute set; 2. Because wechat applet does not support parameter passing in functions, it is necessary to use WXS language to judge the disabled condition, which cannot be used! AvailableColorArray. Includes (item. Color).

// WXS does not support ES6 syntax
The reason for using two commas is that there may be cases where the name contains the test and the test color (one color contains the name of another color)
// We can loop through the array to see if the names are the same, and return true if they are
function hasTag(tags, name) {
  var testStr = ', ' + tags.join(', ') + ', ' 
  return testStr.indexOf(', ' + name + ', ') != -1
}

/ / export
module.exports = {
  hasTag: hasTag
}
Copy the code
<view class="attribute-warp">color</view>
<view class="flex-button-warp stepper-warp">
    <van-button  
        wx:for="{{colors}}"
        wx:key="id" 
        custom-class="color {{selectedColorId === item.id ? 'selected' : '' }}"
        bindtap="handleColorClicked"
        data-color="{{item}}"
        disabled = "{{! m1.hasTag(availableColorArray, item.color)}}"
    >
        {{item.color}}
    </van-button>       
</view> 
Copy the code

Code integration

Utils \ helper. WXS:

// Es6 syntax is not supported
function hasTag(tags, name) {
  var testStr = ', ' + tags.join(', ') + ', ' 
  return testStr.indexOf(', ' + name + ', ') != -1
}

/ / export
module.exports = {
  hasTag: hasTag
}
Copy the code

Components \ common \ popup \ index WXML:

<wxs
  src=".. /.. /.. /utils/helper.wxs"
  module="m1"
/>
<van-popup
  closeable
  show="{{ visible }}"
  position="bottom"
  custom-class="popup"
  bind:close="onClose"
>
    <view class="flex-warp">
        <view class="product-picture">
            <base-image
                width="182rpx"
                height="182rpx"
                class="product-image"
                src="{{product.images[0]}}"
            /> 
        </view>
        <view class="product-explain">
            <view class="product-price-warp">
                <view>RMB</view>
                <view class="product-price">{{ currentPrice }}</view>
            </view>
            <view class="message">
                <text wx:if="{{ selectedColor === '' && selectedSize === ''}}">Please select product properties</text>
                <text wx:else>Selected properties: {{selectedColor}} {{selectedSize}}</text>
            </view>
        </view>    
    </view> 
    <view class="attribute-warp">color</view>
    <view class="flex-button-warp stepper-warp">
        <van-button  
            wx:for="{{colors}}"
            wx:key="id" 
            custom-class="color {{selectedColorId === item.id ? 'selected' : '' }}"
            bindtap="handleColorClicked"
            data-color="{{item}}"
            disabled = "{{! m1.hasTag(availableColorArray, item.color)}}"
        >
            {{item.color}}
        </van-button>       
    </view> 
    <view class="attribute-warp">size</view>
    <view class="flex-button-warp stepper-warp">
        <van-button  
            wx:for="{{sizes}}"
            wx:key="id" 
            custom-class="color {{selectedSizeId === item.id ? 'selected' : '' }}"
            bindtap="handleSizeClicked"
            data-size="{{item}}"
            disabled = "{{! m1.hasTag(availableSizeArray, item.size)}}"
        >
            {{item.size}}
        </van-button>    
    </view> 
    <view class="attribute-warp">The number of</view>
    <view class="stepper-warp">
        <van-stepper 
            max="10000"
            value="{{ quantity }}" 
            async-change 
            bind:change="onChange" 
            input-class="stepper-input"
            plus-class="stepper-operation"
            minus-class="stepper-operation"
        />
    </view>
    <view bindtap="handleConfirmed" class="confirm-button">determine</view>
</van-popup>

Copy the code

Components \ common \ popup \ index WXSS:

.popup {
    background: #FFFFFF;
    box-shadow: 0px -4px 8px 0px rgba(0.0.0.0.06);
    border-radius: 20px 20px 0px 0px;
}

.flex-warp {
    margin: 41rpx 17rpx 51rpx 32rpx;
    display: flex;
}

.product-expalin {
    display: flex;
    flex-direction: column;
    margin-left: 17rpx;
}
.product-price-warp {
    display: flex;
    margin-top: 39rpx;
    margin-bottom: 36rpx;
    font-size: 34rpx;
    font-weight: bold;
    color: #FA5151;
}
.message {
    font-size: 30rpx;
    color: #7A7A7A;
}
.attribute-warp {
    font-size: 30rpx;
    color: # 181818;
    margin: 47rpx 0 20rpx 33rpx;
}
.confirm-button {
    margin-top: 62rpx;
    height: 80rpx;
    color: #fff;
    line-height: 80rpx;
    text-align: center;
    font-size:30rpx;
    background: #FA5151;
}
.stepper-input {
    width: 166rpx;
    height: 80rpx;
    font-size: 30rpx;
    color: # 181818;
}
.stepper-operation {
    width: 87rpx;
    height: 80rpx;
    background: #F2F2F2;
    border-radius: 10rpx;
}
.stepper-warp {
    margin-left: 30rpx;
}
.flex-button-warp {
    display: flex;
    flex-wrap: wrap;
}
.color {
    min-width: 200rpx;
    height: 80rpx;
    background: #F2F2F2;
    border-radius: 40rpx;
    font-size: 30rpx;
    color: # 181818;
    text-align: center;
    line-height: 80rpx;
    margin: 20rpx 10rpx 0 0;
}
.product-explain {
    margin-left: 17rpx;
}
.selected {
    background: #FFFFFF;
    border: 2rpx solid # 181818;
}
Copy the code

The components \ common \ popup \ index. Js:

import api from '.. /.. /.. /api/index'

Component({
  /** * Component property list */
  properties: {
    product: {
      type: Object.observer: function (data) {
        if(Object.keys(data).length > 0) {
          this.onClose()
          const colors = Array.from(new Set(this.data.product.variants.map(item= > item.color))).map((color, index) = > ({
            color,
            id: index
          }))
          const sizes = Array.from(new Set(this.data.product.variants.map(item= > item.size))).map((size, index) = > ({
            size,
            id: index
          }))
          this.setData({
            colors,
            sizes
          })
        }
      }
    },
    type: {
      type: String.default: 'add'
    },
    visible: {
      type: Boolean}},/** * The initial data of the component */
  data: {
    quantity: 1.selectedColor: ' '.selectedSize: ' '.currentPrice: 100.colors: {},
    sizes: {},
    selectedColorId: -1.selectedSizeId: -1.availableSizeArray: [].availableColorArray: []},/** * list of component methods */
  methods: {
    getSelectedVariant() {
      if(this.data.selectedColor ! = =' ' && this.data.selectedSize ! = =' ') {
        const variant = this.data.product.variants.find(item= > {
         return item.color === this.data.selectedColor && item.size === this.data.selectedSize
        })
        return variant
      }
    },
    async handleConfirmed() {
      if(! getApp().globalData.token) { wx.navigateTo({url: '/pages/login/login'
        })
        return
      }
      // Check when all are selected
      if(this.data.selectedColor ! = =' ' && this.data.selectedSize ! = =' ') {
        const variant = this.getSelectedVariant()
        const variantId =  this.getSelectedVariant().id
        // If adding to cart, request adding to cart interface
        if(this.data.type === 'add') {
          await api.cart.add({
            product_id: this.data.product.id,
            variant_id: variantId,
            quantity: this.data.quantity
          }).then(() = > {
            wx.showToast({
              title: 'Added to cart'.icon: 'success'.duration: 2000
            }); 
            const cartList = wx.getStorageSync('cartList').length > 0 ? wx.getStorageSync('cartList') : [] 
            const currentProduct = cartList.find( item= > {
              if(item && item.variant && item.variant.id) {
                return item.product.id === this.data.product.id && item.variant.id === variantId
              }
            })
            if(! currentProduct) { cartList.push({product: this.data.product,
                variant,
                is_selected: true.quantity: this.data.quantity
              })
            } else {
              const index = cartList.indexOf(currentProduct)
              cartList[index].quantity += this.data.quantity
            }
            wx.setStorageSync('cartList',cartList)
          }) 
        } else {
          // If it is a one-click order, save the data that the interface needs to call
          getApp().globalData.oneClickOrderData = {
            product_id: this.data.product.id,
            variant_id: variantId,
            merchant_id: this.data.product.merchant.id
          }
          wx.navigateTo({
            url: '/pages/cart/submit-order/index? type=buy'
          });
        }
        // Called when all are selected and verified successfully
        this.onClose()
      } else {
        // called when not all are selected
        wx.showToast({
          title: 'Please select product attribute'.icon: 'none'.duration: 2000}); }},onChange(event) {
      if(this.data.selectedSize === ' ' || this.data.selectedColor === ' ') {
        wx.showToast({
          title: 'Please select product attribute'.icon: 'none'.duration: 2000
        }); 
        return
      }
      this.setData({
        quantity: event.detail
      })
    },
    onClose() {
      const availableSizeArray = this.data.product.variants.map(item= > item.size)
      const availableColorArray = this.data.product.variants.map(item= > item.color)
      wx.nextTick(() = > {
        this.setData({
          currentPrice: this.data.product.lowest_price,
          visible: false.quantity: 1.selectedColor: ' '.selectedSize: ' '.selectedColorId: -1.selectedSizeId: -1,
          availableSizeArray,
          availableColorArray
        })
      });
      this.triggerEvent('close')},setSelectedPrice() {
      let currentPrice
      if(this.data.selectedColor ! = =' ' && this.data.selectedSize ! = =' ') {
        currentPrice = this.data.product.variants.find(item= > item.size === this.data.selectedSize && item.color === this.data.selectedColor).price
      } else {
        currentPrice = this.data.product.lowest_price
      }
      this.setData({
        currentPrice
      }) 
    },
    handleSizeClicked(e) {
      // If no if condition is used, the button can still be clicked
      if (!this.data.availableSizeArray.includes(e.currentTarget.dataset.size.size)) return
      if(this.data.selectedSizeId === -1 || this.data.selectedSizeId ! == e.currentTarget.dataset.size.id) {const availableColorArray = this.data.product.variants.filter(item= > item.size === e.currentTarget.dataset.size.size).map(item= > item.color)
        this.setData({
          selectedSizeId: e.currentTarget.dataset.size.id,
          selectedSize: e.currentTarget.dataset.size.size,
          availableColorArray
        })
      } else if(this.data.selectedSizeId === e.currentTarget.dataset.size.id) {
        const availableColorArray = this.data.colors.map(item= > item.color)
        this.setData({
          quantity: 1.selectedSizeId: -1.selectedSize: ' ',
          availableColorArray
        })
      }
      this.setSelectedPrice()
    },
    // There is a one-to-many problem in disabling. You cannot use the disabled item directly. You can only use the disabled item and then reverse the disabled item
    handleColorClicked(e) {
      // If no if condition is used, the button can still be clicked
      if (!this.data.availableColorArray.includes(e.currentTarget.dataset.color.color)) return
      if(this.data.selectedColorId.length === 0 || this.data.selectedColorId ! == e.currentTarget.dataset.color.id) {const availableSizeArray = this.data.product.variants.filter(item= > item.color === e.currentTarget.dataset.color.color).map(item= > item.size)
        this.setData({
          selectedColorId: e.currentTarget.dataset.color.id,
          selectedColor: e.currentTarget.dataset.color.color,
          availableSizeArray
        })
      } else if(this.data.selectedColorId === e.currentTarget.dataset.color.id) {
        const availableSizeArray = this.data.sizes.map(item= > item.size)
        this.setData({
          quantity: 1.selectedColorId: -1.selectedColor: ' ',
          availableSizeArray
        })
      }
      this.setSelectedPrice()
    }
  }
})
Copy the code

The components \ common \ popup \ index. Json:

{
  "component": true."usingComponents": {}}Copy the code

Write in the last

GitLens — Git Supercharged. This plugin allows you to check your commit records on Git. Write it here as a memo. Because the code in this paper is written in a hurry, the logic is still complex, there is a lot of repeated code, partners with time and interest can use the idea of componentization to rewrite the code. Young and frivolous, always think the world, can not do, time wasted, finally feel the world has an end. Young ignorance, fool think good friends, sincerity, time flow, eventually sigh social people hide a sword in a smile. Society is very big, the heart is very complicated, along the way, carry the blackest iron pot, make the biggest joke. However, it is a blessing in disguise. Besides, I am also glad that I met such a good mother. I hope I can keep my heart and not be changed by the society.


Learning & Summarizing