diff --git a/content/docs/storefront/themes/guides/meta.json b/content/docs/storefront/themes/guides/meta.json index 6b56a043..c53f1de0 100644 --- a/content/docs/storefront/themes/guides/meta.json +++ b/content/docs/storefront/themes/guides/meta.json @@ -1,4 +1,4 @@ { "title": "Guides", - "pages": ["custom-page-templates", "custom-product-templates", "product-variants"] + "pages": ["custom-page-templates", "custom-product-templates", "product-variants", "personalized-products"] } diff --git a/content/docs/storefront/themes/guides/personalized-products.mdx b/content/docs/storefront/themes/guides/personalized-products.mdx new file mode 100644 index 00000000..e6ec9898 --- /dev/null +++ b/content/docs/storefront/themes/guides/personalized-products.mdx @@ -0,0 +1,131 @@ +--- +title: Personalized Products Guide +sidebar_label: Personalized Products +tags: + - Guide +--- +import { Callout } from 'fumadocs-ui/components/callout'; +import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Cards, Card } from 'fumadocs-ui/components/card'; + +Some products need customer input at the time of purchase — an engraving on a mug, a monogram on a bag, a gift message on a card. This information is captured as **line item properties**: name and value pairs attached to a cart line rather than to the product itself. No variant, SKU, or inventory is required for each possible value. + + + + + +### Add Property Inputs to the Product Template + +Property inputs are named `properties[]`, where `` is the label stored with the line. Add them inside the [add-to-cart form](/docs/storefront/themes/templates/tags#cart_form) alongside the fields generated by the `cart_form` tag. + +The engraving label and input are the only addition to the add-to-cart form. + +```django title="templates/catalogue/product.html" +{% purchase_info_for_product request product as session %} +{% if session.availability.is_available_to_buy %} + {% cart_form request product 'single' as cart_form %} +
+ {% csrf_token %} + {% include "partials/form_fields.html" with form=cart_form %} + + + +
+{% else %} + {% t "store.catalogue.out_of_stock" %} +{% endif %} +``` + +Add one input per property. A mug with an engraving and a font choice uses `properties[Engraving]` and `properties[Font]`. + +
+ + + +### Display Properties in the Cart + +The cart template receives a `formset` of cart line forms. Each `form.instance` is a [line](/docs/storefront/themes/templates/objects#line) with a `properties` list of `key` and `value` pairs. + +```django title="partials/cart_line_properties.html" +{% for property in properties %} + {% if property.value %} +
{{ property.key }}: {{ property.value }}
+ {% endif %} +{% endfor %} +``` + +```django title="templates/cart.html" +{% for form in formset %} + {% with line=form.instance %} + {% include "partials/cart_line_properties.html" with properties=line.properties %} + {% endwith %} +{% endfor %} +``` + + +Themes that ship their own side cart JavaScript need to render properties there too. Request `properties { key value }` on the cart lines in your side cart query and output them alongside the product title. + + +
+ +
+ +### Behavior to Expect + +| Behavior | Detail | +| ----- | ------ | +| Unique lines | The same product added with different property values creates a separate cart line for each combination. Adding it again with identical values increases the quantity of the existing line. | +| Empty values | A property submitted with an empty value does not create a separate line. It is still recorded on the line, so guard your display with `{% if property.value %}`. | +| Hidden properties | Property names starting with an underscore, such as `properties[_source]`, are stored but excluded from `line.properties` and from the Storefront GraphQL API. Use them for data that should not be shown to customers. | +| Value length | Values longer than 500 characters are truncated to 500 characters. | +| Checkboxes | When a checkbox named `properties[...]` changes, the value submitted is `yes` or `no`. | +| File uploads | File inputs are not supported. Inputs with `type="file"` are ignored. | +| Set once | Properties are captured when the line is added to the cart. There is no update path for changing them afterwards. | + +### Add Properties with the Storefront GraphQL API + +Themes that add to the cart through the [Storefront GraphQL API](/docs/storefront/graphql) pass properties on each line as a JSON object of name and value pairs. The field is available on [createCart](/docs/storefront/graphql/mutations/create-cart) and [addCartLines](/docs/storefront/graphql/mutations/add-cart-lines), and is returned on cart lines as `properties { key value }`. + +```json title="addCartLines Variables" +{ + "input": { + "cartId": "", + "lines": [ + { + "productPk": 1, + "quantity": 1, + "properties": { "Engraving": "Alex" } + } + ] + } +} +``` + + +`properties` must be a JSON object. Any other value returns the error `Properties must be a JSON object.` + + +### Related + + + + + + + diff --git a/content/docs/storefront/themes/templates/objects.mdx b/content/docs/storefront/themes/templates/objects.mdx index aa3c45e7..388d8b78 100644 --- a/content/docs/storefront/themes/templates/objects.mdx +++ b/content/docs/storefront/themes/templates/objects.mdx @@ -758,6 +758,32 @@ There are three filter types: `price_range`, `boolean`, and `list`. Each type ha | `label` | String | Display label for this value. | +### line + +Cart line object available on the cart page (`templates/cart.html`) through the `formset` context. Each form in the formset exposes its cart line as `form.instance`. + +```django title="Example Cart Line Properties" +{% for form in formset %} + {% with line=form.instance %} + {% for property in line.properties %} +
{{ property.key }}: {{ property.value }}
+ {% endfor %} + {% endwith %} +{% endfor %} +``` + +| Property | Type | Description | +| ----- | ------ | ------ | +| `properties` | List | Line item properties captured on the product page, see [Personalized Products](/docs/storefront/themes/guides/personalized-products). Property names starting with an underscore are excluded. | + +**line.properties** + +| Property | Type | Description | +| ----- | ------ | ------ | +| `key` | String | The property name, taken from the `properties[]` input on the product page. | +| `value` | String | The value submitted by the customer. | + + ## Template Contexts All templates receive the [Global Objects](#global-objects) (`store`, `settings`, `currencies`, `languages_active_storefront`, `menus`, `products`, `product_categories`, `posts`, `post_categories`, `privacy_policy`, `terms_and_conditions`, `subscription_terms_and_conditions`, `request`, `storefront_geos`). The table below lists additional view-specific context variables passed to each template. @@ -765,6 +791,7 @@ All templates receive the [Global Objects](#global-objects) (`store`, `settings` | Template | View-Specific Context | | ----- | ------ | | `templates/index.html` | Global objects only. Use the [`where`](/docs/storefront/themes/templates/tags#where) tag to query products and categories. | +| `templates/cart.html` | `formset` (cart line forms, each `form.instance` is a [line](#line)) | | `templates/catalogue/product.html` | `product`, `variant_form`, `interval_count_choices` | | `templates/catalogue/index.html` | `products` (paginated), `paginator`, `page_obj` | | `templates/catalogue/category.html` | `category`, `products` (paginated), `filters`, `has_active_filter`, `paginator`, `page_obj` | diff --git a/content/docs/storefront/themes/templates/tags.mdx b/content/docs/storefront/themes/templates/tags.mdx index 1657263f..982575e7 100644 --- a/content/docs/storefront/themes/templates/tags.mdx +++ b/content/docs/storefront/themes/templates/tags.mdx @@ -100,24 +100,61 @@ The `cart_form` tag generates an add-to-cart form for a product. Required on eve {% cart_form request product 'single' as cart_form %} ``` -```django title="Example Product Add to Cart Form" -{% cart_form request product 'single' as cart_form %} -
- {% csrf_token %} - {{ cart_form }} - -
+The tag returns a form object, not rendered HTML. Loop over it to render each field in your own markup, as the Intro Bootstrap theme does through its `partials/form_fields.html` partial. + +```django title="templates/catalogue/product.html" +{% block product_cart_form %} +
+ {% purchase_info_for_product request product as session %} + {% if session.availability.is_available_to_buy %} + {% cart_form request product 'single' as cart_form %} +
+ {% csrf_token %} + {% include "partials/form_fields.html" with form=cart_form breakpoint_col='col-3 col-md-2' size='lg' nolabel=True %} +
+
+ +
+
+
+ {% else %} +
{% t "store.catalogue.out_of_stock" %}
+ {% endif %} +
+{% endblock %} +``` + +```django title="partials/form_fields.html" +{% if form.is_bound and not form.is_valid %} + +{% endif %} + +{% if form.non_field_errors %} + {% for error in form.non_field_errors %} + + {% endfor %} +{% endif %} + +{% for field in form %} + {% include 'partials/form_field.html' with field=field style=style %} +{% endfor %} ``` | Argument | Description | | --- | --- | | request | The current `request` context object. | | product | The `product` context object. | -| form_type | The form type, typically `'single'`. | +| quantity_type | Accepts `'single'` or `'multiple'`. | | variable | Assigned template variable name for the form. | +Add [line item property](/docs/storefront/themes/guides/personalized-products) inputs to this form to capture customer personalization such as an engraving or gift message. + ### comment Ignores everything between `{% comment %}` and `{% endcomment %}`. An optional note may be inserted in the first tag. For example, this is useful when commenting out code for documenting why the code was disabled.