This is the documentation for LemonStand V1, which has been discontinued. You can learn more and upgrade your store here.

LemonStand Version 1 Has Been Discontinued

This documentation is for LemonStand Version 1. LemonStand is now offered as a cloud-based eCommerce platform.
You can try the new LemonStand and learn about upgrading here.

Displaying Product Bundle Items

The task of displaying product bundle items is simplified by Shop_BundleHelper class, which has methods which incapsulate low-level Bundles API. You can find the bundle feature implementation in Simplicity theme. In this article we use simplified code snippets from this theme. Please note that the used markup (for example tables) is not required. You can use any markup in your store. The only requirement is saving the correct control names.

Bundle items section should be implemented in a separate partial, to be updatable with AJAX. Create a new partial and name it shop:bundle. Add this partial to the product page, above the Add to Cart button, and wrap it into a DIV element with a specific identifier:

<div id="product_bundle_items"><? $this->render_partial('shop:bundle') ?></div>
<div id="product_bundle_items">{{ render_partial('shop:bundle') }}</div>

The shop:bundle partial should output a list of product bundle items and render another partial, which output products for each bundle item. Below you will find the shop:bundle partial content:

<? if ($product->bundle_items->count): ?>
  <h3>Bundle items</h3>

  <table>
    <? foreach ($product->bundle_items as $item): ?>
      <tr>
        <th>
          <?= h($item->name) ?>
          <? if ($item->description): ?>
            <p><?= h($item->description) ?></p>
          <? endif ?>
        </th>
        <td><? $this->render_partial('shop:bundle_item_products', array('item'=>$item)) ?></td>
      </tr>
    <? endforeach ?>
  </table>
<? endif ?>
{% if field(product, 'bundle_items').count %}
  <h3>Bundle items</h3>

  <table>
    {% for item in field(product, 'bundle_items') %}
      <tr>
        <th>
          {{ item.name }}
          {% if item.description %}
            <p>{{ item.description }}</p>
          {% endif %}
        </th>
        <td>{{ render_partial('shop:bundle_item_products', {'item': item}) }}</td>
      </tr>
    {% endfor %}
  </table>
{% endif %}

The code iterates through the product bundle item list and displays a table row for each item. In the first column it outputs the bundle item name and description, and in the second column the shop:bundle_item_products is rendered. This partial outputs bundle item products in accordance with the bundle item configuration.

shop:bundle_item_products partial

Create a new partial and name it shop:bundle_item_products. This partial should output products for a specific bundle item, which is passed as the partial parameter (see the item partial parameter in the code snippet above). The partial intensively uses the Shop_BundleHelper class. Partial code:

<? if ($item->control_type == 'dropdown'):
  $selected_item_product = Shop_BundleHelper::get_bundle_item_product_item($item);
  $selected_product = Shop_BundleHelper::get_bundle_item_product($item, $selected_item_product);  
?>
  <select 
    name="<?= Shop_BundleHelper::get_product_selector_name($item, $selected_item_product) ?>" 
    onchange="$(this).getForm().sendRequest('on_action', {update: {'product_bundle_items': 'shop:bundle'}})">
    
    <? if (!$item->is_required): ?>
      <option value="">&lt;please select&gt;</option>
    <? endif ?>
    
    <? foreach ($item->item_products as $item_product): ?>
      <option 
        value="<?= Shop_BundleHelper::get_product_selector_value($item_product) ?>" 
        <?= option_state(Shop_BundleHelper::is_item_product_selected($item, $item_product), true) ?>>
          <?= h($item_product->product->name) ?>
      </option>
    <? endforeach ?>
  </select>

  <? $this->render_partial('shop:bundle_product_parameters', array(
    'product'=>$selected_product, 
    'item_product'=>$selected_item_product, 
    'item'=>$item)) ?>

  <?= Shop_BundleHelper::get_item_hidden_fields($item, $selected_item_product) ?>
<? elseif ($item->control_type == 'checkbox'): ?>
<ul>
  <? 
    foreach ($item->item_products as $item_product):
      $selected_product = Shop_BundleHelper::get_bundle_item_product($item, $item_product);
      $is_selected = Shop_BundleHelper::is_item_product_selected($item, $item_product);
  ?>
  <li>
    <label>
      <input 
        type="checkbox" 
        name="<?= Shop_BundleHelper::get_product_selector_name($item, $item_product) ?>" 
        value="<?= Shop_BundleHelper::get_product_selector_value($item_product) ?>" 
        <?= checkbox_state($is_selected) ?>
       />
      <?= h($item_product->product->name) ?>
    </label>
     
      <div class="<?= $is_selected ? null : 'hidden' ?>">
        <? $this->render_partial('shop:bundle_product_parameters', array(
          'product'=>$selected_product, 
          'item_product'=>$item_product, 
          'item'=>$item)) ?>
      </div>
  </li>
  <? endforeach ?>
</ul>
<? else: ?>
  <ul>
    <? if (!$item->is_required): ?>
      <li>
        <label>
          <input 
            type="radio" 
            name="<?= Shop_BundleHelper::get_product_selector_name($item,null) ?>" 
            value="" 
            checked="checked"/>
          No, thank you.
        </label>
      </li>
    <? endif ?>
    <?
      foreach ($item->item_products as $item_product):
        $selected_product = Shop_BundleHelper::get_bundle_item_product($item, $item_product);
        $is_selected = Shop_BundleHelper::is_item_product_selected($item, $item_product);
    ?>
      <li>
        <label>
          <input 
            type="radio" 
            name="<?= Shop_BundleHelper::get_product_selector_name($item, $selected_product) ?>"
            value="<?= Shop_BundleHelper::get_product_selector_value($item_product) ?>" 
            <?= radio_state($is_selected) ?>
           />
          <?= h($item_product->product->name) ?>
        </label>
    
        <div class="<?= $is_selected ? null : 'hidden' ?>">
          <? $this->render_partial('shop:bundle_product_parameters', array(
            'product'=>$selected_product, 
            'item_product'=>$item_product, 
            'item'=>$item)) ?>
        </div>
      </li>
    <? endforeach ?>
  </ul>
<? endif ?>
{% if item.control_type == 'dropdown' %}
  {% set selected_item_product = method('Shop_BundleHelper', 'get_bundle_item_product_item', item) %}
  {% set selected_product = method('Shop_BundleHelper', 'get_bundle_item_product', item, selected_item_product) %}
  <select 
    name="{{ method('Shop_BundleHelper', 'get_product_selector_name', item, selected_item_product) }}" 
    onchange="$(this).getForm().sendRequest('on_action', {update: {'product_bundle_items': 'shop:bundle'}})">
    
    {% if item.is_required %}
      <option value="">&lt;please select&gt;</option>
    {% endif %}
    
    {% for item_product in item.item_products %}
      <option 
        value="{{ method('Shop_BundleHelper', 'get_product_selector_value', item_product) }}" 
        {{ option_state(method('Shop_BundleHelper', 'is_item_product_selected', item, item_product), true) }}>
          {{ item_product.product.name }}
      </option>
    {% endfor %}
  </select>

  {{ render_partial('shop:bundle_product_parameters', {
    'product': selected_product, 
    'item_product': selected_item_product, 
    'item': item}) }}

  {{ method('Shop_BundleHelper', 'get_item_hidden_fields', item, selected_item_product) }}
{% elseif item.control_type == 'checkbox' %}
<ul>
  {% for item_product in item.item_products %}
    {% set selected_product = method('Shop_BundleHelper', 'get_bundle_item_product', item, item_product) %}
    {% set is_selected = method('Shop_BundleHelper', 'is_item_product_selected', item, item_product) %}
  <li>
    <label>
      <input 
        type="checkbox" 
        name="{{ method('Shop_BundleHelper', 'get_product_selector_name', item, item_product) }}" 
        value="{{ method('Shop_BundleHelper', 'get_product_selector_value', item_product) }}" 
        {{ checkbox_state(is_selected) }}
       />
      {{ item_product.product.name }}
    </label>
     
      <div class="{{ is_selected ? null : 'hidden' }}">
        {{ render_partial('shop:bundle_product_parameters', {
          'product': selected_product, 
          'item_product': item_product, 
          'item': item}) }}
      </div>
  </li>
  {% endfor %}
</ul>
<? else: ?>
  <ul>
    {% if item.is_required %}
      <li>
        <label>
          <input 
            type="radio" 
            name="{{ method('Shop_BundleHelper', 'get_product_selector_name', item, null) }}" 
            value="" 
            checked="checked"/>
          No, thank you.
        </label>
      </li>
    {% endif %}
    {% for item_product in item.item_products %}
      {% set selected_product = method('Shop_BundleHelper', 'get_bundle_item_product', item, item_product) %}
      {% set is_selected = method('Shop_BundleHelper', 'is_item_product_selected', item, item_product) %}
      <li>
        <label>
          <input 
            type="radio" 
            name="{{ method('Shop_BundleHelper', 'get_product_selector_name', item, selected_product) }}"
            value="{{ method('Shop_BundleHelper', 'get_product_selector_value', item_product) }}" 
            {{ radio_state(is_selected) }}
           />
          {{ item_product.product.name }}
        </label>
    
        <div class="{{ is_selected ? null : 'hidden' }}">
          {{ render_partial('shop:bundle_product_parameters', {
            'product': selected_product, 
            'item_product': item_product, 
            'item': item}) }}
        </div>
      </li>
    {% endfor %}
  </ul>
{% endif %}

If you explore the code, you will see that it is quite simple. It first determines what control type should be used for the bundle item and then outputs corresponding control(s) taking into account whether the item is required. The code refers to the shop:bundle_product_parameters partial which you should create.

shop:bundle_product_parameters partial

Create a new partial and name it shop:bundle_product_parameters. This partial should output options, extra options and grouped product for a specific bundle item product. For simplicity's sake we have split this partial to a number of other partials, but you can output all possible product parameters in a single partial. Partial code:

<? if ($item_product && $product): ?>  
  <? if ($product->images->count): ?>
    <a 
      title="<?= h($product->images->first->title) ?>" 
      href="<?= $product->images->first->getThumbnailPath(500, 'auto') ?>">
        <img src="<?= $product->images->first->getThumbnailPath(50, 'auto') ?>" alt="" />
    </a>
  <? endif ?>

  <?
    if ($product && ($product->grouped_products->count || $product->options->count)):
  ?>
    <table>
      <? 
        $this->render_partial('shop:bundle_grouped_products', array(
          'product'=>$product, 
          'item'=>$item, 
          'item_product'=>$item_product));

        $this->render_partial('shop:bundle_product_options', array(
          'product'=>$product, 
          'item'=>$item, 
          'item_product'=>$item_product));
      ?>
    </table>
  <? endif ?>

  <? 
    $this->render_partial('shop:bundle_product_extras', array(
      'product'=>$product, 
      'item'=>$item, 
      'item_product'=>$item_product));

    $this->render_partial('shop:bundle_product_quantity', array(
      'product'=>$product, 
      'item'=>$item, 
      'item_product'=>$item_product));
  ?>
<? endif ?>
{% if item_product and product %}
  {% if product.images.count %}
    <a 
      title="{{ product.images.first.title }}" 
      href="{{ product.images.first.getThumbnailPath(500, 'auto') }}">
        <img src="{{ product.images.first.getThumbnailPath(50, 'auto') }}" alt="" />
    </a>
  {% endif %}

  {% if product and (field(product, 'grouped_products').count or product.options.count) %}
    <table>
      {{ render_partial('shop:bundle_grouped_products', {
          'product': product, 
          'item': item, 
          'item_product': item_product}) }}
      {{ render_partial('shop:bundle_product_options', {
          'product': product, 
          'item': item, 
          'item_product': item_product}) }}
    </table>
  {% endif %}

  {{ render_partial('shop:bundle_product_extras', {
      'product': product, 
      'item': item, 
      'item_product': item_product}) }}
  {{ render_partial('shop:bundle_product_quantity', {
      'product': product, 
      'item': item, 
      'item_product': item_product}) }}
{% endif %}

shop:bundle_grouped_products partial output grouped products of a bundle item product. When a grouped product is selected, partial updates the bundle item user interface with AJAX. Partial code:

<?
  if ($product && $product->grouped_products->count):
?>
  <tr>
    <th><?= h($product->grouped_menu_label) ?>:</th>
    <td>
      <select 
        name="<?= Shop_BundleHelper::get_product_control_name($item, $item_product, 'grouped_product') ?>"
        onchange="$(this).getForm().sendRequest('on_action', {update: {'product_bundle_items': 'shop:bundle'}})">
        <? foreach ($product->grouped_products as $cur_grouped_product): ?>
          <option 
            <?= option_state($product->id, $cur_grouped_product->id) ?> 
            value="<?= $cur_grouped_product->id ?>"
          >
            <?= h($cur_grouped_product->grouped_option_desc) ?>
          </option>
        <? endforeach ?>
      </select>
    </td>
  </tr>
<? endif ?>
{% if product and field(product, 'grouped_products').count %}
  <tr>
    <th>{{ field(product, 'grouped_menu_label') }}:</th>
    <td>
      <select 
        name="{{ method('Shop_BundleHelper', 'get_product_control_name', item, item_product, 'grouped_product') }}"
        onchange="$(this).getForm().sendRequest('on_action', {update: {'product_bundle_items': 'shop:bundle'}})">
        {% for cur_grouped_product in field(product, 'grouped_products') %}
          <option 
            {{ option_state(product.id, cur_grouped_product.id) }} 
            value="{{ cur_grouped_product.id }}"
          >
            {{ cur_grouped_product.grouped_option_desc }}
          </option>
        {% endfor %}
      </select>
    </td>
  </tr>
{% endif %}

shop:bundle_product_options partial outputs options of a bundle item product:

<?
  if ($product && $product->options->count):
    $control_name = Shop_BundleHelper::get_product_control_name($item, $item_product, 'options');
?>
  <? foreach ($product->options as $option): ?>
  <tr>
    <th><?= h($option->name) ?>:</th>
    <td>
      <select name="<?= $control_name.'['.$option->option_key.']' ?>">
        <?
          $values = $option->list_values();
          foreach ($values as $value):      
            $is_selected = Shop_BundleHelper::is_product_option_selected($option, $value, $item, $item_product);
        ?>
          <option <?= option_state($is_selected, true) ?> value="<?= h($value) ?>"><?= h($value) ?></option>
        <? endforeach ?>
      </select>
    </td>
  </tr>
  <? endforeach ?>
<? endif ?>
{% if product and product.options.count %}
{% set control_name = method('Shop_BundleHelper', 'get_product_control_name', item, item_product, 'options') %}
  {% for option in product.options %}
  <tr>
    <th>{{ option.name }}</th>
    <td>
      <select name="{{ control_name~'['~option.option_key~']' }}">
        {% set values = option.list_values() %}
        {% for value in values %}
          {% set is_selected = method('Shop_BundleHelper', 'is_product_option_selected', option, value, item, item_product) %}
          <option {{ option_state(is_selected, true) }} value="{{ value }}">{{ value }}</option>
        {% endfor %}
      </select>
    </td>
  </tr>
  {% endfor %}
{% endif %}

shop:bundle_product_extras partial outputs extra options of a bundle item product. Partial code:

<?
  if ($product && $product->extra_options->count):
    $control_name = Shop_BundleHelper::get_product_control_name($item, $item_product, 'extra_options');
?>
  <table>
    <? foreach ($product->extra_options as $option):
      $is_checked = Shop_BundleHelper::is_product_extra_option_selected($option, $item, $item_product);
    ?>
    <tr>
      <td>
        <input 
          name="<?= $control_name.'['.$option->option_key.']' ?>" 
          <?= checkbox_state($is_checked) ?> 
          id="extra_option_<?= $option->id ?>" 
          value="1" 
          type="checkbox"/>
      </td>
      <th>
        <label for="extra_option_<?= $option->id ?>"><?= h($option->description) ?>:</label>
        <? if ($option->price > 0): ?>
          + <?= format_currency($option->get_price($product)) ?>
        <? else: ?>
          free
        <? endif ?>
      </th>
    </tr>
    <? endforeach ?>
  </table>
<? endif ?>
{% if product and field(product, 'extra_options').count %}
  {% set control_name = method('Shop_BundleHelper', 'get_product_control_name', item, item_product, 'extra_options') %}

  <table>
    {% for option in field(product, 'extra_options') %}
      {% set is_checked = method('Shop_BundleHelper', 'is_product_extra_option_selected', option, item, item_product) %}
    <tr>
      <td>
        <input 
          name="{{ control_name~'['~option.option_key~']' }}" 
          {{ checkbox_state(is_checked) }}
          id="extra_option_{{ option.id }}" 
          value="1" 
          type="checkbox"/>
      </td>
      <th>
        <label for="extra_option_{{ option.id }}">{{ option.description }}:</label>
        {% if option.price > 0 %}
          + {{ option.get_price(product)|currency }}
        {% else %}
          free
        {% endif %}
      </th>
    </tr>
    {% endfor %}
  </table>
{% endif %}

shop:bundle_product_quantity partial outputs price and the quantity input field for a bundle item product. Partial code:

Price: <?= format_currency($item_product->get_price($product)) ?>
<? if ($item_product->allow_manual_quantity): ?>
   <input
      type="text" 
      name="<?= Shop_BundleHelper::get_product_control_name($item, $item_product, 'quantity') ?>" 
      value="<?= Shop_BundleHelper::get_product_quantity($item, $item_product) ?>"/>
<? endif ?>
Price: {{ item_product.get_price(product)|currency }}
{% if item_product.allow_manual_quantity %}
  <input
    type="text" 
    name="{{ method('Shop_BundleHelper', 'get_product_control_name', item, item_product, 'quantity') }}" 
    value="{{ method('Shop_BundleHelper', 'get_product_quantity', item, item_product) }}"/>
{% endif %}


Previous: Displaying Product Manufacturer Information
Return to Displaying a List of Products