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 aMoney:
- Print
.text. Never add, multiply, divide, or format an amount yourself. wasandsavedarenullwhen 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. Iterateview.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.titlefor a collection selector, elseselector.products[0].title. - A product with no entry in
selectionsis unpicked (quantity0). Draw it with itsdefaultVariantIdvariant — 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 itsdefaultVariantIdvariant’s. - Display
selection.quantity. Never hardcode a starting quantity; the engine sets initial picks before the first render. view.pageProductIdis the product whose page shows the bundle (nulloff a product page). Compare it withproduct.idto set the page’s own product apart.- A row is optional when no tier’s
requirementsname it with aminabove 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 returnnullwhere the surface has no pages.
Options and per-unit picks
Each product lists itsoptions ({ 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 fromselector.optionPicks. On each change the engine merges the pick overoptionPicksand sets the selection to the one variant that carries every picked value, or to none. A value with no purchasable variant stays selectable.
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:
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 inrewardGroups. 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
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
plansof that item’s own chosen variant, each with itsnameandprice.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’ssubscriptionOnlyis false. - Mark selected whatever
selection.planIdholds. It isnull(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.
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
Examples
Collection rows
One row per product, for a collection selector whoseasPageProduct 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 fromrequirements, 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 whenview.subscriptions is set. Returns "" for an item with no plans.