Liquid (skio-plan-picker.liquid)

The Plan Picker Liquid file renders the entire plan picker element server-side prior to page load, allowing nearly zero performance impact on your store. No delayed loading, no flashing on load!

Usage

Usage required:

{% render 'skio-plan-picker' %}

Usage conditionally required:

{% render 'skio-plan-picker', product: product, form_id: form_id, key: key %}

Usage optional:

{% render 'skio-plan-picker', product: product, key: key, form_id: form_id, start_onetime: false, onetime_first: false, discount_format: 'absolute' %}

Parameters

product

Required on non-product pages, snippets only have access to the globally available product variable by default. If the product variable is defined locally, for example inside a for each product in a collection, the snippet will not have access to the product variable unless explicitly passed.

form_id

Required when plan picker isn't nested in a form[action="/cart/add"]. The form_id has to match the id on the form element. It is used to connect a form field that is outside of a form to it using the form attribute.

<input form="form-123"/>

<form id="form-123">
  ...
</form>

key

Passed in for uniqueness; defaults to product.id this is particularly useful when using multiple plan pickers on the same page.

start_onetime

for choosing whether the one-time option is selected on page load; defaults to true

onetime_first

for choosing whether the one-time option displays above the selling plan option; defaults to true

discount_format

for formatting selling plan discounts; either 'percent' or 'absolute'; defaults to 'percent'

Page-load & Asynchronous Javascript

In order to have a near-zero impact on store performance and page load speed, the plan picker runs a script to asynchronously load our larger JS file after the DOMContentLoaded event. An eventListener is set on the window to wait until mouseover or focus on one of the following: the plan picker element (fieldset[skio-plan-picker]), a product variant selector on the page ([name="id"]), or .product-form__input for Dawn-based themes. Once triggered, it fires the window.SkioLoadJS function, which injects a <script> tag for the plan picker JS file into <head>, initializing the plan picker's JS functionality. The script will only auto-inject once, and any instances of the plan picker element that load after the initial injection will be auto-initialized once loaded.

skio-plan-picker.liquid
  {% comment %} SCRIPT {% endcomment %}
  <script>
    const selector = 'fieldset[skio-plan-picker], [id="name"], .product-form__input';
    // Only loads the script if a plan picker is on the page, and only once
    // the main element is hovered over. doesn't load multiple times if there is are multiple plan pickers.
    // Defer loading to reduce load impact
    if (window.skio_plan_picker_script_load === undefined) {
      window.skio_plan_picker_script_load = true;

      const script = document.createElement('script');
      script.type = 'text/javascript';
      script.defer = true;
      script.src = '{{ 'skio-plan-picker.js' | asset_url }}';

      window.SkioLoadJS = (e) => {
        document.head.append(script);

        document.querySelectorAll(selector).forEach(el => {
          el.removeEventListener('mouseover', window.SkioLoadJS);
          el.removeEventListener('focus', window.SkioLoadJS, { capture: true });
        });
      }

      window.addEventListener('DOMContentLoaded', () => {
        document.querySelectorAll(selector).forEach(el => {
          el.addEventListener('mouseover', window.SkioLoadJS);
          el.addEventListener('focus', window.SkioLoadJS, { capture: true });
        });
      });
    }

    // Handling async load of skio-plan-picker.liquid
    if (window.SkioPlanPickerAutoInit) {
      window.SkioPlanPickerAutoInit();
    } else {
      document.querySelectorAll(selector).forEach(el => {
        el.addEventListener('mouseover', window.SkioLoadJS);
        el.addEventListener('focus', window.SkioLoadJS, { capture: true });
      });
    }
  </script>

CSS Styles

The CSS file skio-plan-picker.css is included immediately after the script, and should be placed in the assets/ folder of your theme. You can make changes to this file if you would like to customize the CSS stylings used.

skio-plan-picker.liquid
  {{ 'skio-plan-picker.css' | asset_url | stylesheet_tag }}

Optional Liquid Parameters

Liquid logic determines the current_variant prior to page load via a URL parameter, as well as handling the optional snippet parameters: key, start_onetime, onetime_first, and discount_format .

skio-plan-picker.liquid
{% liquid
    assign current_variant = product.selected_or_first_available_variant
    unless key
      assign key = product.id
    endunless
    if start_onetime == nil
      assign start_onetime = true
    endif
    if product.requires_selling_plan
      assign start_onetime = false
    endif
    if product.selected_selling_plan
      assign start_onetime = false
    endif
    if onetime_first == nil
      assign onetime_first = true
    endif
    if discount_format == nil
      assign discount_format = 'percent'
    endif
  %}

Plan Picker Element

The fieldset.skio-plan-picker is the plan picker element. It assigns a unique key as an identifier ([skio-plan-picker="key"]) to differentiate between other plan pickers on the page. [skio-auto-init] is an identifier used within the larger JS file.

skio-plan-picker.liquid
<fieldset class="skio-plan-picker" skio-plan-picker="{{ key }}" skio-auto-init skio-discount-format="{{ discount_format }}">
    ...
</fieldset>

Product JSON

In order to load all product JSON data in the JS file, the below line is required:

skio-plan-picker.liquid
{% comment %} PRODUCT JSON {% endcomment %}
<script type="text/json" skio-product-json>{{ product | json }}</script>

Selling Plan and Discount logic

To properly display the plan picker on page load, Liquid is used to determine the correct selling plan and related discount (if applicable). Using the Product object's selected_selling_plan variable, it checks if there is a selling_plan_id specified in the URL. If so, that is the selling plan to render; else, it defaults to the first compatible selling plan. The selling_plan_id is stored as the value in a hidden input[name="selling_plan"], which is connected to the form[action="/cart/add"] via being embedded in the form or including the form_id Liquid parameter. With the current selling plan determined, Liquid is used to calculate the discounted price (if any) of the selling plan checking the discount_format Liquid parameter to determine how to render the discounted value (i.e. % or using the Liquid money filter). The discount text to display in the cart and at checkout is stored as the value in a hidden input[name=โ€œproperties[Discount]โ€] , which is disabled unless the subscription option is selected and is connected to the form[action="/cart/add"] via being embedded in the form or including the form_id Liquid parameter.

skio-plan-picker.liquid
  {% comment %} SELLING PLAN {% endcomment %}
  {% liquid
    if product.selected_selling_plan
      assign selected_selling_plan_id = product.selected_selling_plan.id
    else
      assign selected_selling_plan_id = product.selling_plan_groups.first.selling_plans.first
    endif
  %}
  <input name="selling_plan" type="hidden" value="{% unless start_onetime %}{{ selected_selling_plan_id }}{% endunless %}" 
    {% if form_id %}form="{{ form_id }}"{% endif %}
  />
  
  {% liquid 	
    if product.selected_selling_plan 	
      assign price_adjustment = product.selected_selling_plan.price_adjustments.first	
    elsif start_onetime == false	
      assign price_adjustment = product.selling_plan_groups.first.selling_plans.first.price_adjustments.first	
    endif	
    if price_adjustment	
      case price_adjustment.value_type	
        when 'percentage'	
          assign discount_percent = price_adjustment.value	
          assign discount_absolute = current_variant.price | times: price_adjustment.value | divided_by: 100.0	
        when 'fixed_amount'	
          assign discount_percent = price_adjustment.value | times: 1.0 | divided_by: current_variant.price | times: 100.0	
          assign discount_absolute = price_adjustment.value	
        when 'price'	
          assign discount_percent = current_variant.price | minus: price_adjustment.value | times: 1.0 | divided_by: current_variant.price | times: 100.0	
          assign discount_absolute = current_variant.price | minus: price_adjustment.value	
      endcase	
      if discount_percent == 0	
        assign discount_text = ''	
      elsif discount_format == 'percent' or discount_format == blank	
        assign discount_text = discount_percent | append: '%'	
      else	
        assign discount_text = discount_absolute | money	
      endif	
    endif	
  %}	
  <input name="properties[Discount]" type="hidden" value="{{ discount_text }}" {% if start_onetime %}disabled{% endif %}	
    {% if form_id %}form="{{ form_id }}"{% endif %}
  />

One-time and Subscription Radio buttons

The primary HTML and Liquid of the Onetime and Subscription radio buttons are captured as Liquid variables, the order of which gets determined by the value of the onetime_first Liquid parameter. The onetime radio button isnโ€™t rendered if the product requires a selling plan regardless of the other Liquid parameters. The entire snippet isn't rendered if the product has no selling plans. The subscription selector uses Liquid logic to generate the selling plan selector for the variant loaded on page load. The same discount logic as earlier determines the amount to display, along with the slashed out non discounted price.

skio-plan-picker.liquid
  {% comment %} ONE TIME {% endcomment %}
  {% capture onetimeHTML %}
    {% unless product.requires_selling_plan %}
      <div class="skio-group-container skio-group-container--available{% if start_onetime %} skio-group-container--selected{% endif %}" skio-group-container>
        <input id="skio-one-time-{{ key }}" class="skio-group-input" name="skio-group-{{ key }}" 
          type="radio" value="" skio-one-time {% if start_onetime %}checked{% endif %}>
        <label class="skio-group-label" for="skio-one-time-{{ key }}">
          <div class="skio-radio-svg__container">
            <svg class="skio-radio-svg skio-radio-svg--unchecked" height="18" width="18" viewBox="0 0 24 24" aria-hidden="true" aria-label="" role="img"><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.6 0 12 0zm0 22.2C6.45 22.2 1.8 17.7 1.8 12 1.8 6.3 6.3 1.8 12 1.8c5.7 0 10.2 4.5 10.2 10.2 0 5.7-4.65 10.2-10.2 10.2z"></path></svg>
            <svg class="skio-radio-svg skio-radio-svg--checked" height="18" width="18" viewBox="0 0 24 24" aria-hidden="true" aria-label="" role="img"><path d="M12 0a12 12 0 100 24 12 12 0 000-24zM6.37 11.61a1.25 1.25 0 011.76 0l2.37 2.36 5.37-5.35a1.25 1.25 0 011.76 1.76L10.5 17.5l-4.13-4.13a1.25 1.25 0 010-1.76z"></path></svg>
          </div>
          <div>
            One-time purchase:
            <span skio-onetime-price>{{ current_variant.price | money }}</span>
          </div>
        </label>
      </div>
    {% endunless %}
  {% endcapture %}

  {% comment %} SUBSCRIPTIONS {% endcomment %}
  {% capture subscriptionHTML %}
    {% assign firstSelected = false %}
    {% for group in product.selling_plan_groups %}
      {% if group.name == 'Subscription' or group.name == 'Prepaid' %}
        
        {% liquid
          assign group_available = false
          for allocation in current_variant.selling_plan_allocations
            if allocation.selling_plan_group_id == group.id
              assign group_available = true
            endif
          endfor

          assign group_selected = false
          unless start_onetime
            if product.selected_selling_plan
              if group.selling_plan_selected
                assign group_selected = true
              endif
            elsif firstSelected == false and group_available == true
              assign group_selected = true
              assign firstSelected = true
            endif
          endunless
        %}
        
        <div class="skio-group-container{%- if group_available %} skio-group-container--available{% endif -%}
          {%- if group_selected %} skio-group-container--selected{% endif %}" skio-group-container>
          
          <input id="skio-selling-plan-group-{{ forloop.index }}-{{ key }}" class="skio-group-input"
            name="skio-group-{{ key }}"
            type="radio" value="{{ group.id }}" {% if group_selected %}checked{% endif %}
            skio-selling-plan-group="{{ group.id }}">
          
          <label class="skio-group-label" for="skio-selling-plan-group-{{ forloop.index }}-{{ key }}">
            <div class="skio-radio-svg__container">
              <svg class="skio-radio-svg skio-radio-svg--unchecked" height="18" width="18" viewBox="0 0 24 24" aria-hidden="true" aria-label="" role="img"><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.6 0 12 0zm0 22.2C6.45 22.2 1.8 17.7 1.8 12 1.8 6.3 6.3 1.8 12 1.8c5.7 0 10.2 4.5 10.2 10.2 0 5.7-4.65 10.2-10.2 10.2z"></path></svg>
              <svg class="skio-radio-svg skio-radio-svg--checked" height="18" width="18" viewBox="0 0 24 24" aria-hidden="true" aria-label="" role="img"><path d="M12 0a12 12 0 100 24 12 12 0 000-24zM6.37 11.61a1.25 1.25 0 011.76 0l2.37 2.36 5.37-5.35a1.25 1.25 0 011.76 1.76L10.5 17.5l-4.13-4.13a1.25 1.25 0 010-1.76z"></path></svg>
            </div>
            <div>
              <span>
                {%- if group.name == 'Subscription' -%}
                  Subscription
                {%- else -%}
                  Prepaid
                {%- endif -%}
              </span>

              {% liquid 
                for selling_plan in group.selling_plans
                  assign selected = false
                  if group.selling_plan_selected
                    if product.selected_selling_plan.id == selling_plan.id
                      assign selected = true
                    endif
                  elsif forloop.first
                    assign selected = true
                  endif
  
                  if selected
                    assign price_adjustment = selling_plan.price_adjustments.first
                    
                    case price_adjustment.value_type
                      when 'percentage'
                        assign discount_percent = price_adjustment.value
                        assign discount_absolute = current_variant.price | times: price_adjustment.value | divided_by: 100.0
                      when 'fixed_amount'
                        assign discount_percent = price_adjustment.value | times: 1.0 | divided_by: current_variant.price | times: 100.0
                        assign discount_absolute = price_adjustment.value
                      when 'price'
                        assign discount_percent = current_variant.price | minus: price_adjustment.value | times: 1.0 | divided_by: current_variant.price | times: 100.0
                        assign discount_absolute = current_variant.price | minus: price_adjustment.value
                    endcase
                    
                    if discount_percent == 0
                      assign discount_text = ''
                    elsif discount_format == 'percent' or discount_format == blank
                      assign discount_text = discount_percent | append: '%'
                    else
                      assign discount_text = discount_absolute | money
                    endif
  
                    assign price = current_variant.price | minus: discount_absolute
                  endif
                endfor
              %}
              
              <span skio-discount>{{ discount_text }}</span>: <span skio-subscription-price>{{ price | money }}</span>
  
              <div class="skio-frequency">
                <div>
                  Delivery {% if group.selling_plans.size == 1 %}{{ group.selling_plans[0].options[0].value }}{% endif %}
                </div>
                <select skio-selling-plans="{{ group.id }}" {% if group.selling_plans.size == 1 %}style="display: none;"{% endif %}>
                  {% for plan in group.selling_plans %}
                    {% liquid 
                      assign selected = false
                      if product.selected_selling_plan
                        if product.selected_selling_plan.id == plan.id
                          assign selected = true
                        endif
                      elsif forloop.first
                        assign selected = true
                      endif
                    %}
                    <option value="{{ plan.id }}" {% if selected %}selected{% endif %}>{{ plan.options[0].value }}</option>
                  {% endfor %}
                </select>
              </div>
            </div>
          </label>
        </div>
      {% endif %}
    {% endfor %}
  {% endcapture %}

After the Liquid variables have been captured, there is a check of the onetime_first Liquid parameter; if true, the one-time option is rendered first, else the subscription option is rendered first.

skio-plan-picker.liquid
  {% if onetime_first == true %}
    {{ onetimeHTML }}
    {{ subscriptionHTML }}
  {% else %}
    {{ subscriptionHTML }}
    {{ onetimeHTML }}
  {% endif %}

Last updated