Customizing the Product Bundle rendering

Abstract

Overview of the Product Bundle rendering variant Scriban template, which you can customize by modifying the code.

The Default Product Bundle rendering includes a rendering variant in the form of a Scriban template. The Default Product Bundle rendering handles both static and dynamic bundles. All the nested items in the original rendering variant are folded into a single Scriban template. While you can modify the Scriban template, it is best to create a new custom rendering variant in this case so that the customization does not get overridden during software upgrades. The ease with which you can modify the template , for example to add a custom label, makes Scriban templates highly customizable and extensible. You can also change the labels for the rendering.

Note

The Product Bundle -deprecated rendering is only available for existing sites and is based on a MVC view instead of a Scriban template in a rendering variant.

The Product Bundle rendering variant has its own default rendering variant root and default rendering variant (/sitecore/Content/<tenant>/<site>/Presentation/Rendering Variants/Product Bundle). It uses a Scriban template for generating the markup, JavaScript for retrieving the stock and price information (as an AJAX request), and the SXA Dictionary for label translation.

The following code shows the Product Bundle rendering Scriban template defined within the default rendering variant. The name of the catalog and the bundle ID are passed to the MVC Controller, which is responsible for populating the model using the associated bundle repository. The model is passed to the view and available in the rendering variant and in Scriban templates using the embedded item o_model. The model exposes all the objects and values needed to render a product bundle including the optional bundled items and the upgradable items.

{{ # The <div> element contains the bundle data attributes with the values populated from the data model and data source. }}
<div class="bundle-header" data-catalog-name="{{ o_model.catalog_name | html.escape }}" data-bundle-id="{{ o_model.bundle_id }}"
    data-bundle-list-price="{{ o_model.list_price }}" data-bundle-adjusted-price="{{ o_model.adjusted_price }}"
    data-bundle-is-on-sale="{{ o_model.is_on_sale }}" data-bundle-is-dynamic="{{ o_model.is_dynamic_bundle }}"
    data-bundle-savings-percentage="{{ i_datasource.SavingsLeadText | html.escape }} {{ o_model.savings_percentage | html.escape }}"
    data-bundle-invalid-variant-error-message="{{ i_datasource.InvalidVariantMessage | html.escape }}"
    data-bundle-invalid-range-error-message="{{ i_datasource.InvalidRangeMessage | html.escape }}">
    {{ # The <h3> element displays the bundle title text retrieved from the data source. }} 
    <h3 class="bundle-title">{{  i_datasource.TitleText }}</h3>
</div>
{{ # The <div> element contains all the bundle line items. }} 
<div class="bundle-groups-container">
    {{ # Iterate through each bundle line item. }} 
    {{ for bundleLineItem in o_model.line_items }}
    {{ ## The <form> element contains the bundle data attributes with the values populated from the bundle line item.
          The 'noPanel' CSS class will be applied if the 'hasPanel' computed property in the view model returns false. ## }}
    <form autocomplete="off" class="bundle-group" data-bundle-line-item-id="{{ bundleLineItem.bundle_line_item_id }}" 
        data-bundle-line-item-name="{{ bundleLineItem.display_name | html.escape }}"
        data-bundle-line-item-optional="{{ bundleLineItem.is_optional }}"
        data-bundle-line-item-upgradable="{{ bundleLineItem.is_upgradable }}"
        data-bundle-line-item-min-quantity="{{ bundleLineItem.quantity }}" 
        data-bundle-line-item-max-quantity="{{ bundleLineItem.maximum_quantity }}" 
        data-bundle-line-item-link="{{ bundleLineItem.product_link }}"
        data-bind="css: { noPanel: !hasPanel() }">
        {{ # The <div> element contains all the header elements. }} 
        <div class="bundle-group-header-container">
            {{ # The <div> element has a binding with the click event to show or hide its content. }} 
            <div class="bundle-group-header" data-bind="click: toggleBundleGroupBody">
                {{ ## The <span> element displays the bundle line item summary in this format: 
                      <Product Display Name> <Separator> <Quantity> <Unit of Measurement>
                      Example: Habitat Dwell Digital Wifi Scale (Black) - 2 pcs ## }} 
                <span class="bundle-group-product-name">
                    {{ ## The <span> element displays the display name of the bundle line item. 
                          It has a text binding with the bundleLineItemName observable to ensure the value is updated in the 
                          case when an upgrade item is selected. ## }}
                    <span class="product-name-label" data-bind="text: bundleLineItemName">{{ bundleLineItem.display_name }}</span>
                    {{ # The <span> element displays the separator. }}
                    <span class="bundle-group-header-separator">-</span>
                    {{ ## The <span> element displays the quantity of the bundle line item. 
                          It has a text binding with the bundleLineItemQuantity observable to ensure the value is updated in the 
                          case when an upgrade item is selected. ## }}
                    <span data-bind="text: bundleLineItemQuantity">{{ bundleLineItem.quantity }}</span>
                    {{ # The <span> element displays the unit of measurement retrieved from the data source. }}
                    <span class="bundle-group-header-uom">{{ i_datasource.UnitOfMeasurement }}</span>
                </span>
                {{ # The setArrowClass computed property in the view model determines whether to display the accordion toggle button. }}
                <span data-bind="css: setArrowClass()"></span>
                {{ ## The <a> element is a link to the product page of the bundle line item. Link text is retrieved from the data source. 
                      It has a href attribute binding with the bundleLineItemLink observable to ensure the value is updated in the 
                      case when an upgrade item is selected. ## }}
                <a class="product-link" data-bind="attr: { href: bundleLineItemLink }">{{ i_datasource.ViewProductText }}</a>
            </div>
            {{ ## The <div> element has a visible binding with the bundle line item 'is_optional' flag. 
                  Hence, it will only be displayed if the flag is true. ## }} 
            <div class="bundle-group-optional" data-bind="visible: {{ bundleLineItem.is_optional }}">
                {{ ## The <input> element is a checkbox with a checked binding to indicate whether the bundle line item is included in the bundle or not. 
                      The element is disabled if the computed property 'isCheckboxDisabled' returns true. ## }}
                <input type="checkbox" class="checkbox-inline" data-bind="checked: isIncluded, disable: isCheckboxDisabled()">
                {{ # The <label> element is the checkbox label text that is retrieved from the data source. }}
                <label class="bundle-group-optional-label">{{ i_datasource.IncludeItemText }}</label>
            </div>
            {{ ## The <div> element contain all elements to display and modify the bundle line item quantity.
                  It has a visible binding with the 'is_dynamic_bundle' flag in the model. 
                  Hence, it will only be displayed if the flag is true. ## }} 
            <div class="bundle-group-quantity" data-bind="visible: {{ o_model.is_dynamic_bundle }}">
                {{ # The <label> element is the quantity label text that is retrieved from the data source. }}
                <label class="bundle-group-quantity-label">{{ i_datasource.QuantityLabel }}</label>
                {{ ## The <div> element contains the input field and buttons to set the quantity. }}
                <div class="bundle-group-quantity-input">
                    {{ ## The <button> element is used to decrease the bundle line item quantity by one (1) unit. 
                          Whenever the button is clicked, the click binding will invoke the 'decreaseBundleItemQuantity' function.   
                          The button is enabled if the computed property 'isQuantityEnabled' returns true. ## }}
                    <button class="decrease" data-bind="event: { click: decreaseBundleItemQuantity }, enable: isQuantityEnabled()"></button>
                    {{ ## The <input> element is a field to enter the desired quantity for the bundle line item. 
                          Whenever the input value is changed, the event binding will invoke the 'updateBundleItemQuantity' function.   
                          The input is enabled if the computed property 'isQuantityEnabled' returns true. ## }}
                    <input name="bundleLineItemQuantity" class="quantity" type="number" data-val="true" 
                    data-bind="event: { change: updateBundleItemQuantity }, value: bundleLineItemQuantity, valueUpdate: 'input', enable: isQuantityEnabled()"> 
                    {{ ## The <button> element is used to increase the bundle line item quantity by one (1) unit. 
                          Whenever the button is clicked, the click binding will invoke the 'increaseBundleItemQuantity' function.   
                          The button is enabled if the computed property 'isQuantityEnabled' returns true. ## }}
                    <button class="increase" data-bind="event: { click: increaseBundleItemQuantity }, enable: isQuantityEnabled()"></button>
                </div>
                {{ ## The <span> element contains the validation message which is displayed when the bundleLineItemQuantity is invalid. }}
                <span class="validationMessage" data-bind="validationMessage: bundleLineItemQuantity"></span>
            </div>
            {{ ## The <div> element displays information on the selected variants for the bundle line item.
                  It has a foreach binding to iterate through the selected variants and is only visible if the bundle
                  line item has selected variants. ## }}
            <div class="selected-variant-preview" data-bind="visible: hasSelectedVariants, foreach: selectedVariants">
                {{ ## The <div> element groups together the variant's key and value. }}
                <div class="selected-variant-group">
                    {{ # The <span> elements displays the variant's key and value from the view model in this format: <key>: <value> }}
                    <span data-bind="text: displayName"></span><span></span>
                    <span class="selected-value" data-bind="text: value"></span>
                </div>
            </div>
        </div>
        {{ # The <div> element contains all the body elements. }} 
        <div class="bundle-group-body-container">
            {{ # The <div> element contains the available upgrade items of the bundle line item. }}
            <div class="group-upgrade-section">
                {{ ## The <div> element contains the selection elements for the upgrade items.
                      The element is visible if the bundle item is upgradable and disabled otherwise. ## }}
                <div class="group-upgrade-selection" data-item-id="{{ bundleLineItem.bundle_line_item_id }}" data-bind="visible: {{ bundleLineItem.is_upgradable }}">
                    {{ # The <label> element is the upgrade options label text that is retrieved from the data source. }}
                    <label class="group-upgrade-label">{{ i_datasource.UpgradeOptionsLabel }}</label>
                    {{ ## The <select> element contains all the viable upgrade items. 
                          Whenever the option is changed, the event binding will invoke the 'onUpgradeItemChanged' function. ## }}
                    <select class="group-upgrade-variant-select" data-bind="event: { change: onUpgradeItemChanged }">
                        {{ ## Default <option> element when no upgrade item is selected, which is the bundle line item itself. 
                              The element contains data attributes that is required to update the view model.
                              The placeholder text is retrieved from the data source. ## }} 
                        <option data-upgrade-item-id="{{ bundleLineItem.bundle_line_item_id }}"
                        data-upgrade-item-name="{{ bundleLineItem.display_name | html.escape }}"
                        data-upgrade-item-min-quantity="{{ bundleLineItem.quantity }}" 
                        data-upgrade-item-max-quantity="{{ bundleLineItem.maximum_quantity }}"
                        data-upgrade-item-link="{{ bundleLineItem.product_link }}" selected>{{ sc_raw i_datasource "UpgradeOptionsPlaceholder" }}</option>
                        {{ # Iterate through the upgradable items of the bundle item. }} 
                        {{ for upgradeItem in bundleLineItem.upgradable_sellable_item_list }}
                        {{ # Populate the display name of the upgrade item into the <option> element. }} 
                        <option data-upgrade-item-id="{{ upgradeItem.bundle_line_item_id }}"
                        data-upgrade-item-name="{{ upgradeItem.display_name | html.escape }}"
                        data-upgrade-item-min-quantity="{{ upgradeItem.quantity }}" 
                        data-upgrade-item-max-quantity="{{ upgradeItem.maximum_quantity }}"
                        data-upgrade-item-link="{{ upgradeItem.product_link }}">{{ upgradeItem.display_name | html.escape }}</option>
                        {{ end }}
                    </select>
                </div>
            </div>
            {{ # The <div> element contains the available variant selections of the bundle line item. }}
            <div class="group-variant-section">
                {{ # Iterate through each variant selection. }} 
                {{ for variantselection in bundleLineItem.variant_selections }}
                <div class="group-variant-selection" data-product-id="{{ bundleLineItem.bundle_line_item_id }}">
                    {{ # The <label> element contains the display name of the variant. }}
                    <label class="group-variant-label">{{ variantselection.display_name }}</label>
                    {{ ## The <select> element contains all the viable options for the variant selection. 
                          Whenever the option is changed, the event binding will invoke the 'onBundleItemChanged' function. 
                          The element is disabled if there is only one option and enabled otherwise. ## }}
                    <select class="group-variant-select" data-bind="event: { change: onBundleItemChanged }, disable: {{ variantselection.options.size == 1 }}"
                        data-variant-key="{{ variantselection.property_name }}"
                        data-variant-display-name="{{ variantselection.display_name }}">
                        {{ # Iterate through the options of each variant selection. }} 
                        {{ for option in variantselection.options }}
                        {{ # Populate the option value into the <option> element. }} 
                        <option>{{ option.value }}</option>
                        {{ end }}
                    </select>
                </div>
                {{ end }}
            </div>
            {{ # Iterate through each upgrade item of the bundle line item. }} 
            {{ for upgradeItem in bundleLineItem.upgradable_sellable_item_list }}
            <div class="group-upgrade-variant-section">
                {{ # Iterate through each upgrade item variant selection. }} 
                {{ for variantselection in upgradeItem.variant_selections }}
                <div class="group-variant-selection" data-product-id="{{ upgradeItem.bundle_line_item_id }}" >
                    {{ # The <label> element contains the display name of the variant. }}
                    <label class="group-variant-label">{{ variantselection.display_name }}</label>
                    {{ # The <select> element contains all the viable options for the variant selection. }}
                    <select class="group-variant-select" data-variant-key="{{ variantselection.property_name }}">
                        {{ # Iterate through the options of each variant selection. }} 
                        {{ for option in variantselection.options }}
                        {{ # Populate the option value into the <option> element. }} 
                        <option class="group-variant-select-option">{{ option.value }}</option>
                        {{ end }}
                    </select>
                </div>
                {{ end }}
            </div>
            {{ end }}
            <div class="product-link-container">
            {{ ## The <a> element is a link to the product page of the bundle line item. Link text is retrieved from the data source. 
                  It has a href attribute binding with the bundleLineItemLink observable to ensure the value is updated in the 
                  case when an upgrade item is selected. ## }}
                <a class="product-link" data-bind="attr: { href: bundleLineItemLink }">{{ i_datasource.ViewProductText }}</a>
            </div>
        </div>
        {{ # The <div> element contains the combination of variants that are valid. }}
        <div class="bundle-variant-combination-list">
            {{ # Iterate through the valid product variants. }} 
            {{ for validProductVariant in bundleLineItem.valid_product_variants }}
            {{ # The <ul> element has data attributes to store the product ID and variant ID. }}
            <ul class="bundle-variant-combination" data-product-id="{{ bundleLineItem.bundle_line_item_id }}" data-variant-id="{{ validProductVariant.variant_id }}">
                {{ # Iterate through the variants of the valid product variants. }} 
                {{ for variant in validProductVariant.variant_properties }}
                {{ # The <li> element has data attributes to store the variant key and value. }}
                <li class="bundle-variant-combination-value" data-variant-key="{{ variant.property_name }}"
                    data-variant-value="{{ variant.value }}"></li>
                {{ end }}
            </ul>
            {{ end }}
        </div>
        {{ # Iterate through each upgrade item of the bundle line item. }} 
        {{ for upgradeItem in bundleLineItem.upgradable_sellable_item_list }}
        <div class="bundle-upgrade-variant-combination-list">
            {{ # Iterate through the valid product variants. }} 
            {{ for validProductVariant in upgradeItem.valid_product_variants }}
            {{ # The <ul> element has data attributes to store the product ID and variant ID. }}
            <ul class="bundle-variant-combination" data-product-id="{{ upgradeItem.bundle_line_item_id }}" data-variant-id="{{ validProductVariant.variant_id }}">
                {{ # Iterate through the variants of the valid product variants. }} 
                {{ for variant in validProductVariant.variant_properties }}
                {{ # The <li> element has data attributes to store the variant key and value. }}
                <li class="bundle-variant-combination-value" data-variant-key="{{ variant.property_name }}"
                    data-variant-value="{{ variant.value }}"></li>
                {{ end }}
            </ul>
            {{ end }}
        </div>
        {{ end }}
    </form>
    {{ end }}
</div>

In the Product Bundle Scriban template, you access the properties and objects contained in the ProductBundleRenderingModel model through the embedded item o_model as shown in the following figure.

Diagram of the Product Bundle rendering model