Skip to main content
ctx.view is everything the widget draws. The engine has already applied every rule and worked out every price, so what you need is in the view or one lookup away. There are no conditions, reward rules, or engine state in it.

How to read it

Money

Every price is a Money:
  • Print .text. Never add, multiply, divide, or format an amount yourself.
  • was and saved are null when nothing is discounted. Draw one price then, never a struck-through duplicate.
  • Every amount in one view is in the same currency.
  • Never present the total as the checkout total. Discount codes and other apps’ discounts are invisible to the widget.

Selectors

A selector is one place the shopper picks — one product set in the editor. Iterate view.selectors (merchant order) and branch on itemType and type before reading products. A collection selector also has:

What to draw

A product selector with type: "multi" is still one product row; the shopper mixes variants, one pick per unit when unitPicks is set. Reading recipes:
  • A selector’s heading is selector.collection.title for a collection selector, else selector.products[0].title.
  • A product with no entry in selections is unpicked (quantity 0). Draw it with its defaultVariantId variant — that variant’s price, picture, and stock — and mark it selected in its <select>.
  • A product’s picture is its picked variant’s image, else its defaultVariantId variant’s.
  • Display selection.quantity. Never hardcode a starting quantity; the engine sets initial picks before the first render.
  • view.pageProductId is the product whose page shows the bundle (null off a product page). Compare it with product.id to set the page’s own product apart.
  • A row is optional when no tier’s requirements name it with a min above 0. Draw it as an add-on if the design calls for it.
  • Link to a product or collection with ctx.productUrl(handle) / ctx.collectionUrl(handle). Both return null where the surface has no pages.

Options and per-unit picks

Each product lists its options ({ name, values }, merchant order) and each variant its option values. You can draw variants two ways — pick one shape per selector:
  • One variant dropdown — <option value> is the variant id. See Data attributes.
  • One dropdown per option (Color, Size…), for one-product rows only — <option value> is the option value; read the current answer from selector.optionPicks. On each change the engine merges the pick over optionPicks and sets the selection to the one variant that carries every picked value, or to none. A value with no purchasable variant stays selectable.
When selector.unitPicks is not null, the shopper picks each unit on its own. Draw one line of option dropdowns per unit, numbered “#1”, “#2”… Read that unit’s optionPicks, and its pick from the selection whose unitIndex is the unit’s index. Put data-nameless-unit="{index}" on each of its <select>s.

Large collections

A collection selector loads its products in batches of 50. moreProducts says what’s next: While it is not none, draw one <button data-nameless-load-more="{selector.id}"> after the rows. Without it, the runtime still loads more when the last row scrolls into view — but never for rows drawn as <option>s. New rows arrive as a new render; keep each row’s id stable so rows already drawn stay put.

Selections

usedUnits is below quantity when a tier’s max capped the line — those units pay full price. discountedUnits is below usedUnits when a reward’s slice lowered only some units (“buy 3, get 1 free”). When discountedUnits is below quantity, say so — for example ctx.t("{{count}} of {{quantity}} discounted", …) — rather than letting eachNow stand for every unit.

Tiers

view.tiers is every promise the bundle makes, in the merchant’s order. Tiers with the same offerId belong together — one ladder, smallest first. Several tiers can be applied at once, one per offer. Highlight a tier with state === "applied". When several rungs of one ladder are met and none is chosen, the offer prices its highest met rung, as checkout does. Give every rung its own control so the shopper can pick a lower one.

Requirements — naming a tier

requirements is a list of alternative requirement sets. Meeting every range of any one set meets the tier — join sets with “or”. Each range is one pool:
Requirements are worked out with nothing picked, so they don’t move while the shopper picks. An empty list means no picks can meet the tier. Write the tier name from them with the numbers as variables, so the name follows the merchant’s next edit and the translation key stays the same. Never type a count into a literal: what is the headings of selectorIds joined with ctx.t("and"). Join one set’s ranges with ” + ”, and several sets with ctx.t("or").

Rewards — what a tier gives

The shopper gets every reward of exactly one group in rewardGroups. Today every tier has one group. When there are several, join them with ctx.t("or") — never as a list the shopper gets in full — and the group with state: "applied" is the one checkout charges. Each reward has id, selectorIds (what it lowers), slice, and an outcome to put into words: slice is null when the reward lowers every unit the tier used. Otherwise it is { after, maxUnits, sort }: the units it skips first, and the most it lowers after them, cheapest first. “Buy 3, get 1 free” is 100% off with { after: 3, maxUnits: 1 } — of 4 units, 1 is free. The outcome is for words only. Never work a price out of it — prices come from selections, summary, and ifChosen. A reward is applied when some selection’s rewardIds names it.

Comparing before a tap

tier.ifChosen.summary (was, now, saved, unitCount) and tier.ifChosen.selections (selector id → rows) are the bundle after a tap. Show them on a tier card so the shopper can compare. An add-on tier with nothing picked yet is priced with one unit of each, so its price shows before the pick. A chosen tier has ifChosen: null — tapping it again only clears the choice. Draw it from selector.selections and view.summary as they are. Draw a tier as a control when ifChosen is not null or chosen is true. Otherwise draw it as information, without data-nameless-tier-id. Write ctx.t("Add {{count}} more", …) from a positive toAddTotal, or name each selector from toAdd.

Summary

Take the footer from view.summary, never from your own sum of the rows.

Subscriptions

view.subscriptions is null when the bundle sells one-time only — the default. Then draw no plan control anywhere. When it is set, draw one plan <select> per picked item whose chosen variant has plans:
  • List the plans of that item’s own chosen variant, each with its name and price.text. Subscription apps mint a different plan id per variant, so never build a shared list by hand.
  • Offer a one-time option (value="") only when the product’s subscriptionOnly is false.
  • Mark selected whatever selection.planId holds. It is null (one-time) until the shopper chooses. Never preselect a plan.
  • Draw no control for a variant with empty plans.
  • The selection’s prices already reflect the chosen plan.
One shared plan control is the alternative, for a design that asks for one subscription choice for the whole bundle. Use it only while view.subscriptions.sharedPlanOffered is true — the engine sets it when every selector draws one fixed product on this page and no selector is picked unit by unit. Offer only sharedGroups. When it is empty, say in words that the items share no subscription — don’t draw an empty <select>. Never draw per-item plan controls beside the shared one. A group with several plans (monthly and fortnightly) resolves to the first plan the group declares, so the shared control can’t express delivery frequency.

Add-to-cart state

See Renderer function → Add-to-cart button.

Full type

Treat every id as an opaque string: copy it into attributes exactly as the view gives it, and never parse or rebuild it.

Examples

Collection rows

One row per product, for a collection selector whose asPageProduct is false. Every control on the row carries both the selector id and data-nameless-product-id:

Tier cards

One card per tier. The card carries only the tier id; the engine works out the quantities. Names come from requirements, sentences from outcome, prices from ifChosen, and what’s missing from toAdd. heading is the helper from the minimal example.

Per-item plan control

Only when view.subscriptions is set. Returns "" for an item with no plans.