<ul class="product-controls">
    <li class="product-controls__item">
        <button class="product-controls__button product-controls__button--wishlist" data-product-control="wishlist" data-active-variant="202-02" data-product-list="202-02 H202-2M">
      <span class="product-controls__state product-controls__state--add">
        <svg class="icon icon--heart">
  <use xlink:href="/assets/icons/icons.svg#heart"></use>
</svg>

        <span class="product-controls__label product-controls__label--small">Wishlist</span>
        <span class="product-controls__label product-controls__label--medium">Add to Wishlist</span>
        <span class="product-controls__label product-controls__label--large">Add to Wishlist</span>
      </span>
      <span class="product-controls__state product-controls__state--remove">
        <svg class="icon icon--heart-filled">
  <use xlink:href="/assets/icons/icons.svg#heart-filled"></use>
</svg>

        <span class="product-controls__label product-controls__label--small">Wishlist</span>
        <span class="product-controls__label product-controls__label--medium">Remove from Wishlist</span>
        <span class="product-controls__label product-controls__label--large">Remove from Wishlist</span>
      </span>
    </button>
    </li>
    <li class="product-controls__item">
        <button class="product-controls__button" data-active-variant="202-02" data-product-list="">
      <span class="product-controls__state">
        <svg class="icon icon--binoculars">
  <use xlink:href="/assets/icons/icons.svg#binoculars"></use>
</svg>

        <span class="product-controls__label product-controls__label--small">Find a Retailer</span>
        <span class="product-controls__label product-controls__label--medium">Find a Retailer</span>
        <span class="product-controls__label product-controls__label--large">Find a Prescription Retailer</span>
      </span>
    </button>
    </li>
    <li class="product-controls__item">
        <button class="product-controls__button product-controls__button--compare" data-product-control="compare" data-active-variant="202-02" data-product-list="">
      <span class="product-controls__state product-controls__state--add">
        <svg class="icon icon--compare">
  <use xlink:href="/assets/icons/icons.svg#compare"></use>
</svg>

        <span class="product-controls__label product-controls__label--small">Compare</span>
        <span class="product-controls__label product-controls__label--medium">Add to Compare</span>
        <span class="product-controls__label product-controls__label--large">Add to Compare</span>
      </span>
      <span class="product-controls__state product-controls__state--remove">
        <svg class="icon icon--compare-filled">
  <use xlink:href="/assets/icons/icons.svg#compare-filled"></use>
</svg>

        <span class="product-controls__label product-controls__label--small">Remove</span>
        <span class="product-controls__label product-controls__label--medium">Remove from Compare</span>
        <span class="product-controls__label product-controls__label--large">Remove from Compare</span>
      </span>
    </button>
    </li>
</ul>
<ul class="product-controls">
  {{#each controls}}
  <li class="product-controls__item">
    <button class="product-controls__button{{#if control}} product-controls__button--{{control}}{{/if}}"{{#if control}} data-product-control="{{control}}"{{/if}} data-active-variant="{{../current}}" data-product-list="{{ productList }}">
      {{#each states}}
      <span class="product-controls__state{{#if state}} product-controls__state--{{state}}{{/if}}">
        {{render (dynamicVariant 'icons' icon)}}
        <span class="product-controls__label product-controls__label--small">{{label-small}}</span>
        <span class="product-controls__label product-controls__label--medium">{{label-medium}}</span>
        <span class="product-controls__label product-controls__label--large">{{label-large}}</span>
      </span>
      {{/each}}
    </button>
  </li>
  {{/each}}
</ul>
{
  "pageCss": [
    "products"
  ],
  "current": "202-02",
  "controls": [
    {
      "control": "wishlist",
      "productList": "202-02 H202-2M",
      "states": [
        {
          "state": "add",
          "icon": "heart",
          "label-small": "Wishlist",
          "label-medium": "Add to Wishlist",
          "label-large": "Add to Wishlist"
        },
        {
          "state": "remove",
          "icon": "heart-filled",
          "label-small": "Wishlist",
          "label-medium": "Remove from Wishlist",
          "label-large": "Remove from Wishlist"
        }
      ]
    },
    {
      "states": [
        {
          "icon": "binoculars",
          "label-small": "Find a Retailer",
          "label-medium": "Find a Retailer",
          "label-large": "Find a Prescription Retailer"
        }
      ]
    },
    {
      "control": "compare",
      "productList": null,
      "states": [
        {
          "state": "add",
          "icon": "compare",
          "label-small": "Compare",
          "label-medium": "Add to Compare",
          "label-large": "Add to Compare"
        },
        {
          "state": "remove",
          "icon": "compare-filled",
          "label-small": "Remove",
          "label-medium": "Remove from Compare",
          "label-large": "Remove from Compare"
        }
      ]
    }
  ]
}
  • Content:
    (function (window) {
      'use strict';
    
      const activeVariantClass = '.js-product-detail-variant--active';
      const hasVariantClass = 'js-product-controls-button--active';
      const disabledButtonClass = 'js-product-controls-button--disabled';
      const productListAttribute = 'data-product-list';
      const activeVariantAttribute = 'data-active-variant';
      const controlTypeAttribute = 'data-product-control';
      const maximumCompareItems = 4;
    
      function changeProductControl(type, action, variant, products) {
        componentEvents.emitEvent('product-control-change', [{
          controlType: type,
          controlAction: action,
          currentVariant: variant,
          currentProducts: products
        }]);
      }
    
      function getCurrentProductsList(element) {
        const attributeValue = element.getAttribute(productListAttribute).trim();
        return attributeValue ? attributeValue.split(' ') : [];
      }
    
      function isControlListOverLimit(controlType, currentProductsLength) {
        return (controlType === 'compare' && currentProductsLength >= maximumCompareItems);
      }
    
      function handleProductControl(e) {
        const addState = e.target.querySelector('.product-controls__state--add');
        const removeState = e.target.querySelector('.product-controls__state--remove');
        const currentProducts = getCurrentProductsList(e.target);
        const currentVariant = e.target.getAttribute(activeVariantAttribute);
        const controlType = e.target.getAttribute(controlTypeAttribute);
        const compareOverLimit = isControlListOverLimit(controlType, currentProducts.length);
        let controlAction = '';
    
        // Manipulate current products list in this element (add or remove the
        // currentVariant from the currentProducts array).
        //
        // Remove something--it should always be possible to remove something.
        if (window.isHidden(addState)) {
          currentProducts.splice(currentProducts.indexOf(currentVariant), 1);
          controlAction = 'remove';
          // We can safely attempt this every time the remove action happens.
          e.target.classList.remove(disabledButtonClass);
        }
        // (Try to) add something--it's not always going to be possible.
        else if (window.isHidden(removeState)) {
          if (!compareOverLimit) {
            currentProducts.unshift(currentVariant);
            controlAction = 'add';
          }
          else {
            e.target.classList.add(disabledButtonClass);
            controlAction = 'listOverLimit';
          }
        }
    
        // Re-set the attribute with the modified list.
        e.target.setAttribute(productListAttribute, currentProducts.join(' '));
    
        // Alter element state.
        //
        // Specifically, toggle the add/remove toggle any time we are doing a
        // 'remove' action, and whenver this isn't a 'compare' control already
        // over limit.
        //
        // Order is important here; this should come AFTER our other operations on
        // this element.
        if (!compareOverLimit || controlAction === 'remove') {
          e.target.classList.toggle(hasVariantClass);
        }
    
        // Hand off to backend code handler.
        changeProductControl(controlType, controlAction, currentVariant, currentProducts);
      }
    
      function init() {
        const localControls = document.querySelectorAll('button[data-product-control]');
        const activeVariant = document.querySelector(activeVariantClass);
    
        if (activeVariant) {
          const activeVariantId = activeVariant.getAttribute('data-id');
    
          for (let i = 0; i < localControls.length; i++) {
            // Find out what products are already in the wish/compare list.
            const productList = localControls[i].getAttribute(productListAttribute);
            // Convert to array to check each SKU.
            const productListArr = productList.split(' ');
    
            // Set the active variant attribute on the control so the control can
            // pass that on as needed.
            localControls[i].setAttribute(activeVariantAttribute, activeVariantId);
    
            // Add/remove the class showing that the compare/wish list contains THIS
            // element.
            if (productListArr.indexOf(activeVariantId) !== -1) {
              localControls[i].classList.add(hasVariantClass);
            }
            else {
              localControls[i].classList.remove(hasVariantClass);
            }
    
            // Handle clicks on controls.
            localControls[i].addEventListener('click', handleProductControl);
          }
        }
      }
    
      // Allow application JS to reinitialize any instances added with Ajax, etc.
      if (typeof componentEvents !== 'undefined') {
        componentEvents.on('active-product-variant', init);
      }
    
      init();
    
    })(this);
    
  • URL: /components/raw/product-controls/product-controls.js
  • Filesystem Path: src/components/02-components/product-controls/product-controls.js
  • Size: 4.5 KB
  • Content:
    .product-controls {
      @include reset-list;
      @include clearfix;
    }
    
    .product-controls__item {
      width: 33.3333%;
      float: left;
    
      @include breakpoint($breakpoint-xl) {
        width: auto;
    
        &:not(:first-child) {
          margin-left: 30px;
          padding-left: 30px;
          border-left: 1px solid $color-medium-dark;
        }
      }
    }
    
    .product-controls__button {
      @include reset-button;
      @include text-label;
      width: 100%;
      transition: color $transition-standard;
      color: $color-darkest;
      letter-spacing: 1px;
      text-decoration: none;
    
      .icon {
        display: block;
        margin: 0 auto 10px;
        transition: fill $transition-standard;
        fill: $color-medium-dark;
      }
    
      .icon--heart-filled,
      .icon--compare-filled {
        fill: $color-primary;
      }
    
      &:hover,
      &:focus {
        .icon {
          fill: $color-primary;
        }
      }
    
      @include breakpoint($breakpoint-xl) {
        .icon {
          display: inline-block;
          margin: 0 10px 0 0;
          vertical-align: middle;
        }
      }
    }
    
    .product-controls__label {
      display: none;
    }
    
    .product-controls__label--small {
      @include breakpoint($breakpoint-sm-only) {
        display: block;
      }
    }
    
    .product-controls__label--medium {
      @include breakpoint($breakpoint-md $breakpoint-xl - 1) {
        display: block;
      }
    }
    
    .product-controls__label--large {
      @include breakpoint($breakpoint-xl) {
        display: inline-block;
        vertical-align: middle;
      }
    }
    
    .product-controls__state {
      pointer-events: none;
    }
    
    .product-controls__state--add {
      display: block;
    }
    
    .product-controls__state--remove {
      display: none;
    }
    
    .js-product-controls-button--active {
      .product-controls__state--add {
        display: none;
      }
    
      .product-controls__state--remove {
        display: block;
      }
    }
    
  • URL: /components/raw/product-controls/product-controls.scss
  • Filesystem Path: src/components/02-components/product-controls/product-controls.scss
  • Size: 1.7 KB

The Product Control component’s markup requires no changes for implementation or on page-load, except for the HTML attribute data-product-list as noted below.

Product Control HTML Attributes

This component uses HTML data-* attributes to track the state of the Wishlist and Compare product controls. Each control contains the following attribute:

  1. data-product-control
    • This attribute identifies the element as a product control. For example, in the library implementation, ‘Wishlist’ and ‘Compare’ are product controls, but ‘Find a Retailer’ is not.
    • The attribute’s value should be a one-word string such as wishlist or compare.
    • This attribute’s value never changes.
  2. data-active-variant
    • This attribute stores the SKU of the current product variant.
    • It changes each time the visible product variant changes.
    • This attribute’s value is automatically set on page load, and when the currently visible variant changes.
  3. data-product-list
    • This attribute stores a space-separated list of the product SKUs in the Wishlist or Compare list.
    • The control reads the current value of data-active-variant (above) on click, and adds or removes that value from this attribute.
    • This attribute must be pre-pouplated on page load.

Product Control Events

Each time a Product Control is clicked, the component emits an event, product-control-change containing the data shown below. This event and the data it provides can be used to perform actions on back end data.

  • controlType The type of Product Control; in the current implementation, this will be one of either wishlist or compare.
  • controlAction The type of action initiated by the user; in this implementation, this will be one of the following:
    • add: when a product has just been added to a control’s list,
    • remove: when a product has just been removed from a control’s list,
    • listOverLimit: when a product could not be added to a control’s list because the control already has its maximum number of items.
  • currentVariant The SKU of the product that has just been added to or removed from the Wishlist or Compare list.
  • currentProducts The current list of products maintained by the Product Control; this can be used to compare with the current state of the application.