Skip to content

Bundles

The PDP bundle widget ("Buy together, save 10%") - renders the bundles the Storesynk Shopify

The PDP bundle widget (“Buy together, save 10%”) - renders the bundles the Storesynk Shopify app synced for the current product: one PUBLIC_READ $app:bundle metaobject per live bundle (title, percentage pricing, members + per-set quantities, date window), referenced from the product’s bundles metafield and read with the browser-safe public token. Like the collection module, it ships as its own on-demand chunk (bundle.js, loaded only when a <storesynk-bundle> is on the page - nothing to configure, the elements bundle handles it). A bundle renders only while its date window is open and every member resolves (deleted/unpublished members drop the whole bundle). Blueprint: build-patterns §2.10.

Namespace is required configuration: Storefront queries cannot use the $app: shorthand, so the app’s concrete namespace (app--<numeric-app-id>) must reach the widget - via app-namespace on <storesynk-store> (recommended; one setting serves every widget) or a per-widget namespace attribute. Neither → the widget stays [ss-empty] (SSK-211).

  • Purpose: the widget root. Queries the current product’s live bundles and clones its template once per bundle.
  • Attributes:
    • namespace : string = "" - overrides the store-level app-namespace for this widget alone.
  • Required ancestor / context: storesynk-product (consumes productContext for the product id - SSK-102) + storeContext (client, locale, appNamespace).
  • Children / template rule: TEMPLATE-CLONE - its first element child (a bundle-offer, or a wrapper containing one) is the per-bundle template (SSK-107).
  • Events / state: sets [ss-loading] during the fetch and [ss-empty] when there are no live bundles (or no namespace) - hide it via storesynk-bundle[ss-empty] { display: none }. A market/locale switch re-fetches (prices are market-aware).
  • Purpose: one bundle. Provides bundleContext (entry, members, pricing) to the displayers, member list, and add button inside; fed via provideBundle() by the widget.
  • Attributes: none authored (ss-bundle-key is SSR-set - never author it).
  • Required ancestor: storesynk-bundle (SSK-106 - only the widget clones and feeds it).
  • State it owns: the bundle’s variant selection. Each member card is a full storesynk-product, so a shopper’s pick bubbles the standard sf:variant-change up here; the offer records the selection per member and re-provides the bundle with re-keyed members + recomputed pricing - every displayer, the ×N subtotals, and the add button’s variant ids track the selection. Selection state is per clone: two bundles sharing a product select independently. A shopper can only select among the member’s merchant-allowed variants (the card is fed only those), so the add button never sends a disallowed id.
  • Purpose: the bundle’s member cards. Clones its member-card template once per member and feeds each clone’s storesynk-product via provideProduct() - the standard product displayers (show-title, show-price, show-image, product-link, change-option, …) all work inside. When the merchant restricted a member to specific variants, the card is fed only those variants - change-option offers only the allowed values (option values are derived from the allowed variants), exactly one allowed variant renders no selector, and unknown ids fall back to all variants. Also attaches a per-clone bundleMemberContext provider (the resolved member with its bundle quantity) read by the show-member-* displayers.
  • Attributes: none.
  • Required ancestor / context: bundle-offer (consumes bundleContext - SSK-105).
  • Children / template rule: TEMPLATE-CLONE - its first element child (a storesynk-product, or a wrapper containing one) is the member-card template (SSK-107). The member-card storesynk-product carries no handle/product-id and is the one sanctioned storesynk-product nesting (exempt from SSK-103). Selection updates refresh cards in place (re-cloning would wipe the shopper’s picks).
  • Variant pickers in the card: use change-option in native-<select> mode with the positional group="1"/"2"/"3" idiom (members are arbitrary products; extras self-hide). Single-variant members render no selector (the synthetic Title option is stripped from the member’s data). Sold-out combos come pre-disabled in the <select>. A member with more variants than the query cap (100) degrades to default-variant mode - no selectors - and its card gets [ss-variants-truncated]: style a “choose options on the product page” product-link affordance off it.

show-bundle-title / show-bundle-total / show-bundle-original-total / show-bundle-savings / show-bundle-percentage

Section titled “show-bundle-title / show-bundle-total / show-bundle-original-total / show-bundle-savings / show-bundle-percentage”
  • Purpose: bundle displayers - the customer-facing title; the discounted total (what the Cart Transform merge will charge); the pre-discount component sum (struck-through “was” price); the money saved; the discount percentage rendered as N% (add suffixes like ” off” via CSS ::after).
  • Attributes: none. Money displayers format via the store’s formatMoney.
  • Required ancestor / context: bundle-offer (consume bundleContext - SSK-105);
    • storeContext for money formatting.
  • State: show-bundle-original-total / show-bundle-savings / show-bundle-percentage set [ss-empty] when there are no savings (0%) - hide them via [ss-empty] { display: none }.

show-member-quantity / show-member-subtotal

Section titled “show-member-quantity / show-member-subtotal”
  • Purpose: member-card displayers - the member’s per-set quantity as “×N” (e.g. ×2), and the member’s line contribution (unit price × quantity, cent-exact with the bundle total).
  • Attributes: none.
  • Required ancestor / context: inside the member-card template within bundle-member-list (consume the per-clone bundleMemberContext - SSK-105); + storeContext for money.
  • State: show-member-quantity hides (native hidden + [ss-empty]) at quantity 1 - a single unit needs no multiplier; show-member-subtotal hides the same way when no subtotal is computable.
  • Purpose: adds every member to the cart in ONE multi-line window.Shopify.actions.updateCart({ lines }) call (each member’s selected variant - default until the shopper picks - with its bundle quantity); the app’s Cart Transform Function merges those lines into the single discounted bundle line server-side, so the cart drawer renders it natively with no extra work.
  • Attributes: none. Recommended: nest a native <button> (else the host upgrades itself to role="button" + tabindex + Enter/Space, per the action-control conventions).
  • Required ancestor / context: bundle-offer (consumes bundleContext - SSK-105).
  • Events: emits sf:add-to-cart (bubbles, composed; detail = { bundle }) on success - the cart drawer’s open-on-add works for bundle adds too. A failed add never fires it.
  • State it sets (VariantButton conventions): [ss-unavailable] on itself when some member’s selection resolved to no variant, [ss-out-of-stock] when every member has a variant but at least one is unpurchasable (mutually exclusive; the bundle stays visible either way); [ss-loading] during the add; aria-disabled="true" on the focusable control whenever blocked (clicks are ignored).

Imported from the Storesynk skill v0.1.0 (references/component-reference.md). To change this page, change it there.