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
- In the theme editor, add the Bundle Selection block to your bundle product template.
- Turn on Metadata only in the block's Advanced settings.
- 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)
| Parameter | Type | Description |
|---|---|---|
productId | number or string | The bundle product's numeric ID, such as {{ product.id }} in Liquid |
index | number | Optional. 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:
- 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. - 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.readywaits 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
| Property | Type | Description |
|---|---|---|
id | string | The bundle product's ID |
ready | Promise<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 |
context | object | The bundle's groups, products and price range. Always the latest data. See Bundle data |
inventory | array | Stock information for every variant in the bundle |
selected | array | The products currently in the customer's bundle |
branches | array | The condition branches the customer has chosen |
config | object | The 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.
| Option | Type | Description |
|---|---|---|
quantity | number | How many bundles to add. Defaults to 1 |
useCartAttributes | boolean | Also add each product as a visible line item property. Defaults to false. See Use cart attributes |
onSuccess | function | Called with Shopify's cart response when the bundle is added |
onError | function | Called 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
}
]
}
]
}
| Field | Description |
|---|---|
groups[].id | The group's ID, used with the API's methods |
groups[].title | The group's title |
groups[].position | The group's order in the bundle |
groups[].min, max | The group's minimum and maximum selections of optional products |
groups[].linked_group | The parent group's ID. Only present on nested groups |
groups[].linked_product | The guid of the parent product that shows this group. Only present on nested groups set to show for one product |
groups[].condition_group | true for condition groups |
groups[].logic[0].branches | A condition group's branches. Each has an id, a label, a position and the group it shows |
groups[].variants[] | The group's products |
variants[].guid | The product's ID within the bundle, used with the API's methods |
variants[].id, product_id | The Shopify variant and product IDs |
variants[].label | The product's name in the bundle |
variants[].price | The price of one unit in the bundle, in your store's currency |
variants[].quantity | How many units the customer gets |
variants[].optional | true 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:
| Field | Description |
|---|---|
total | The bundle's price for one bundle, including fixed products, in your store's currency |
price | The price of the chosen optional products only |
quantity | The quantity selected |
products | The chosen optional products, each as a group ID (g) and position (p) |
included | The fixed products included, each as a group ID (g) and variant ID (i) |
branches | The 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.
