2.0 Beta

Smootify 2.0 is in public beta. Your 1.x store keeps working. Move when you want.

What changed

Events

The events Smootify sends when something happens on the page, how to listen with Smootify.on and once, which event to use for which task, and the ones sent on an element instead of the document.

v2.0 beta
Open .md
On this page
  1. Listen to an event
  2. Which event for what
  3. The events
  4. Loading and products
  5. Cart
  6. Customer, market and consent
  7. Wishlist
  8. Search and discovery
  9. Events sent on an element
  10. Other DOM events
  11. Example: a Magic Box title that follows its contents
  12. Example: the store credit a cart earns
  13. Where people get stuck
  14. Coming from 1.x

Smootify sends an event each time something happens: the cart changes, a product loads, the visitor logs in or changes country. Your script listens and reacts. An event only tells what happened: what your handler returns is ignored. To change what Smootify does, use a filter. Each event, with its payload, is also in the Reference under API.

Listen to an event

window.Smootify = window.Smootify || [];
window.Smootify.push((Smootify) => {
  const stop = Smootify.on("added_to_cart", ({ lines, cart }) => {
    console.log(lines.length, cart.totalQuantity);
  });
  Smootify.once("initiate_checkout", ({ cart }) => console.log(cart.id));
  // later: stop();
});
  • The name can be written with or without its smootify: prefix: "cart_updated" or "smootify:cart_updated".
  • on and once return a function that stops listening. once stops by itself after the first event.
  • { replay: true }, as the third argument of on, also calls the handler at once with the last payload of an event that already happened.
  • Every event is also a DOM event, with the payload in event.detail: document.addEventListener("smootify:cart_updated", (event) => console.log(event.detail)). Four of them are sent on an element instead of document: see Events sent on an element.
  • Listening to consent_changed is enough to make Smootify load Shopify’s consent script.

Add your listeners in a push function: they then get every event from the first product_loaded and cart_updated on. See Run your scripts with Smootify.

Which event for what

  • Start your code once everything is loaded: loaded, with { replay: true }.
  • Keep your own cart counter or total in sync: cart_updated. It also fires when the cart first loads.
  • Track an addition: added_to_cart. For the checkout: initiate_checkout.
  • React to the chosen variant: variant_changed.
  • Know whether the visitor is logged in: user_auth_change with { replay: true }, or await Smootify.getCustomer().
  • Start analytics after consent: consent_changed.

The events

Loading and products

Smootify.on("loaded",
  () => { /* … */ })
loaded

Smootify is loaded: the session, the market and the cart are known.

Payload
none
once per pageuse { replay: true }
Smootify.on("product_loaded",
  ({ product, id }) => { /* … */ })
product_loaded

A product loaded, once per product, after loaded.

product
the product, or null when Shopify does not return it: not published on the Headless channel, or not sold in the market
id
the id of the product asked for
Smootify.on("product_expired_load",
  ({ product, id }) => { /* … */ })
product_expired_load

A product shown from the cache has changed on Shopify.

product
the fresh product from Shopify
id
its id
Smootify.on("loadedAdditionalVariants",
  ({ product }) => { /* … */ })
loadedAdditionalVariants

The page's product has more than 250 variants: it now carries all of them. Once, after its product_loaded.

product
the product with every variant
once
Smootify.on("variant_changed",
  ({ element, product, variant }) => { /* … */ })
variant_changed

A product's variant changed, not at the first render.

element
the smootify-product on the page
product
its product
variant
the new variant, undefined for a combination that does not exist
document.querySelector("smootify-add-to-cart")
  .addEventListener("smootify:changed_option",
    (event) => { /* event.detail */ })
// detail: { optionChangedIndex, selectedValue, variant }
changed_option

The visitor picked an option value.

optionChangedIndex
the position of the option that changed
selectedValue
the value picked
variant
the variant it leads to, when it exists
variants
the product's variants
product
the product
on an element

In product_loaded, product is null when Shopify does not return the product: it is not published to the store’s sales channel, or not sold in the visitor’s market. In variant_changed, element is the smootify-product on the page and variant is undefined for a combination that does not exist.

Cart

Smootify.on("cart_updated",
  cart => { /* … */ })
cart_updated

Every change of the cart, the first load included.

cart
the whole cart: its lines, quantities, totals and discounts
Smootify.on("added_to_cart",
  ({ lines, cart, goToCheckout }) => { /* … */ })
added_to_cart

Lines were added, after Shopify answered.

lines
the lines added: the variant (merchandiseId), the quantity, the line attributes and the selling plan
cart
the cart after the addition
goToCheckout
true with Buy now
Smootify.on("removed_from_cart",
  lineIds => { /* … */ })
removed_from_cart

Lines were removed.

lineIds
the ids of the cart lines removed
Smootify.on("errors",
  errors => { /* … */ })
errors

Shopify refused a cart operation, or a filter threw an error.

errors
each error with its message, and the field and code when Shopify gives them
Smootify.on("initiate_checkout",
  ({ cart }) => { /* … */ })
initiate_checkout

The visitor leaves for checkout.

cart
the cart the visitor checks out
Smootify.on("cart_opened",
  ({ element }) => { /* … */ })
cart_opened

A mini cart opened. cart_closed is sent when it closes.

element
the smootify-cart that opened or closed
also cart_closed

cart_opened and cart_closed fire for every kind of mini cart (dialog, details, Webflow Dropdown, popover or your own trigger), never for a cart page. element is the smootify-cart.

Smootify.on("user_auth_change",
  customer => { /* … */ })
user_auth_change

The session is known, or changes.

customer
the logged-in customer, or null for a guest
use { replay: true }
Smootify.on("changed_country",
  ({ country, oldCountry }) => { /* … */ })
changed_country

The visitor's country changed.

country
the new country code, such as IT
oldCountry
the country before
Smootify.on("consent_changed",
  consent => { /* … */ })
consent_changed

The consent is known on the page, then at every change.

consent
loaded (Shopify's script answered), required (the banner shows), allowed (true or false per category), answers, region, saleOfDataRegion and interacted
Smootify.on("created_address",
  ({ address }) => { /* … */ })
created_address

A create-address form saved an address.

address
the address saved

The first user_auth_change goes out before the push functions run. Listen with { replay: true } to get it.

Wishlist

Smootify.on("wishlist:changed_product_state",
  ({ node, product, active }) => { /* … */ })
wishlist:changed_product_state

The visitor added (active: true) or removed a product.

node
the wishlist-toggle
product
the product added or removed
active
true when added, false when removed
Smootify.on("wishlist:changed_product",
  ({ node, product }) => { /* … */ })
wishlist:changed_product

A wishlist-toggle got its product.

node
the wishlist-toggle
product
the product it now follows
Smootify.on("wishlist:changed_variant_state",
  ({ node, variant, active }) => { /* … */ })
wishlist:changed_variant_state

The visitor added or removed a variant.

node
the wishlist-toggle
variant
the variant added or removed
active
true when added, false when removed
data-use-variants
Smootify.on("wishlist:changed_variant",
  ({ node, variant }) => { /* … */ })
wishlist:changed_variant

The toggle follows another variant.

node
the wishlist-toggle
variant
the variant it now follows
data-use-variants

node is the wishlist-toggle. More in Wishlist.

Search and discovery

document.querySelector("smootify-search-discovery")
  .addEventListener("smootify:filter_initial_change",
    (event) => { /* event.detail */ })
// detail: { name, value }
filter_initial_change

A filter was applied from selected-value or the page URL, before the first search.

name
the label of the filter
value
the value applied, or the list of values; undefined when none
on an element
document.querySelector("smootify-search-discovery")
  .addEventListener("smootify:filter_changed",
    (event) => { /* event.detail */ })
// detail: { name, value }
filter_changed

The visitor changed a filter (name: "query" for the search text).

name
the label of the filter, or query for the search text
value
the value chosen, or the list of values
on an element
document.querySelector("smootify-search-discovery")
  .addEventListener("smootify:sort_changed",
    (event) => { /* event.detail */ })
// detail: { name: "sort", value }
sort_changed

The visitor changed the sorting.

name
always sort
value
the sort option chosen
on an element

These three are sent on an element: see below.

Events sent on an element

Four events are not sent on document, and do not bubble up to it:

  • changed_option is sent on the smootify-add-to-cart form around the picker, or on the picker when it is outside the form;
  • filter_initial_change, filter_changed and sort_changed are sent on the smootify-search-discovery block, or on the smootify-search-page block.

Where you listen decides whether your handler runs:

Smootify.on(…)

On document

Not called. The event is sent on the element and does not bubble up to document. The same for a plain document listener.

addEventListener(…, true)

On document, in the capture phase

Called. With true as the third argument, the listener catches the event on its way down to the element.

el.addEventListener(…)

On the element

Called. The event is sent there: listen on the element named in the list above.

The two ways that work, in code:

document.addEventListener(
  "smootify:changed_option",
  (event) => console.log(event.detail.optionChangedIndex, event.detail.selectedValue),
  true,
);

document
  .querySelector("smootify-search-discovery")
  ?.addEventListener("smootify:filter_changed", (event) => console.log(event.detail.name, event.detail.value));

To react to a new variant, variant_changed is simpler: it is sent on document and carries the product element.

Other DOM events

Smootify also sends a few DOM events kept from 1.x. They are not in smootify.d.ts:

  • productLoaded and changeVariant on each smootify-product element, with the product or the variant in event.detail. They do not bubble: listen on the element. changeVariant also fires at the first render.
  • changePlan on each smootify-product element, when the visitor picks a selling plan or goes back to the one-time purchase. event.detail has plan (null for the one-time purchase) and variant. It does not bubble: listen on the element.
  • addon:state_change on document and on the smootify-add-to-cart form, each time an add-on is picked or dropped. event.detail has wrapper (the add-on element), product, variant and included.
  • variantsRendered on document.body, each time a variant card (smootify-variant) is filled.
  • smootify:productImagesModuleLoaded on document, each time a deprecated product="images" element draws its images. The product="gallery" element does not send it. It does not mean every image has finished downloading.
  • smootify:magic_box_change on document.body, once the box has loaded and after each change: see Example: a Magic Box title that follows its contents.
  • smootify:price_render on document, each time a smootify-price writes a new amount. event.detail.element is the price element.
  • smootify.devMetafield and smootify.renderedMetafields on document, for metafields: see Render a metafield yourself.

Example: a Magic Box title that follows its contents

smootify:magic_box_change carries element (the smootify-magic-box), currentQuantity (how many products the box holds) and cartItems, one { lineItem, variant } per product. The box element has two methods: modifyBoxTitle(title) and modifyBoxImage(url), which accepts only an image served by Shopify’s CDN (upload it under Content > Files). The new title and image are the ones the box line takes in the cart and at checkout.

document.body.addEventListener("smootify:magic_box_change", (event) => {
  const { element, currentQuantity } = event.detail;
  element.modifyBoxTitle(`Box of ${currentQuantity} wines`);
  element.modifyBoxImage(
    currentQuantity > 3 ? "https://cdn.shopify.com/…/box-large.jpg" : "https://cdn.shopify.com/…/box-small.jpg",
  );
});

Add the listener before the box loads, in the page’s head code, so it also gets the first event.

Example: the store credit a cart earns

When a Shopify Flow workflow gives store credit on paid orders, like Store credit on a paid order (5% of the subtotal from 50 up), the cart can say what this order will earn. Smootify has no attribute for it, because the rule is yours: the percentage, the minimum and whether to show it at all. A few lines on cart_updated do it.

Put the message next to the cart’s subtotal, outside the line template, and hide it in Webflow:

<p data-credit-estimate hidden>This order earns you <span data-credit-amount></span> of store credit.</p>

Then add the script, with the same numbers as your workflow:

window.Smootify = window.Smootify || [];
window.Smootify.push((Smootify) => {
  const RATE = 0.05; // the share your workflow credits
  const MINIMUM = 50; // its minimum subtotal
  const SHOP_CURRENCY = "EUR"; // the currency of that minimum

  Smootify.on("cart_updated", (cart) => {
    const { amount, currencyCode } = cart.cost.subtotalAmount;
    const subtotal = Number(amount);
    const earns = currencyCode === SHOP_CURRENCY && subtotal >= MINIMUM;
    document.querySelectorAll("[data-credit-estimate]").forEach((message) => {
      message.hidden = !earns;
      const value = message.querySelector("[data-credit-amount]");
      if (earns && value) value.textContent = Smootify.formatMoney((subtotal * RATE).toFixed(2));
    });
  }, { replay: true });
});
  • It is an estimate. The credit is what the workflow gives when the order is paid. Change the numbers in both places, or the cart promises what the workflow does not give.
  • One currency. The minimum is in the shop’s currency, so the message shows only when the visitor’s cart is in that currency. In other markets it stays hidden.
  • { replay: true } shows it at once for the cart that already loaded.

Where people get stuck

Check these first

  • Smootify.on("changed_option") never runs. The event is sent on the form or the picker, not on document. Listen in the capture phase, or use variant_changed.
  • Smootify.on("filter_changed") never runs. The same: the event is sent on the search and discovery block. Listen on that element.
  • The first user_auth_change is missed. It goes out before the push functions run. Add { replay: true }.
  • A script added by a tag manager never hears loaded. The event fired before the script arrived. Add { replay: true }.
  • Returning a value from a handler changes nothing. Events only report. To change the lines, the checkout address or a draft order, use a filter.
  • A wishlist handler runs twice. It listens to both the 1.x name (smootify_wishlist:…) and the 2.0 name, and Smootify sends both. Keep the 2.0 name.

Coming from 1.x

The names and the payloads of the events are the same as in 1.x, with these changes:

  • Wishlist events are renamed smootify:wishlist:…, like every other event. The 1.x names (smootify_wishlist:changed_product, changed_product_state, changed_variant, changed_variant_state) are still sent, with the same payload, until 3.0.
  • New in 2.0: cart_opened, cart_closed, consent_changed and variant_changed.
  • Now sent: created_address and the two wishlist:changed_variant events, which 1.x declared and never sent.
  • One event per thing that happened. product_loaded goes out once per product, changed_option only when the visitor picks a value, sort_changed not for a selected-value preselection, and smootify:price_render only when the amount changes. A script that counted events to detect the end of loading should listen to loaded.
  • smootify:color_patterns is no longer sent: 2.0 does not load the shop’s colour patterns.
  • Smootify.on and once are new: in 1.x, scripts used document.addEventListener, which still works.

    ↑ ↓ to moveEnter to openEsc to close