LemonStand Documentation

Cart page

The cart page displays a list of items (active and postponed) in the shopping cart and the estimated total order cost. Customers can remove items from the cart or change the quantity of items. 

To create a cart page, start by creating a new page. Assign it a title and URL, for example /cart. The actual URL doesn't matter from LemonStand's point of view. You just need to specify the correct URL of the cart page on other pages.

Next, go to the Action tab and select the shop:cart action from the Action drop-down menu. The shop:cart action processes events such as changing the quantity of itmes ,moving or removing an item to the postponed items list, and deleting items (for consistency).

Creating a cart page

The shopping cart source code is simple. It outputs 2 lists of cart items for active and postponed items respectively. The code uses the same partial for displaying both lists.

<? 
  $active_items = Shop_Cart::list_active_items();
  $postponed_items = Shop_Cart::list_postponed_items();
?>
<?= flash_message() ?>
<? if ($active_items): ?>
  <?= open_form() ?>
    <? $this->render_partial('shop:cart_table', array('items'=>$active_items)) ?>
  
    <h3>Cart Total</h3>
    <p><?= format_currency($cart_total) ?></p>
  
    <h3>Discount</h3>
    <p><?= format_currency($discount) ?></p>

    <h3>Estimated Total</h3>
    <p><?= format_currency($estimated_total) ?></p>

    <p>* Shipping cost, taxes and discounts will be evaluated during the checkout process.</p>
    <label for="coupon_code">Do you have a coupon?</label> 
    <input id="coupon_code" value="<?= h($coupon_code) ?>" type="text" name="coupon"/>
    <input type="submit" value="Apply Changes"/>
  </form>   
<? else: ?>
  <p>Your cart is empty.</p>
<? endif ?>

<? if ($postponed_items): ?>
  <?= open_form() ?>
    <h3>Postponed items</h3>
    <? $this->render_partial('shop:cart_table', array('items'=>$postponed_items, 'postponed'=>true)) ?>
  
    <input type="submit" value="Apply Changes"/>
  </form>
<? endif ?>
{% set active_items = method('Shop_Cart', 'list_active_items') %}
{% set postponed_items = method('Shop_Cart', 'list_postponed_items') %}

{{ flash_message() }}
{% if active_items %}
  {{ open_form() }}
    {{ render_partial('shop:cart_table', {'items': active_items}) }}
  
    <h3>Cart Total</h3>
    <p>{{ cart_total|currency }}</p>
  
    <h3>Discount</h3>
    <p>{{ discount|currency }}</p>

    <h3>Estimated Total</h3>
    <p>{{ estimated_total|currency }}</p>

    <p>* Shipping cost, taxes and discounts will be evaluated during the checkout process.</p>
    <label for="coupon_code">Do you have a coupon?</label> 
    <input id="coupon_code" value="{{ coupon_code }}" type="text" name="coupon"/>
    <input type="submit" value="Apply Changes"/>
  </form>   
{% else %}
  <p>Your cart is empty.</p>
{% endif %}

{% if postponed_items %}
  {{ open_form() }}
    <h3>Postponed items</h3>
    {{ render_partial('shop:cart_table', {'items': postponed_items, 'postponed': true}) }}
    
    <input type="submit" value="Apply Changes"/>
  </form>
{% endif %}

The code fetches the lists of active and postponed cart items using the corresponding methods of the Shop_Cart class. The flash_message function is called to put out possible messages from LemonStand. Then the code checks whether $active_items and $postponed_items arrays have any items. If any items are found, a new FORM element is displayed and the item list is rendered using the shop:cart_table partial. We will explain how to create this partial later in this article. Also, if there are active items in the cart, the estimated total cost of the shopping cart is displayed.

Please note each list is wrapped in a separate html form. The Apply Changes button must be the submit type element.

There are AJAX handlers for all operations required for managing the shopping cart content, including the item deletions and applying changes. Please read the shop:cart action description for details. 

Creating a partial for displaying a list of cart items

Items in the LemonStand shopping cart can be postponed and so we need a way to display both active and postponed items on the cart page. As the nature of both lists are the same, it is a good idea to use the same partial for both lists.

A list of items in the shopping cart must contain an item image, if any, item title, item options and extra options controls for postponing an item and making it active, item quantity, item single price and item total price.

The following code demonstrates an example of the cart item list partial. The partial accepts 2 parameters to be passed in it - $postponed and $items. $postponed parameter must have a value TRUE for the postponed item list.

Create a new partial and name it shop:cart_table. The partial name could be anything, but we used the shop:cart_table in the cart page code example above.

<? $postponed = isset($postponed) ? $postponed : null ?>
<table>
  <tr>
    <th>Cart Items</th>
    <th><? if (!$postponed): ?>Postpone<? else: ?>Postponed<? endif ?></th>
    <th>Quantity</th>
    <th>Delete</th>
    <th>Price</th>
    <th>Discount</th>
    <th>Total</th>
  </tr>
  <? if (!count($items)): ?>
    <tr>
      <td colspan="6">Your cart is empty.</td>
    </tr>
  <? else: ?>
  <?
    foreach ($items as $item):
    $image_url = $item->product->image_url(0, 60, 'auto');
    $options_str = $item->options_str();
  ?>
    <tr>
      <td>
        <? if ($image_url): ?>
          <img src="<?= $image_url ?>" alt="<?= h($item->product->name) ?>"/>
        <? endif ?>
        <strong><?= h($item->product->name) ?></strong>
        <? if (strlen($options_str)): ?>
          <br/><?= h($options_str) ?>.
        <? endif ?>
        <? if ($item->extra_options): ?>
          <? foreach ($item->extra_options as $option): ?>
            <br/>
            + <?= h($option->description) ?>:
            <?= format_currency($option->get_price($item->product)) ?>
          <? endforeach ?>
        <? endif ?>
      </td>
      <td>
        <input type="hidden" name="item_postponed[<?= $item->key ?>]" value="0"/>
        <input type="checkbox" <?= checkbox_state($item->postponed) ?> name="item_postponed[<?= $item->key ?>]" value="1"/>
      </td>
      <td>
        <? if (!$postponed): ?>
          <input type="text" name="item_quantity[<?= $item->key ?>]" value="<?= $item->quantity ?>"/>
        <? else: ?>
          <?= $item->quantity ?>
        <? endif ?>
      </td>
      <td><input type="checkbox" name="delete_item[]" value="<?= $item->key ?>"/></td>
      <td><?= format_currency($item->single_price()) ?></td>
      <td><?= format_currency($item->total_discount()) ?></td>
      <th><?= format_currency($item->total_price()) ?></th>
    </tr>
    <? endforeach ?>
  <? endif ?>
</table>
<table>
  <thead>
    <tr>
      <th>Cart Items</th>
      <th>{% if not postponed %}Postpone{% else %}Postponed{% endif %}</th>
      <th>Quantity</th>
      <th>Delete</th>
      <th>Price</th>
      <th>Discount</th>
      <th>Total</th>
    </tr>
  </thead>
  <tbody>
    {% if items|length == 0 %}
      <tr>
        <td colspan="5">Your cart is empty.</td>
      </tr>    
    {% else %}
      {% for item in items %}
        {% set image_url = item.product.image_url(0, 60, 'auto') %}
        {% set options_str = item.options_str() %}
        <tr>
          <td>
            {% if image_url is not empty %}
              <img src="{{ image_url }}" alt="{{ item.product.name }}"/>
            {% endif %}
            
            <strong>{{ item.product.name }}</strong>
            {% if options_str|length > 0 %}
              <br/>{{ options_str }}.
            {% endif %}
            
            {% if item.extra_options|length > 0 %}
              {% for option in item.extra_options %}
                <br/>
                + {{ option.description }}:
                  {{ option.get_price(item.product)|currency }}
              {% endfor %}
            {% endif %}
          </td>
          <td>
            <input type="hidden" name="item_postponed[{{ item.key }}]" value="0"/>
            <input type="checkbox" {{ checkbox_state(item.postponed) }} name="item_postponed[{{ item.key }}]" value="1"/>
          </td>
          <td>
            {% if not postponed %}
              <input type="text" name="item_quantity[{{ item.key }}]" value="{{ item.quantity }}"/>
            {% else %}
              {{ item.quantity }}
            {% endif %}
          </td>
          <td><input type="checkbox" name="delete_item[]" value="<?= $item->key ?>"/></td>
          <td>{{ item.single_price()|currency }}</td>
          <td>{{ item.total_discount()|currency }}</td>
          <th>{{ item.total_price()|currency }}</th>
        </tr>
      {% endfor %}
    {% endif %}
  </tbody>
</table>

The code iterates over the passed $items array and creates a table row for each item.

The first column of the table contains an item image, name, and selected options.

The second column contains a checkbox for postponing or activating an item. Please note the name of the checkbox – item_postponed[item_key]. This name should not be changed in order for LemonStand to be able to process the checkbox state. You may also note a hidden element with the same name placed before the checkbox. It is used to pass 0 value to PHP in case the checkbox is not checked. By default, values of unchecked checkboxes are not sent to the server on form submit.

The third column contains a checkbox for deleting an item. The checkbox INPUT element has name delete_item[] and value corresponding to the item key. The name of the checkbox element should not be changed.

The next column contains the item quantity input field. The name item_quantity[item_key] should not be changed. Note that the postponed items the code does not output the field, replacing it with a static text.

The two last columns display the item individual price, item discount and row total price. The discount value is calculated basted on the currently active catalog-level price rules.

Displaying bundle items in the cart

There are different ways to represent bundle products in the cart. For example, if your base bundle product has no price and its price is a sum of bundle items, then you can display the total product bundle price right in the base product row. Another way to display the total bundle price is a separate line in the cart table. This option is more universal, and its implementation is described in this section. This approach is implemented in our Simplicity theme:

In general the process if displaying bundle items in the cart is similar to the process described above, but there are a number of significant differences:

  1. Required bundle items cannot be removed from the cart, so  Delete link should be hidden.
  2. Some bundle items do not allow manual quantity input, so Quantity field should be hidden and replaced with a static text. Also, you should use cart item's get_quantity() method to display the item quantity instead of the $quantity field. get_quantity() method correctly calculates the item item quantity in bundle. For example, imagine that your base product is a PC and it has a bundle item RAM which quantity is 1. If the user sets quantity 2 for the PC, this will mean that the total quantity is RAM is 2, but there will be still 1 RAM per 1 PC. get_quantity() method will return 1 in this case (meaning 1 RAM item per each PC product), while $quantity field will return 2 (the total quantity value for RAM in the order). 
  3. Bundle items cannot be postponed separately from the base bundle product, so the Postpone checkbox should not be visible.
  4. Value in the total column for bundle items should display the total price per single master product. Optionally you can display the multiplier value (see the code snippet below).

Updating the cart partial to display bundle items

The following code represents the updated cart table body for displaying bundle items:

<?
  $last_index = count($items)-1;

  foreach ($items as $index=>$item):
  $image_url = $item->product->image_url(0, 60, 'auto');
  $options_str = $item->options_str();
  
  // Load item bundle item and bundle item product objects
  $bundle_item = $item->get_bundle_item();
  $bundle_item_product = $item->get_bundle_item_product();
  
?>
  <tr>
    <td>
      <? if ($image_url): ?>
        <img src="<?= $image_url ?>" alt="<?= h($item->product->name) ?>"/>
      <? endif ?>
      <strong><?= h($item->product->name) ?></strong>
      <? if (strlen($options_str)): ?>
        <br/><?= h($options_str) ?>.
      <? endif ?>
      <? if ($item->extra_options): ?>
        <? foreach ($item->extra_options as $option): ?>
          <br/>
          + <?= h($option->description) ?>:
          <?= format_currency($option->get_price($item->product)) ?>
        <? endforeach ?>
      <? endif ?>
    </td>
    <td>
      <!-- Do not display the Postpone checkbox for bundle items -->
      <? if (!$bundle_item_product): ?>
        <input type="hidden" name="item_postponed[<?= $item->key ?>]" value="0"/>
        <input 
          type="checkbox" 
          <?= checkbox_state($item->postponed) ?> name="item_postponed[<?= $item->key ?>]" value="1"/>
      <? endif  ?>
    </td>
    <td>
      <!-- Do not display the Delete checkbox for required bundle items -->
      <? if (!$bundle_item || !$bundle_item->is_required): ?>
        <input type="checkbox" name="delete_item[]" value="<?= $item->key ?>"/>
      <? endif  ?>
    </td>
    <td>
      <!-- Do not display the Quantity field for postponed items and 
           bundle items which do not allow manual quantity input -->
      <? if (!$postponed && (!$bundle_item_product || $bundle_item_product->allow_manual_quantity)): ?>
        <input type="text" name="item_quantity[<?= $item->key ?>]" value="<?= $item->get_quantity() ?>"/>
      <? else: ?>
        <?= $item->get_quantity() ?>
      <? endif ?>
    </td>
    <td><?= format_currency($item->single_price()) ?></td>
    <td><?= format_currency($item->total_discount()) ?></td>
    <th>
      <? if (!$item->is_bundle_item()): ?>
        <?= format_currency($item->total_price()) ?>
      <? else:
        // For bundle items use the bundle_item_total_price() method to get 
        // the total item price per single base product
        // and display the multiplier.
        $master_item = $item->get_master_bundle_item();
        $multiplier = ($master_item && $master_item->quantity > 1) ? ' x '.$master_item->quantity : null;
      ?>
        <?= format_currency($item->bundle_item_total_price()).$multiplier ?>
      <? endif  ?>
    </th>
  </tr>
  
  <!-- Display the bundle total row -->
  
  <? 
    if (
          ($bundle_item && $index == $last_index) 
          || ($bundle_item && $bundle_item->id && !$items[$index+1]->get_bundle_item())): 
      $master_item = $item->get_master_bundle_item();
      if ($master_item):
  ?>
      <tr>
        <td colspan="3"><?= h($master_item->product->name) ?> bundle totals</td>
        <td><?= $master_item->quantity ?></td>
        <td><?= format_currency($master_item->bundle_single_price()) ?></td>
        <td><?= format_currency($master_item->bundle_total_discount()) ?></td>
        <th><?= format_currency($master_item->bundle_total_price()) ?></th>
      </tr>
  <? 
      endif;
    endif ?>  
<? endforeach ?>
{% set last_index = items|length-1 %}
{% for index, item in items %}
  {% set image_url = item.product.image_url(0, 60, 'auto') %}
  {% set options_str = item.options_str() %}
  
  {# Load item bundle item and bundle item product objects #}
  {% set bundle_item = item.get_bundle_item() %}
  {% set bundle_item_product = item.get_bundle_item_product()  %}
  <tr>
    <td>
      {% if image_url is not empty %}
        <img src="{{ image_url }}" alt="{{ item.product.name }}"/>
      {% endif %}
      
      <strong>{{ item.product.name }}</strong>
      {% if options_str|length > 0 %}
        <br/>{{ options_str }}.
      {% endif %}
      
      {% if item.extra_options|length > 0 %}
        {% for option in item.extra_options %}
          <br/>
          + {{ option.description }}:
            {{ option.get_price(item.product)|currency }}
        {% endfor %}
      {% endif %}
    </td>
    <td>
      <!-- Do not display the Postpone checkbox for bundle items -->
      {% if not bundle_item_product %}
        <input type="hidden" name="item_postponed[{{ item.key }}]" value="0"/>
        <input type="checkbox" {{ checkbox_state(item.postponed) }} name="item_postponed[{{ item.key }}]" value="1"/>
      {% endif %}
    </td>
    <td>
      <!-- Do not display the Quantity field for postponed items and 
          bundle items which do not allow manual quantity input -->
      {% if not postponed or (not bundle_item_product or bundle_item_product.allow_manual_quantity) %}
        <input type="text" name="item_quantity[{{ item.key }}]" value="{{ item.quantity }}"/>
      {% else %}
        {{ item.quantity }}
      {% endif %}
    </td>
    <td>
      <!-- Do not display the Delete checkbox for required bundle items -->
      {% if not bundle_item or not bundle_item.is_required %}
        <input type="checkbox" name="delete_item[]" value="<?= $item->key ?>"/>
      {% endif %}
    </td>
    <td>
      {% if not item.is_bundle_item() %}
        {{ item.total_price()|currency }}
      {% else %}
        {#
          For bundle items use the bundle_item_total_price() method to get 
          the total item price per single base product
          and display the multiplier.
        #}
        {% set master_item = item.get_master_bundle_item() %}
        {% set multiplier = (master_item and master_item.quantity > 1) ? ' x '~master_item.quantity : null %}
        {{ item.bundle_item_total_price()|currency~multiplier }}
      {% endif %}
    </td>
    <td>{{ item.total_discount()|currency }}</td>
    <th>{{ item.total_price()|currency }}</th>
  </tr>
  
  <!-- Display the bundle total row -->
  {% if 
    (bundle_item and index == last_index) or 
    (bundle_item and bundle_item.id and 
      not attribute(items, index+1).get_bundle_item()) %}
      
    {% set master_item = item.get_master_bundle_item() %}
    {% if master_item %}
      <tr>
        <td colspan="3">{{ master_item.product.name }} bundle totals</td>
        <td>{{ master_item.quantity }}</td>
        <td>{{ master_item.bundle_single_price()|currency }}</td>
        <td>{{ master_item.bundle_total_discount()|currency }}</td>
        <th>{{ master_item.bundle_total_price()|currency }}</th>
      </tr>
    {% endif %}
  {% endif %}
{% endfor %}

Creating the Checkout button and the Coupon field

Usually the Checkout button is linked to the Cart page. In the simplest case, if you are not going to use coupons for promoting products from your catalog, the checkout button can be just a link to the Checkout page.

If you need to have a field for entering a coupon code, you can place it on the cart page near the Checkout and Apply Changes buttons. The shop:cart action can process coupon codes and redirect the browser to a specified URL (if needed). The following code snippet demonstrates the technique.

<label for="coupon_code">Do you have a coupon?</label> 
<input id="coupon_code" value="<?= h($coupon_code) ?>" type="text" name="coupon"/>

<input type="submit" value="Checkout" name="set_coupon_code"/>
<input type="hidden" name="redirect" value="/checkout_start"/>
<label for="coupon_code">Do you have a coupon?</label> 
<input id="coupon_code" value="{{ coupon_code }}" type="text" name="coupon"/>

<input type="submit" value="Checkout" name="set_coupon_code"/>
<input type="hidden" name="redirect" value="/checkout_start"/>

The code creates a label, a text field, a button and a hidden field. The names of the coupon code text field, button and the hidden field should be the coupon, set_coupon_code and redirect correspondingly. The redirect hidden field should contain the URL of the checkout page. The code should be placed inside the FORM element. You can use the same form which wraps the shopping cart content tables, or create another form using the open_form function.

Also there is the AJAX way to post the coupon code and redirect the browser to the checkout page. Please read the description of the shop:cart action. The event handler for processing the coupon code is called on_setCouponCode.

See also:

Next: Implementing the Shipping Cost Estimator Feature
Previous: Product page
Return to Building your online store