Home
Wiki
Home
Wiki
  1. 2. Components
  • Back to home
  • 1. Themes
  • Vs Code
    • Getting Started
  • Kitchenware
    • Layout
      • New Layout
      • Legacy Layout
    • Components
      • Announcement
      • Banner Carousel
      • Banner With Products Carousel
      • Blog Category List
      • Blog List
      • Brand List
      • Brands Carousel
      • Breadcrumb
      • Call To Action
      • Cart
      • Categories List
      • Change Password
      • Checkout
      • Cookie Manager
      • Filter list
      • Footer
      • Forgot Password
      • Form
      • Hero Carousel
      • Icon Block
      • Invitation
      • Last Visited Products
      • Layout
      • Login
      • Map
      • Nav Bar
      • Offer
      • Product Attachments
      • Product Attributes
      • Product Documentation
      • Product Expected
      • Product Modal
      • Products Block
      • Products Carousel
      • Product Single
      • Profile
      • Quote
      • Register
      • Related Products
      • Search
      • Stores
      • Subscribe Newsletter
      • Text with Image
      • Top Bar
      • Video
    • Reusables
      • Getting Started
    • Assets
      • Getting Started
    • SDK
      • Products
        • _findProductsByCategory
        • _findProductsByIds
        • _findProductsByTitle
        • _findProductsByFilter
        • _findProductsByCriteria
        • _findProductsAndCalculate
        • _findProductsThenCalculate
        • _getProductAttributeSet
        • _setLastVisited
      • Categories
        • _findCategoryTreeById
        • _findCategoriesByIds
        • _findCategoryByAlias
        • _findCategoryTreeByAlias
        • _getCategoryContent
      • Collections
        • _getCollectionContent
        • _findCollectionsByIds
        • _findCollectionsByIdsThenCalculate
      • Brands
        • _getBrandContent
        • _findBrandsByIds
      • Cart
        • _addToCartMulti
        • _addToCart
        • _setCart
        • _clearCart
        • _setCartListener
        • _removeFromCart
        • _calculateCart
      • Checkout
        • _startCheckout
        • _updateCheckout
        • _completeCheckout
      • Shopping Lists
        • _getShoppingLists
        • _updateShoppingList
        • _createShoppingList
        • _deleteShoppingList
        • _getShoppingListByAlias
      • Navigation
        • _getFooterMenu
        • _getHeaderMenu
      • Users
        • _getUserById
      • Utils
        • _calculateCurrency
        • _getCurrencySymbol
        • _getCulture
        • _subscribeToNewsletter
        • _findUnitsByIds
  • Noir
    • 0. Introduction
    • 6. FAQ
    • 1. Structure
      • _Overview
      • LayoutA.liquid
      • ComponentsList.liquid
      • Metas.liquid
      • CssVariables.liquid
      • Json.liquid
      • GoogleTagManager.liquid
      • StagingButton.liquid
    • 2. Components
      • _Overview
      • Announcement
      • BannerCarousel
      • BlogCategoryList
      • BlogHighlights
      • BlogList
      • BrandList
      • Breadcrumb
      • Cart
      • CategoriesList
      • ChangePassword
      • Checkout
      • CookieManager
      • CustomTable
      • Faq
      • FilterList
      • Footer
      • ForgotPassword
      • Form
      • IconBlock
      • Invitation
      • LastVisitedProducts
      • Login
      • Map
      • NavBar
      • Offer
      • ProductAttachments
      • ProductAttributes
      • ProductComparison
      • ProductDocumentation
      • ProductMixList
      • ProductsBlock
      • ProductsCarousel
      • ProductSingle
      • Profile
      • Quote
      • Register
      • RelatedProducts
      • SingleBlog
      • Stores
      • TextWithImage
      • ThankYouPage
      • TopBar
      • Wishlist
      • OrderReviews
      • ProductReviews
    • 3. Reusables
      • _Overview
      • Addresses
      • AddressForm
      • AnnouncementModal
      • BackToTop
      • BillingRetail
      • BlogPostingStructuredData
      • BrandListStructuredData
      • CollectionStructuredData
      • Company
      • General
      • GroupChildrenTable
      • Login
      • LoginModal
      • MonthlyTransactions
      • Offers
      • Orders
      • Payment
      • ProductAttachments
      • ProductAttributes
      • ProductAvailabilityAction
      • ProductComparisonButton
      • ProductComparisonFloatingButton
      • ProductGridItem
      • ProductGridItemClassic
      • ProductListItem
      • ProductModal
      • ProductStructuredData
      • ProfileInfo
      • PromptModal
      • Quotes
      • Register
      • Services
      • Shipping
      • ShoppingLists
      • ShoppingListsButton
      • ShoppingListsNavbar
      • StoresStructuredData
      • Toast
      • Transactions
      • Users
      • VariantContent
      • VariantContentClassic
      • WishlistButton
      • ProductRating
    • 4. Assets
      • Fonts
      • Images
      • Templates
      • Javascript
        • _Overview
        • theme.js
      • Css - Scss
        • _Overview
        • ThemeClasses
    • 5. SDK
      • _Overview
      • LiquidGlobals
      • ServicesSDK
  1. 2. Components

ProductSingle

Purpose#

The ProductSingle component renders the product details page.
It:
renders a product gallery (main + thumbnails) and a sticky header summary on scroll
renders product title, tags, SKU/MPN (optionally), price, and add-to-cart actions
supports product actions (comparison, wishlist, shopping lists) when feature flags are enabled
supports variant selection and quantity rules (including a Matrix-style quantity table)
sends GA4 viewItem for the product

Inputs (model contract)#

Model shape (storefront example)#

{
  "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"
      },
      "..."
    ],
    "rating": {
      "average": 4.5,
      "count": 12,
      "distribution": {
        "oneStar": 0,
        "twoStars": 1,
        "threeStars": 1,
        "fourStars": 2,
        "fiveStars": 8
      }
    }
  },
  "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",
    "...": "..."
  }
}

Required fields#

settings.id
productData
The component assumes productData is a full product view model.

Optional fields#

Flags that affect template rendering:
showGalleryInline
mainGalleryToLeft (used only when showGalleryInline == true)
showAttributes
showAttachments
variantsDisplayType (Simple or Matrix)
productData.mediaItems[]
When empty/missing, the template renders a fallback “no image” slide.
productData.tags[]
productData.isMpnVisible, productData.isSkuVisible
productData.dimension1, productData.dimension2
When present, the UI renders variant selectors / matrix table.
groupProductsData
When present (not null/empty), some sticky add-to-cart UI is suppressed.
productData.rating
How the product's published reviews rate it: average (two decimals), count and the per-star
distribution (oneStar to fiveStars), for a summary next to the title.
Null while the product has no published review, and while GlobalData.Settings.ShowRatingOnProductPage
is false (the store does not show the rating in the product details, does not collect reviews, or the
company lacks the Product reviews module). When the setting is on, the ProductRating reusable
renders even without reviews (gray stars and (0)); model.Settings.ProductRatingReusable swaps it.
The product's reviews are not part of this component: the ProductReviews component shows them in
a section of their own, with the rating, whatever ShowRatingOnProductPage says.
translations.*
Many user-facing strings come from translations (only a small sample is shown above).
The component settings also include:
model.Settings.CssClass
model.Settings.VariantsDisplayType
Root.Page.Metas (Liquid)
Optional page metadata available in the global render context; used by the template for SEO/meta tags (Open Graph, structured data, etc.).

Template behavior (Liquid + Alpine)#

Describe rendering flow, state branches, and key Liquid/Alpine behavior.

JavaScript#

Global object#

Components/ProductSingle/Default.js exposes:
Liquid calls it on page load:
productsingledefault.init({{ product | serialize | escape }});

init#

Builds a minimal GA item object (id/title/price/initialPrice).
Sends GA4 viewItem for the product.
Key behaviors / edge cases
Price selection:
if product.hasPriceRange → uses product.startPrice
otherwise → uses product.productVariants[0].finalPrice
Notes:
If you change where the “current variant price” lives, update this GA mapping.

matrixTable#

Returns an Alpine state object for the Matrix variant UI (quantity-per-dimension grid).
Manages quantities keyed by rendered matrix coordinates and prepares payloads for a “multiple add to cart” request.
Key behaviors / edge cases
Supports 1D and 2D matrices:
1D: only one dimension present (rows are null)
2D: both dimensions present
If Color is on dimension2, it swaps row/column axes so Color becomes rows.
Uses per-cell debounce + axios cancel tokens when checkQuantity is enabled.
Notes:
The mapping between dimension items and UI axes is subtle (because of the Color swap). Be careful when changing dimension types.

getKey#

Builds a stable string key for a matrix cell.
Builds a stable string key for a matrix cell.
Key behaviors / edge cases
1D keys are just the column id.
2D keys are ${rowId}_${columnId}.

getVariantByMatrix#

Finds the product variant object that corresponds to the provided matrix coordinates.
Finds the product variant object that corresponds to the provided matrix coordinates.
Key behaviors / edge cases
1D: matches either dimension1ItemId or dimension2ItemId.
2D: matches both dimension ids, respecting the Color swap rule.

init#

Computes the matrix axes (rows, columns) based on product.dimension1 / product.dimension2.
Initializes quantities for each variant.
Initializes rowQuantities in 2D mode.
Computes the matrix axes (rows, columns) based on product.dimension1 / product.dimension2.
Initializes quantities for each variant.
Initializes rowQuantities in 2D mode.
Calls buildMatrix() to build the initial matrix, then hydrateConstraints() to load personalized quantity constraints.

buildMatrix#

Builds rows/columns/rowName/columnName from product.dimension1/product.dimension2, applying the Color-swap rule (Color always renders as rows in 2D mode).
Seeds quantities for every variant, keyed by matrix coordinates (getKey), and recomputes rowQuantities per row.
Key behaviors / edge cases
Only seeds a quantity when the coordinate key is still undefined; re-running buildMatrix() after async constraint hydration never resets quantities a user already typed while hydration was in flight.
Runs again after hydrateConstraints() resolves, so the matrix reflects real per-visitor constraints without losing in-progress input.

hydrateConstraints#

Hydrates personalized quantity constraints client-side via servicesreusabledefault.hydrateProductConstraints(product), since the server renders the matrix from a cached, personalization-free payload.
On success or failure, reassigns this.variants from the shared product.productVariants reference and calls buildMatrix() again so min/step/max and validity reflect the real constraints.
Key behaviors / edge cases
Logs a console error on failure but still rebuilds the matrix, so the UI never gets stuck without quantities.
The server revalidates the real constraints again on the actual add-to-cart call, so this hydration is a UX optimization, not a security boundary.

updateRowQuantities#

Computes the total quantity across all columns for a single row.
Computes the total quantity across all columns for a single row.

updateVariantsToSend#

Keeps variantsToSend in sync with the current quantities (only entries with quantity > 0).
Keeps variantsToSend in sync with the current quantities (only entries with quantity > 0).

getTotalQuantity#

Sums all quantities values.
Sums all quantities values.

getTotalPrice#

Calculates the total value based on each cell quantity * the matched variant finalPrice.
Calculates the total value based on each cell quantity * the matched variant finalPrice.
Key behaviors / edge cases
Returns a string with 2 decimals (toFixed(2)).

getVariant#

Convenience wrapper for getVariantByMatrix(...).
Convenience wrapper for getVariantByMatrix(...).

debounceUpdateProduct#

Cancels any scheduled update for the same cell and schedules a new one.
Cancels the in-flight axios request for the same cell (if present).
Cancels any scheduled update for the same cell and schedules a new one.
Cancels the in-flight axios request for the same cell (if present).

updateQuantityMultiple#

Updates per-variant quantities.
When checkQuantity == true, calls servicesreusabledefault.updateProductQuantity(...) to validate/normalize requested quantities and update constraints.
Updates per-variant quantities.
When checkQuantity == true, calls servicesreusabledefault.updateProductQuantity(...) to validate/normalize requested quantities and update constraints.
Key behaviors / edge cases
Uses axios.CancelToken.source() per matrix cell to avoid races.
Clears toasts (Alpine.store("toast").removeAll()) before per-cell updates.
On API failure, shows a toast with updateProductQuantityErrorMessage.

isOutOfStock#

Returns whether the cell variant is out of stock (based on variant availability / stock flags).
Treats a variant as out of stock when its quantityConstraints.additive.isValid is false.

isNotifyState#

Returns true when Stock Alerts is active and the cell's variant exists but is out of stock.
In that state the template shows the shared Reusables/ProductAvailabilityAction "Notify me" button instead of the quantity stepper.

getQuantity#

Returns the quantity for a given matrix cell.
Reads the quantity for a given matrix cell.

handleMultiAddToCart#

Sends a “multiple add to cart” request for the selected variants.
Sends a bulk add-to-cart request via servicesreusabledefault.addMultipleToCart(...).
On success, resets quantities based on the returned cart items’ minimum constraints.
Key behaviors / edge cases
Builds a variations payload that includes dimension text values (dim1Value, dim2Value) when dimensions exist.
Shows an error toast on failure.

increaseProductQuantityMultiple#

Increases quantity for the given cell while enforcing step/min/max rules.
Increments a cell quantity, respecting constraint:
minimum
maximum
step

decreaseProductQuantityMultiple#

Decreases quantity for the given cell while enforcing min rules.
Decrements a cell quantity by step; when it drops below minimum, it becomes 0.

groupTable#

Returns an Alpine state object for the group-variants table (products sold as a group of related variants shown in one table, distinct from the dimension Matrix above).
Layered as x-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.
Key behaviors / edge cases
The server-rendered (cached) markup does not include personalized quantity constraints; this component hydrates them client-side per group variant.

init#

Seeds every group variant with permissive default constraints (servicesreusabledefault._applyDefaultConstraints(groupVariant)) before async hydration.
Key behaviors / edge cases
Without this seed, the row would visually look enabled (since 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.
Calls hydrateConstraints() after seeding, and hydratePrices() when pricePending was passed as true.

hydratePrices#

Replaces the server-rendered Shopranos prices with personalized/ERP prices by calling servicesreusabledefault.hydrateProductPrices(groupVariant) for every group variant in parallel (the service batches them into one request).
Reassigns this.groupVariants so Alpine recomputes, then clears groupPricesPending.
Logs a console error on failure; the server-rendered prices stay visible.

hydrateConstraints#

Hydrates real per-visitor quantity constraints for every group variant in parallel via servicesreusabledefault.hydrateProductConstraints(groupVariant).
Reassigns this.groupVariants = [...this.groupVariants] afterwards so Alpine getters reading nested constraint fields recompute.
Key behaviors / edge cases
Logs a console error per failed variant but does not block the others; the seeded defaults from init() remain in place for any variant whose hydration failed.
The server still revalidates the real constraints on the actual add-to-cart call.

gv(variantId)#

Resolves the group variant whose primary product variant (productVariants[0].id) matches the given variantId.

gc(variantId)#

Resolves the additive quantity constraints for a given variant id, returning the seeded/hydrated constraints or a permissive default ({ minimum: 1, maximum: null, step: 1, isValid: true }) when not yet available.

gp#

gp(variantId) resolves the priced product variant (productVariants[0]) of the matching group variant, or {} when none matches.
Shows server-rendered values until hydratePrices() replaces them.

hasDiscount#

hasDiscount(variantId) returns true when the priced variant has an originalPrice greater than its finalPrice.

isOutOfStock#

isOutOfStock(variantId) returns true when the resolved constraints (gc) are not valid.

isPendingState#

Returns true while Stock Alerts is active and constraints are still hydrating (constraintsPending).
The template shows a pending placeholder instead of the quantity/add-to-cart controls or the notify button, so an out-of-stock row never flashes as orderable.

isNotifyState#

isNotifyState(variantId) returns true when Stock Alerts is active, hydration has finished, the group variant exists and is out of stock.
In that state the shared Reusables/ProductAvailabilityAction "Notify me" button replaces the quantity/add-to-cart controls.

Stock loading#

Stock is loaded client-side through servicesreusabledefault.hydrateProductStock; the server-rendered stockAvailability is no longer used for display.
Main product: shows the image (outside the colored badge) and label of currentStock() (the selected variant, else the only variant, else the product-level availability; Varies shows -), and {{ model.Translations.StockQuantity }}: + stockQuantityText() once stockPending is false.
Each Alpine scope (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.

Global Alpine stores (used by ProductSingle)#

Alpine.store("toast") (user feedback)#

The Matrix logic uses the toast store to show errors when API calls fail:
clears previous toasts before an update attempt (removeAll())
shows an error toast when updateProductQuantity(...) fails
shows an error toast when addMultipleToCart(...) fails

Services / API calls#

servicesreusabledefault.updateProductQuantity(...)
Used when Matrix mode validates a requested quantity.
servicesreusabledefault.addMultipleToCart(...)
Used to add multiple variants to cart in one request.
servicesreusabledefault.hydrateProductConstraints(product)
Called on mount to load personalized quantity constraints client-side (non-blocking). The server renders from a cached view model; constraints are fetched separately per product.
servicesreusabledefault._applyDefaultConstraints(product)
Called as a fallback when hydrateProductConstraints fails, to keep the product orderable with permissive defaults.

Dependencies#

Alpine.js
axios (for cancel tokens)
Theme utilities:
sendGAEvent(...)
Alpine stores:
toast

Notes#

The Liquid template uses productlistitemreusabledefault.initComponent(...) for the core product UI state (variant selection, quantity display, add-to-cart interactions, sticky state).
In the Liquid template you’ll see it referenced via Alpine expressions like variantOrDefault(...), canAddToCart(), handleAddToCart(), and updateStickyState().
These are not implemented in Components/ProductSingle/Default.js; they come from the Reusables\\ProductListItem JS.
productsingledefault only adds:
GA4 viewItem (init(product))
Matrix-table logic + API calls for quantity validation / multi-add (matrixTable(...))
The template reads feature flags from GlobalData.Settings (wishlist, comparison, shopping lists) to conditionally render action buttons.
The template renders product.description as raw HTML.

Extras#

Template behavior (Liquid + Alpine)#

Computes the image layout class based on showGalleryInline + mainGalleryToLeft.
Calls swiperInitSingleProductImages(id, showGalleryInline) to initialize the image gallery swipers.
Renders a sticky header summary with:
a fallback image if the product has no media
title, price, and add-to-cart button
sticky navigation buttons to sections that exist on the page (description, attributes, documentation)

Loading state#

The section has no 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.
Modified at 2026-11-02 14:03:45
Previous
ProductsCarousel
Next
Profile
Built with