viewItem for the product{
"showAttributes": false,
"showAttachments": false,
"showGalleryInline": true,
"mainGalleryToLeft": true,
"variantsDisplayType": "Simple",
"productData": {
"id": "Product Id",
"title": "Sample product title",
"tags": [
"sample-tag",
"..."
],
"description": "<p>Sample description</p>",
"link": "product/sample-product",
"isMpnVisible": true,
"isSkuVisible": true,
"hasPriceRange": false,
"maxPrice": 69.9,
"minPrice": 69.9,
"startPriceText": "Sample price text",
"startPrice": 69.9,
"vatIncluded": true,
"inWishlist": false,
"extraFields": [],
"dimension1": {
"attributeId": "Attribute Id",
"name": "Sample text",
"usedAsFilter": true,
"displayOnProduct": true,
"displayOnList": true,
"displayOnCompare": true,
"type": "Color",
"items": [
{
"id": "Attribute Item Id",
"value": "Sample text",
"textColor": "#000000"
},
"..."
]
},
"dimension2": {
"attributeId": "Attribute Id",
"name": "Sample text",
"usedAsFilter": true,
"displayOnProduct": true,
"displayOnList": true,
"displayOnCompare": true,
"type": "Size",
"items": [
{
"id": "Attribute Item Id",
"value": "S"
},
"..."
]
},
"attachments": [
{
"alt": "sample.pdf",
"link": "https://example.com/media/sample.pdf",
"title": "sample.pdf",
"icon": {
"alt": "sample.pdf",
"link": "https://example.com/media/sample.pdf"
}
},
"..."
],
"attributes": [
{
"name": "Sample text",
"attributeItemValue": "Sample text"
},
"..."
],
"icoTags": [
{
"name": "Sample text",
"icon": {}
},
"..."
],
"labels": [
{
"name": "Sample text",
"icon": {}
},
"..."
],
"productVariants": [
{
"id": "Variant Id",
"dimension1ItemId": "Attribute Item Id",
"dimension2ItemId": "Attribute Item Id",
"title": "",
"canOrder": true,
"startQuantity": 1,
"price": 69.9,
"initialPrice": 90,
"finalPriceText": "Sample price text",
"finalPrice": 69.9,
"originalPriceText": "Sample price text",
"originalPrice": 90,
"mediaItem": {
"id": "Media Item Id"
},
"stockAvailability": {
"label": "OutOfStock"
},
"quantityConstraints": {
"additive": {
"minimum": 1,
"maximum": 999999,
"step": 1,
"isValid": true
},
"absolute": {
"minimum": 1,
"maximum": 999999,
"step": 1,
"isValid": true
}
},
"shoppingLists": []
},
"..."
],
"mediaItems": [
{
"id": "Media Item Id",
"link": "https://example.com/media/sample.jpg",
"alt": "sample.jpg"
},
"..."
]
},
"groupProductsData": null,
"name": "ProductSingle",
"view": "Default",
"section": "SectionA",
"settings": {
"additionalFields": [],
"showAttributes": false,
"showAttachments": false,
"showGalleryInline": true,
"mainGalleryToLeft": true,
"variantsDisplayType": "Simple",
"id": "Component Id",
"section": "SectionA",
"type": "NoirProductSingle",
"name": "ProductSingle",
"configuredInContentApi": true,
"view": "Default",
"displayName": "Sample text",
"cssClass": "",
"header": "",
"alignment": "Left"
},
"translations": {
"productImage": "Sample text",
"viewImage": "Sample text",
"oneSize": "Sample text",
"...": "..."
}
}settings.idproductDataproductData is a full product view model.showGalleryInlinemainGalleryToLeft (used only when showGalleryInline == true)showAttributesshowAttachmentsvariantsDisplayType (Simple or Matrix)productData.mediaItems[]productData.tags[]productData.isMpnVisible, productData.isSkuVisibleproductData.dimension1, productData.dimension2groupProductsDatatranslations.*model.Settings.CssClassmodel.Settings.VariantsDisplayTypeRoot.Page.Metas (Liquid)Components/ProductSingle/Default.js exposes:productsingledefault.init({{ product | serialize | escape }});viewItem for the product.product.hasPriceRange → uses product.startPriceproduct.productVariants[0].finalPricenull)checkQuantity is enabled.${rowId}_${columnId}.dimension1ItemId or dimension2ItemId.rows, columns) based on product.dimension1 / product.dimension2.quantities for each variant.rowQuantities in 2D mode.rows, columns) based on product.dimension1 / product.dimension2.quantities for each variant.rowQuantities in 2D mode.buildMatrix() to build the initial matrix, then hydrateConstraints() to load personalized quantity constraints.rows/columns/rowName/columnName from product.dimension1/product.dimension2, applying the Color-swap rule (Color always renders as rows in 2D mode).quantities for every variant, keyed by matrix coordinates (getKey), and recomputes rowQuantities per row.undefined; re-running buildMatrix() after async constraint hydration never resets quantities a user already typed while hydration was in flight.hydrateConstraints() resolves, so the matrix reflects real per-visitor constraints without losing in-progress input.servicesreusabledefault.hydrateProductConstraints(product), since the server renders the matrix from a cached, personalization-free payload.this.variants from the shared product.productVariants reference and calls buildMatrix() again so min/step/max and validity reflect the real constraints.variantsToSend in sync with the current quantities (only entries with quantity > 0).variantsToSend in sync with the current quantities (only entries with quantity > 0).quantities values.quantities values.finalPrice.finalPrice.toFixed(2)).getVariantByMatrix(...).getVariantByMatrix(...).checkQuantity == true, calls servicesreusabledefault.updateProductQuantity(...) to validate/normalize requested quantities and update constraints.checkQuantity == true, calls servicesreusabledefault.updateProductQuantity(...) to validate/normalize requested quantities and update constraints.axios.CancelToken.source() per matrix cell to avoid races.Alpine.store("toast").removeAll()) before per-cell updates.updateProductQuantityErrorMessage.quantityConstraints.additive.isValid is false.true when Stock Alerts is active and the cell's variant exists but is out of stock.Reusables/ProductAvailabilityAction "Notify me" button instead of the quantity stepper.servicesreusabledefault.addMultipleToCart(...).variations payload that includes dimension text values (dim1Value, dim2Value) when dimensions exist.minimummaximumstepx-data on the table inside the root ProductListItem reusable scope, so cart methods (canAddToCart, decreaseQuantity, increaseQuantity, updateQuantity, handleAddToCart, isAdding, quantityCalculating) still resolve to the parent component through Alpine's scope chain — only the constraint-hydration/lookup helpers (gv, gc, hydrateConstraints) live here.servicesreusabledefault._applyDefaultConstraints(groupVariant)) before async hydration.gc() falls back to a permissive default) but clicks would no-op until hydration finished, because the parent's handleAddToCart/canAddToCart treat a missing quantityConstraints.additive.isValid as false.hydrateConstraints() after seeding, and hydratePrices() when pricePending was passed as true.servicesreusabledefault.hydrateProductPrices(groupVariant) for every group variant in parallel (the service batches them into one request).this.groupVariants so Alpine recomputes, then clears groupPricesPending.servicesreusabledefault.hydrateProductConstraints(groupVariant).this.groupVariants = [...this.groupVariants] afterwards so Alpine getters reading nested constraint fields recompute.init() remain in place for any variant whose hydration failed.productVariants[0].id) matches the given variantId.{ minimum: 1, maximum: null, step: 1, isValid: true }) when not yet available.gp(variantId) resolves the priced product variant (productVariants[0]) of the matching group variant, or {} when none matches.hydratePrices() replaces them.hasDiscount(variantId) returns true when the priced variant has an originalPrice greater than its finalPrice.isOutOfStock(variantId) returns true when the resolved constraints (gc) are not valid.true while Stock Alerts is active and constraints are still hydrating (constraintsPending).isNotifyState(variantId) returns true when Stock Alerts is active, hydration has finished, the group variant exists and is out of stock.Reusables/ProductAvailabilityAction "Notify me" button replaces the quantity/add-to-cart controls.servicesreusabledefault.hydrateProductStock; the server-rendered stockAvailability is no longer used for display.currentStock() (the selected variant, else the only variant, else the product-level availability; Varies shows -), and {{ model.Translations.StockQuantity }}: + stockQuantityText() once stockPending is false.matrixTable, groupTable) has its own formatStockQuantity and stockTextFor helpers; the Services reusable only calls the API.matrixTable: hydrateStock(), stockPending and stockQuantityText(variant). Each matrix cell (2D and 1D) shows the label and quantity of its variant.groupTable: hydrateStock(), stockPending, gs(vid) (stock availability) and gq(vid) (formatted quantity), used by desktop and mobile group rows.removeAll())updateProductQuantity(...) failsaddMultipleToCart(...) failsservicesreusabledefault.updateProductQuantity(...)servicesreusabledefault.addMultipleToCart(...)servicesreusabledefault.hydrateProductConstraints(product)servicesreusabledefault._applyDefaultConstraints(product)hydrateProductConstraints fails, to keep the product orderable with permissive defaults.sendGAEvent(...)toastproductlistitemreusabledefault.initComponent(...) for the core product UI state (variant selection, quantity display, add-to-cart interactions, sticky state).variantOrDefault(...), canAddToCart(), handleAddToCart(), and updateStickyState().Components/ProductSingle/Default.js; they come from the Reusables\\ProductListItem JS.productsingledefault only adds:viewItem (init(product))matrixTable(...))GlobalData.Settings (wishlist, comparison, shopping lists) to conditionally render action buttons.product.description as raw HTML.showGalleryInline + mainGalleryToLeft.swiperInitSingleProductImages(id, showGalleryInline) to initialize the image gallery swipers.productSingle--loading modifier, so product details render directly. The gallery uses the shared Swiper skeleton until Swiper initializes. When the main gallery has at most one slide, swiperInitSingleProductImages adds swiper-disabled to reveal it without initializing Swiper; custom static galleries must preserve this opt-out.