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 #}