Developers

JavaScript API

BundleM8's storefront JavaScript API lets developers build a completely custom bundle experience, or add behaviour around the standard form. This page is for developers comfortable editing theme code.

There are two ways to use it:

  • Build your own form with the API's methods, using the Bundle Selection block's Metadata only setting.
  • Listen for events from the standard bundle form, for example to update other parts of the page or send analytics.

Building your own form

1. Add the bundle data to the page

  1. In the theme editor, add the Bundle Selection block to your bundle product template.
  2. Turn on Metadata only in the block's Advanced settings.
  3. Make sure the BundleM8 Scripts app embed is on.

With Metadata only on, the block doesn't show a form. It adds a <bundle-metadata> element containing the bundle's data, and exposes the API.

2. Get the API

Call window.bundlem8() with the bundle's product ID:

const bundle = window.bundlem8(productId)
ParameterTypeDescription
productIdnumber or stringThe bundle product's numeric ID, such as {{ product.id }} in Liquid
indexnumberOptional. Which block to use if the same bundle appears more than once on the page, starting at 1. Defaults to the first

3. Wait for the bundle to be ready

Before using the API, wait for two things:

  1. The BundleM8 script. It loads with defer, so your theme's scripts can run before it. customElements.whenDefined('bundle-metadata') waits until it has loaded.
  2. The bundle's data. For most bundles this is already on the page. Bundles with more than 600 linked variants load their data from BundleM8 after the page opens instead. bundle.ready waits for either case.
async function loadBundle(productId) {
  await customElements.whenDefined('bundle-metadata')
  const bundle = window.bundlem8(productId)

  if (!bundle || !(await bundle.ready)) {
    // The block isn't on the page, or the bundle's data couldn't be loaded.
    return null
  }
  return bundle
}

loadBundle(productId).then((bundle) => {
  if (!bundle) {
    showMessage('This bundle is unavailable right now. Please refresh the page.')
    return
  }
  buildForm(bundle)
})

bundle.ready is a promise that resolves to true once bundle.context is available, or false if the data couldn't be loaded. For bundles whose data is on the page, it resolves straight away, so the same code works for every bundle.

Your form has to wait for a large bundle's data to arrive, so show a loading state until ready resolves:

const form = document.querySelector('#my-bundle-form')
form.setAttribute('aria-busy', 'true')
form.textContent = 'Loading bundle options…'

const bundle = await loadBundle(productId)
form.removeAttribute('aria-busy')
form.textContent = ''
if (bundle) {
  buildForm(bundle)
}

Keep using the same object

bundle.context and bundle.inventory always return the latest data, so the object returned by window.bundlem8() stays up to date after ready resolves. You don't need to call window.bundlem8() again.

4. Build your form from the bundle's data

bundle.context describes the bundle's groups and products. Render your form from it, then call the API's methods as the customer makes choices.

const bundle = await loadBundle(productId)

// Fixed products are always included. Add them first.
bundle.context.groups
  .filter((group) => group.variants.some((variant) => !variant.optional))
  .forEach((group) => bundle.addIncludedProducts(group.id))

// When the customer ticks an option
function onOptionChange(groupId, guid, checked) {
  try {
    checked ? bundle.selectProduct(groupId, guid) : bundle.removeProduct(groupId, guid)
  } catch (error) {
    // For example, "This group has reached its selection limit."
    showMessage(error.message)
  }
  priceElement.textContent = bundle.formatMoney(bundle.total())
}

// When the customer adds the bundle to their cart
addButton.addEventListener('click', () => {
  bundle.addToCart({
    quantity: 1,
    onSuccess: () => openCartDrawer(),
    onError: () => showMessage('Something went wrong. Please try again.'),
  })
})

Check the group rules yourself

The API stops customers going over a group's maximum, adding the same product twice, or choosing in a hidden nested group. It doesn't check minimum selections before adding to the cart. Check each group's min before calling addToCart().


API reference

Properties

PropertyTypeDescription
idstringThe bundle product's ID
readyPromise<boolean>Resolves to true once the bundle's data is available, or false if it couldn't be loaded. See Wait for the bundle to be ready
contextobjectThe bundle's groups, products and price range. Always the latest data. See Bundle data
inventoryarrayStock information for every variant in the bundle
selectedarrayThe products currently in the customer's bundle
branchesarrayThe condition branches the customer has chosen
configobjectThe store's money format and currency settings

addIncludedProducts(groupId)

Adds a group's fixed products to the customer's bundle. Call this for every group with fixed products that's shown to the customer, including nested groups once they become visible.

Throws an error if the group doesn't exist, or if it's a nested group whose parent has no selection.

removeIncludedProducts(groupId)

Removes a group's fixed products from the customer's bundle, along with selections in any groups nested under it.

selectProduct(groupId, guid)

Adds an optional product to the customer's bundle. guid is the product's guid from context.

Throws an error if:

  • The group or product doesn't exist, or the product is fixed.
  • The product is already selected.
  • The group has reached its maximum selections.
  • The group is nested and its parent doesn't have a matching selection.

removeProduct(groupId, guid)

Removes an optional product from the customer's bundle. Selections in nested groups that depended on it are removed too.

selectCondition(groupId, branchId)

Chooses a branch in a condition group. branchId is the branch's id from context. Throws an error if the branch doesn't exist, or if the group already has a branch chosen. Remove the current branch first.

removeCondition(groupId, branchId)

Removes a chosen branch, and clears selections in the group linked to it.

total()

Returns the price of the products currently in the customer's bundle, as a number in your store's currency. Each product's price is multiplied by its quantity.

formatMoney(amount)

Formats an amount as a price using your store's money format, converted to the customer's currency. Pass an amount in your store's currency, such as the result of total().

bundle.formatMoney(bundle.total()) // "$61.20"

addToCart(options)

Adds the customer's bundle to the cart.

OptionTypeDescription
quantitynumberHow many bundles to add. Defaults to 1
useCartAttributesbooleanAlso add each product as a visible line item property. Defaults to false. See Use cart attributes
onSuccessfunctionCalled with Shopify's cart response when the bundle is added
onErrorfunctionCalled with the error if adding fails

Bundle data

context has this shape:

{
  "groups": [
    {
      "id": 12,
      "title": "Choose 3 packs",
      "position": 0,
      "min": 3,
      "max": 3,
      "condition_group": false,
      "variants": [
        {
          "guid": "6f1c…",
          "id": "gid://shopify/ProductVariant/4455",
          "product_id": "gid://shopify/Product/3344",
          "label": "M8 Blend, Ground",
          "price": "19.20",
          "quantity": 1,
          "optional": true,
          "position": 0
        }
      ]
    }
  ]
}
FieldDescription
groups[].idThe group's ID, used with the API's methods
groups[].titleThe group's title
groups[].positionThe group's order in the bundle
groups[].min, maxThe group's minimum and maximum selections of optional products
groups[].linked_groupThe parent group's ID. Only present on nested groups
groups[].linked_productThe guid of the parent product that shows this group. Only present on nested groups set to show for one product
groups[].condition_grouptrue for condition groups
groups[].logic[0].branchesA condition group's branches. Each has an id, a label, a position and the group it shows
groups[].variants[]The group's products
variants[].guidThe product's ID within the bundle, used with the API's methods
variants[].id, product_idThe Shopify variant and product IDs
variants[].labelThe product's name in the bundle
variants[].priceThe price of one unit in the bundle, in your store's currency
variants[].quantityHow many units the customer gets
variants[].optionaltrue for optional products, false for fixed products

Groups are listed in a flat array. Use linked_group to work out which groups are nested, and only show a nested group once its parent has a selection, or once the customer has chosen the branch that links to it.


Events

The standard bundle form fires events on the bundle-selection element. They bubble, so you can listen on document.

bundle-selection-changed

Fired when the customer changes their selection or quantity, and once when the form loads.

document.addEventListener('bundle-selection-changed', (event) => {
  const { total, quantity, products } = event.bundleData
  console.log(`${products.length} products chosen, total ${total * quantity}`)
})

event.bundleData contains:

FieldDescription
totalThe bundle's price for one bundle, including fixed products, in your store's currency
priceThe price of the chosen optional products only
quantityThe quantity selected
productsThe chosen optional products, each as a group ID (g) and position (p)
includedThe fixed products included, each as a group ID (g) and variant ID (i)
branchesThe chosen condition branches

bundle-selection-validated

Fired when the customer selects Add to cart and the form checks their selection.

document.addEventListener('bundle-selection-validated', (event) => {
  if (!event.validated.valid) {
    analytics.track('bundle_validation_failed')
  }
})

event.validated contains valid (true or false) and msg, the form-level error message, if there is one.

If there's more than one bundle form on the page, check event.target to see which form fired the event.

Previous
Styling with CSS