Present for Craft CMS

Templating

Everything is under craft.present.

Finding bundles

{% set bundles = craft.present.bundles.type('giftBox').all() %}
{% set bundle = craft.present.bundle('GIFTBOX') %}   {# by SKU #}
{% set bundle = craft.present.bundle(1234) %}        {# by ID #}

craft.present.bundles is an element query — everything you can do with craft.entries works, plus .type(), .pricingMode(), .before(), .after().

Configuring and pricing

{% set config = craft.present.defaultConfiguration(bundle) %}
{% set config = craft.present.configure(bundle, selection) %}

{% if not config.isValid %}
  <ul>{% for error in config.errors %}<li>{{ error }}</li>{% endfor %}</ul>
{% endif %}

{% set pricing = craft.present.price(config) %}
{{ pricing.unitTotal|commerceCurrency(cart.currency) }}

A selection is {slotHandle: {purchasableId: qty}}. Three shorthands are accepted, so a form can post whichever is natural:

{'mug': 42}                    {# one choice at the slot's quantity #}
{'fillings': [42, 43]}         {# several choices, one of each #}
{'fillings': {42: 2, 43: 1}}   {# choices with quantities #}

Adding to the cart

<form method="post">
  {{ csrfInput() }}
  {{ actionInput('present/cart/add') }}
  {{ redirectInput('shop/cart') }}
  <input type="hidden" name="bundleId" value="{{ bundle.id }}">
  <input type="hidden" name="qty" value="1">

  <input type="radio" name="selection[mug]" value="42">
  <input type="number" name="selection[fillings][43]" value="2">

  <button>Add to cart</button>
</form>

On failure the template is re-rendered with presentErrors and presentConfiguration in scope, so you can show what went wrong without losing what the shopper had chosen.

Post Accept: application/json and you get {success, message, cart, bundle} back instead.

Removing and reconfiguring

{{ actionInput('present/cart/remove') }}
<input type="hidden" name="groupKey" value="{{ group.groupKey }}">

present/cart/update (Pro) replaces a box's contents, keeping its quantity. It needs Editable in the cart on the bundle.

Always remove a box through present/cart/remove. Deleting the container line item by hand works, but goes through the repair path rather than the front door.

The cart

Component line items are real line items, so a template that loops over cart.lineItems naively lists a box's contents as separate rows.

{% for item in craft.present.cartLineItems(cart) %}
  {% if craft.present.isComponent(item) %}
    <tr class="is-component"><td colspan="2">↳ {{ item.qty }} × {{ item.description }}</td></tr>
  {% else %}
    {% set group = craft.present.group(item) %}
    <tr>
      <td>{{ item.qty }} × {{ item.description }}</td>
      <td>{{ (group ? group.total : item.total)|commerceCurrency(cart.currency) }}</td>
    </tr>
  {% endif %}
{% endfor %}

Turning Show components in the cart off makes cartLineItems() leave components out altogether. Present never hides a line item from Commerce — the setting only tells your templates what to render.

Groups

{% for key, group in craft.present.groups(cart) %}
  {{ group.qty }} × {{ group.description }} — {{ group.total|commerceCurrency(cart.currency) }}

  {% for component in group.components %}
    {{ component.qty }} × {{ component.description }}
  {% endfor %}
{% endfor %}

Stock

{% set availability = craft.present.availability(bundle) %}

{{ availability.isAvailable }}
{{ availability.maxBuildable }}   {# null means unlimited #}
{{ availability.isUnlimited }}
{{ availability.reason }}
{{ availability.limitingSlotName }}

Fulfilment

{% for key, fulfillment in craft.present.fulfillment(order) %}
  {{ fulfillment.statusLabel }}
  {{ fulfillment.qtyFulfilled }} / {{ fulfillment.qtyOrdered }}
  {% for blocker in fulfillment.blockers %}{{ blocker.description }}{% endfor %}
{% endfor %}

JSON

{{ craft.present.configJson(bundle) }}   {# the whole payload, inline #}
{{ craft.present.apiUrl(bundle) }}       {# GET endpoint for a JS front end #}