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.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.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.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.