How to "Hook" OptionSelectors() Callback function?

Excursionist
44 0 6

Hi Shopifiers.

We are developing an App that needs to do something every time a shop visitor selects a Variant on a Product page. Right now we have to look at each Shop's theme template files, find the "OptionSelectors" callback function (e.g. "selectCallback"), manually edit the theme file (e.g. "product.liquid") and  jam our code into the callback function, e.g.: 

<script>
  var selectCallback = function(variant, selector) {

     * Template-specific code here

     * Jam our app code in here
                                      
  };
</script>

We would like to accomplish the same thing, without having to manually edit every shop's theme files, e.g. by "hooking" or "chaining" onto the OptionSelectors() Callback function eihter in the "front-end" (client-side, JavaScript) environment or on the server side.

Does anyone know how to do this?

Thanks.

 

 

2 Likes
New Member
4 0 0

Hi there, I would also be very interested if anyone has a good answer to this.

My feeling right now is that it is not possible independent from the theme. There seems to be no common functionality across themes to do this, but I'd love to be told better.

I'm the developer of the Variant Image Penguin app and we used to do it in this way by just overriding the onVariantChange function of the historystate.

if (Shopify.OptionSelectors != undefined) {
            Shopify.OptionSelectors.HistoryState.prototype.onVariantChange = function (t, e, n) {
                try {
                    this.browserSupports() && (!t || n.initialLoad || n.popStateCall || Shopify.setParam("variant", t.id));
                }
                catch (err) {

                }
                if (t) {
                    if (<%= enabled%>) {
                        //only do callback when mappings are loaded
                        VIG.variantsInfoReady.done(function () {
                            VIG.vig_VariantChange_Callout(t.id);
                        });
                    }
                }
            }
        }

However, things have changed and it seems like this isn't used anymore by new themes.

The newest version of brooklyn for example uses events for this and my feeling is that this is the best approach and shopify should really push theme developers to include the triggering of certain events in their themes so that app developers can hook into them:

/**
     * Trigger event when variant image changes
     *
     * @param  {object} variant - Currently selected variant
     * @return {event}  variantImageChange
     */
    _updateImages: function(variant) {
      var variantImage = variant.featured_image || {};
      var currentVariantImage = this.currentVariant.featured_image || {};

      if (!variant.featured_image || variantImage.src === currentVariantImage.src) {
        return;
      }

      this.$container.trigger({
        type: 'variantImageChange',
        variant: variant
      });
    },

We are currently reacting on this with:

$(document).on('variantImageChange', function (event) {
                  VIG.vig_VariantChange_Callout(event.variant.id);
              });

 But since this is not widespread yet it's basically worthless and we had to get back to inserting our calls in the selectCallback as well - annoying.

 

Cheers,

Dave

0 Likes
Excursionist
41 0 8

Hi there, I would also be very interested if anyone has a good answer to this.

My feeling right now is that it is not possible independent from the theme. There seems to be no common functionality across themes to do this, but I'd love to be told better.

I'm the developer of the Variant Image Penguin app and we used to do it in this way by just overriding the onVariantChange function of the historystate, call the original code, then ours.

if (Shopify.OptionSelectors != undefined) {
            Shopify.OptionSelectors.HistoryState.prototype.onVariantChange = function (t, e, n) {
                try {
                    this.browserSupports() && (!t || n.initialLoad || n.popStateCall || Shopify.setParam("variant", t.id));
                }
                catch (err) {

                }
                if (t) {
                    if (<%= enabled%>) {
                        //only do callback when mappings are loaded
                        VIG.variantsInfoReady.done(function () {
                            VIG.vig_VariantChange_Callout(t.id);
                        });
                    }
                }
            }
        }

However, things have changed and it seems like this isn't used anymore by new themes.

The newest version of brooklyn for example uses events for this and my feeling is that this is the best approach and shopify should really push theme developers to include the triggering of certain events in their themes so that app developers can hook into them:

/**
     * Trigger event when variant image changes
     *
     * @param  {object} variant - Currently selected variant
     * @return {event}  variantImageChange
     */
    _updateImages: function(variant) {
      var variantImage = variant.featured_image || {};
      var currentVariantImage = this.currentVariant.featured_image || {};

      if (!variant.featured_image || variantImage.src === currentVariantImage.src) {
        return;
      }

      this.$container.trigger({
        type: 'variantImageChange',
        variant: variant
      });
    },

We are currently reacting to this with:

$(document).on('variantImageChange', function (event) {
                  VIG.vig_VariantChange_Callout(event.variant.id);
              });

 But since this is not widespread yet it's basically worthless and we had to get back to inserting our calls in the selectCallback as well - annoying.

Cheers,

Dave

1 Like
Excursionist
41 0 8

Hi there, I came up with something which is a bit of a workaround and technically not very elegant, but seems to work so far.

Let me know what you think.

Cheers,

Dave

 VIG.registerSelectChange = function()
      {
          //get all select elements and check options for values
          var variantIDs = Object.keys(VIG.variants);
          var selects = $('select');

          //find the select element which contains the variant ids as values
          //this should be available in every addtocart form, since the variant id 
          //is needed to put the item into the cart
          var variantSelect = findVariantSelect(variantIDs,selects);

          //if found monitor the select element for value changes
          //unfortunately the jQuery val() function which is used by most themes to change the 
          //value of the hidden select field doesn't trigger the change event
          //so we have to check very 500ms and compare the values, this is the not so
          //elegant part.

          if(variantSelect != undefined) {
           console.log("variantSelect found. Registering change event.");
              var lastVal = variantSelect.val();

             VIG.triggerInterval = setInterval(function() {
                  var newVal = variantSelect.val();
                  if(newVal !== lastVal) {
                      // it changed, fire an event or process it
                      variant = {id:newVal};
                      console.log('Variant changed!!!');


           //if the value has changed, trigger our event
                      $(document).trigger({
                          type: 'variantImageChange',
                          variant: variant,
                          internalVIPTrigger: true
                      });
                  }
                  lastVal = newVal;
              }, 500);

          }
      }


 var findVariantSelect = function(variantIDs,selects) {
          var variantSelect = undefined;

          selects.each(function(index,element){
              var options = $(element).children('option'),
                  isVariantSelect = false;

              options.each(function(index,element){
                  if(variantIDs.includes($(element).val())) {
                      isVariantSelect = true;
                  }
              });
              if(isVariantSelect) {
                  variantSelect = $(element);
              }
          });
          return variantSelect;
      }

 

1 Like
Excursionist
44 0 6

Oliver. Sorry about the delay in responding - I was coding hard and then on vacation.

Actually, I think your approach here is very clever. The timer may not be the most "elegant" thing, but as you say, it gets the job done, and it solves the HUGE problem of knowing when the user has changed Variants (and which Variant they've selected) in a theme-independent way.

I am going to try implementing your code and may have more comments/questions later. Thanks very much for this clever solution.

I really wish someone at SHOPIFY would provide a robust, systematic solution to this problem, at least for their own themes!

 

0 Likes
Excursionist
44 0 6

Oliver, do you think it would be possible to put both approaches into the JS code (the "override HistoryState" approach AND the "react to events" approach) and have one JS module that works with BOTH  "old" and "new" themes?

0 Likes
Excursionist
44 0 6

Oliver, I spent a lot of time implementing and testing the "timeout" based approach this weekend (find the variant 'select' element in the add-to-cart form, periodically check it's value looking for changes in the selected variant, etc).

It worked fine for some of the older themes, but I ran into an issue with most of the "new" (updated) Shopify themes: for variants that are out-of-stock (sold out), the new themes set the variant select option to 'disabled' and it has no 'value', so when you try to get the value ( .val() ) of the select element, you get 'null' instead of the id of the selected Variant (see for example the 'option' elements for "Green / Small" and "Green / Medium" below).

<form action="/cart/add" method="post" enctype="multipart/form-data" id="AddToCartForm">
<select name="id" id="productSelect" class="product-single__variants">
  <option  data-sku="ABC1234" value="33878382470">Red / Medium - $ 89.00 USD</option>
  <option  data-sku="ABC1235" value="33878382534">Red / Large - $ 99.00 USD</option>
  <option disabled="disabled">Green / Small - Sold Out</option>
  <option disabled="disabled">Green / Medium - Sold Out</option>
  <option  data-sku="ABC1236" value="33878382726">Green / X-Large - $ 149.00 USD</option>
</select>

<div class="product-single__quantity is-hidden">
  <label for="Quantity">Quantity</label>
  <input type="number" id="Quantity" name="quantity" value="1" min="1" class="quantity-selector">
</div>

<button type="submit" name="add" id="AddToCart" class="btn">
  <span id="AddToCartText">Add to Cart</span>
</button>
</form>

I thought of trying to get the index number of the currently selected Variant 'option' and try to determine the id of the selected Variant positionally (i.e. assume that the order of the Variants in the 'select' element is the same as their order in the product.variants array), but that doesn't work either - the value of the selected option is always -1 for out-of-stock Variants.

You may be able to make this approach work if all you care about are the in-stock Variants, but I don't see how to make it work for the out-of-stock Variants, unless you can figure out a way...?

 

0 Likes