GlobalData.Settings.ShowProductComparison is enabled{
"products": [
{
"vatIncluded": true,
"vatRate": 24,
"isMpnVisible": true,
"isSkuVisible": true,
"title": "Sample product title",
"subTitle": "Sample text",
"description": "<p>Sample description</p>",
"alias": "sample-product-alias",
"status": "Active",
"categoryId": "Category Id",
"categoryName": "Sample category name",
"categoryLink": "/category/sample",
"tags": [
"sample-tag",
"..."
],
"pathCategories": [
"Path Category Id",
"..."
],
"attributeSetId": "Attribute Set Id",
"id": "Product Id",
"companyId": "Company Id",
"link": "product/sample-product",
"mediaItems": [
{
"id": "Media Item Id",
"link": "https://example.com/media/sample.jpg",
"position": 0,
"alt": "sample.jpg",
"mediaType": "Image"
},
"..."
],
"productVariants": [
{
"quantityConstraints": {
"additive": {
"minimum": 1,
"maximum": 999999,
"step": 1,
"isValid": true
},
"absolute": {
"minimum": 1,
"maximum": 999999,
"step": 1,
"isValid": true
}
},
"id": "Variant Id",
"mediaItem": {},
"title": "Sample text",
"mpn": "Sample text",
"sku": "Sample text",
"unitPrice": 70,
"price": 70,
"quantity": 0,
"retail": {
"price": 70,
"unitId": "(UNDEFINED)",
"unitPrice": 70
},
"dimension1ItemId": "Attribute Item Id",
"dimension2ItemId": "Attribute Item Id",
"sellOutOfStock": true,
"requiresShipping": false,
"translation": {},
"canOrder": true,
"bundleItems": [],
"shoppingLists": [],
"startQuantity": 1,
"finalPriceText": "Sample price text",
"finalPrice": 70
},
"..."
],
"attributes": [
{
"attributeId": "Attribute Id",
"attributeItemId": "Attribute Item Id",
"attributeItemValue": "Sample text",
"name": "Sample text",
"slug": "sample-slug",
"usedAsFilter": false,
"displayOnProduct": true,
"displayOnList": true,
"displayOnCompare": true
},
"..."
],
"attributeSet": {
"id": "Attribute Set Id",
"companyId": "Company Id",
"title": "Sample text",
"description": "Sample text",
"groups": [
{
"title": "Sample text",
"id": "Attribute Group Id",
"position": 0,
"items": [
{
"attributeId": "Attribute Id",
"name": "Sample text",
"value": "Sample text",
"code": "sample-code",
"usedAsFilter": false,
"displayOnProduct": true,
"displayOnList": true,
"displayOnCompare": true,
"slug": "sample-slug",
"position": 0
},
"..."
]
},
"..."
],
"updateDate": "2025-01-01T00:00:00.0000000+00:00",
"insertDate": "2025-01-01T00:00:00.0000000+00:00"
}
},
"..."
],
"name": "ProductComparison",
"view": "Default",
"section": "SectionA",
"settings": {
"id": "Component Id",
"section": "SectionA",
"type": "NoirProductComparison",
"name": "ProductComparison",
"configuredInContentApi": true,
"view": "Default",
"displayName": "",
"cssClass": ""
},
"translations": {
"compare": "Sample text",
"clearList": "Sample text",
"modalTitleText": "Sample text",
"...": "..."
}
}settings.idsettings.cssClassproducts[]translations.*Components/ProductComparison/Default.js defines a global singleton + Alpine factory:<section
x-data='productcomparisondefault.initComponent("{{ id }}", {{ productsCount }}, {{ products | serialize | escape }}, "{{ model.Translations.Compare }}")'
>initComponent(id, productsCount, products, headerTitle)headerHeight is read from the <header> element; if layout changes, sticky offsets may be wrong.slidesPerView depends on viewport width (>= 1440 → 4 else 2).#carousel-sticky#product-comparison-list-<id>#product-comparison-attribute-list-<id>.attribute-value + data-attributeinit()viewItemList.storage (cross-tab sync)comparisonListUpdated (same-tab updates)resize (uniform heights)lastHandledTimestamp to avoid processing the same update multiple times.showReloadWarning = true when it detects changes from another tab.window.productcomparisondefaultInstance = $data in x-init. The resize listener relies on this.syncListFromGlobal(newList)productcomparisondefault.list) and Alpine state (this.list) aligned.initStickyObserver()top based on headerHeight.checkSticky()isSticky based on scroll position and container bounds.top to match current header height.reloadPage()location.reload()).setUniformHeightsPerAttribute().attribute-value elements by data-attribute.swiperInit()clearList()productcomparisondefault.setList([]).handleClearModal(modalTranslations)Alpine.store("modal").open(...).getList()comparisonData and parses JSON.setList(list)comparisonData).comparisonSync in localStorage with a timestamp (cross-tab).comparisonListUpdated (same-tab).modalReusables\\ProductGridItem\\DefaultprepareListProducts(...)sendGAEvent(...)getCookie(...), setCookie(...)GlobalData.Settings.ShowProductComparison.products is empty, an empty state is rendered.product.attributes.removedIds to hide removed product slides.settings.idproducts[] (the page can render an empty-state when missing/empty)settings.cssClasstranslations.* (all user-facing strings)Components/ProductComparison/Default.js exposes a factory:<section
x-data='productcomparisondefault.initComponent("{{ id }}", {{ productsCount }}, {{ products | serialize | escape }}, "{{ model.Translations.Compare }}")'
>viewItemList for the comparison list.storage events and comparisonListUpdated.Alpine.store("modal").open(...) confirmation.Reusables\\ProductGridItem\\DefaultprepareListProducts(...)sendGAEvent(...)GlobalData.Settings.ShowProductComparison.products is empty, an empty state is rendered (with navigation back to /).product.attributes across all products.removedIds array to hide removed product slides.