Integration Guideitwpf.com.au · Product Comparator

ITWPF Product Comparator — Integration Guide

Audience: Wix site developers integrating the comparator iframe into an ITWPF store page.


Overview

The ITWPF Product Comparator is a standalone React web app hosted on Base44. It lets users:

  • Search the Wix product CMS collection by name, brand, category, or MPN
  • Compare up to 4 products side-by-side in a feature table
  • Receive products from the parent Wix page via postMessage
  • Notify the parent page whenever a comparison slot changes

The app communicates exclusively through the browser's window.postMessage API — no cookies, no shared state.


Quick Start

html
<!-- 1. Embed the iframe on your Wix page (Custom Element or HTML Component) -->
<iframe
  id="product-comparator"
  src="https://<your-base44-app-url>/"
  style="width:100%; height:100vh; border:none;"
  allow="same-origin"
></iframe>

<!-- 2. Listen for COMPARE_READY, then send your initial products -->
<script>
window.addEventListener('message', function(event) {
  if (event.data && event.data.type === 'COMPARE_READY') {
    document.getElementById('product-comparator').contentWindow.postMessage({
      type: 'COMPARE_PRODUCTS',
      productIds: ['wix-product-id-1', 'wix-product-id-2']
    }, '*');
  }
});
</script>

Optional: Use URL parameters for shareable links:

https://<your-base44-app-url>/?products=product-id-1,product-id-2

Embedding the iframe

Wix Editor — HTML Component

  1. In the Wix Editor, add an HTML Component (embed) to your page.
  2. Set the source URL to your Base44 app URL (e.g. https://compare.itwpf.com.au/).
  3. Set the component height to at least 800px, or use full viewport height.
  4. Switch to the Code panel and add the Velo snippet from the Full Velo Example section.

Wix Editor — Custom Element

If you need more control, register the app URL as a custom element:

javascript
// In Wix Velo, $w('#htmlComponent1').src is read-only on some elements.
// Use postMessage directly via the onMessage / postMessage API instead.
$w('#htmlComp').postMessage({ type: 'COMPARE_PRODUCTS', productIds: [...] });

URL Query Parameters (Sharing)

The comparator supports product IDs in the URL for easy sharing and deep-linking:

https://<your-base44-app-url>/?products=id-1,id-2,id-3
  • products: Comma-separated Wix product _id values (max 4)
  • Loads automatically on page mount
  • Updates dynamically as products are added/removed
  • Works alongside postMessage (URL is complementary, not a replacement)

Example shareable link:

https://compare.itwpf.com.au/?products=abc123,def456,ghi789

postMessage API Reference

All messages are plain JSON objects with a type string field.

Outbound — iframe → parent

These are messages the comparator sends to your page. Listen with window.addEventListener('message', handler).

TypePayloadDescription
COMPARE_READY(none)Fired on mount and retried every 300ms until the parent responds (or 10 attempts). The iframe is ready to receive products.
COMPARE_LOADING(none)Fired when a batch COMPARE_PRODUCTS load begins.
COMPARE_LOADED(none)Fired when the batch load completes successfully.
COMPARE_ERROR{ message: string }Fired on any load failure. message contains a human-readable error.
COMPARE_PRODUCT_ADDED{ productId: string, _id: string }Fired when a product is added to a slot (via search UI or ADD_PRODUCT). _id is the raw Wix CMS _id — use this for local storage sync.
COMPARE_PRODUCT_REMOVED{ productId: string, _id: string }Fired when a user removes a product from the comparison table. _id matches what was sent in ADD_PRODUCT._id.
COMPARE_BUNDLE_ADDED{ bundleId: string, _id: string }Fired when a bundle is added to a slot (via search UI or ADD_BUNDLE). _id is the bundle identifier (the bundleName / Bundle record _id you sent). Use this for local storage sync of bundles.
COMPARE_BUNDLE_REMOVED{ bundleId: string, _id: string }Fired when a user removes a bundle from the comparison table. _id matches what was sent in ADD_BUNDLE._id.
COMPARE_ADD_CLICKED{ productId: string, _id: string }Fired only on a manual "Add to Compare" click inside the iframe. Use this to scroll the parent page back to the iframe. _id is the product _id (or bundle identifier if the clicked item is a bundle).
COMPARE_TDS_CLICKED{ productId, _id, mpn, productName, bundleTitle, tdsUrl, proxyUrl, action }Fired when a user clicks View or Download on a product's TDS (Technical Data Sheet) link. tdsUrl is the direct doc1 URL from the Wix File collection; proxyUrl is the full marsbound-storage.vercel.app/api/download-TDS URL; action is "view" or "download". Use this to fire a GA tracking event on the parent page.
COMPARE_SDS_CLICKED{ productId, _id, mpn, productName, bundleTitle, sdsUrl, sdsLabel, action }Fired when a user clicks View or Download on a product's SDS (Safety Data Sheet) link. sdsUrl is the direct SDS document URL; sdsLabel is the part label (e.g. "Part 1", "Part 2"); action is "view" or "download". Use this to fire a GA tracking event on the parent page.

_id field: Every add/remove event includes _id — the exact identifier the parent should use for local storage management. For products, this is the Wix CMS _id. For bundles, this is the bundleName (or Bundle record _id) that was passed in ADD_BUNDLE._id or COMPARE_PRODUCTS.bundleIds. Always use _id (not productId) when syncing storage, since productId may be a normalized internal id that differs from the raw Wix _id.

Example listener

javascript
window.addEventListener('message', function(event) {
  const msg = event.data;
  if (!msg || !msg.type) return;

  switch (msg.type) {
    case 'COMPARE_READY':
      sendInitialProducts();
      break;
    case 'COMPARE_PRODUCT_ADDED':
      console.log('Product added:', msg._id);  // use _id for storage
      break;
    case 'COMPARE_PRODUCT_REMOVED':
      console.log('Product removed:', msg._id);
      break;
    case 'COMPARE_BUNDLE_ADDED':
      console.log('Bundle added:', msg._id);  // _id = bundleName
      break;
    case 'COMPARE_BUNDLE_REMOVED':
      console.log('Bundle removed:', msg._id);
      break;
    case 'COMPARE_ADD_CLICKED':
      window.scrollTo({ top: 0, behavior: 'smooth' });  // scroll parent to iframe
      break;
    case 'COMPARE_TDS_CLICKED':
      // Fire GA tracking event for TDS view/download
      wixWindowFrontend.trackEvent('CustomEvent', {
        eventCategory: 'Downloads',
        eventAction: 'TDS ' + msg.action,           // "TDS view" or "TDS download"
        eventLabel: 'TDS ' + msg.action + ' - ' + msg.bundleTitle,
        eventValue: 1,
      });
      break;
    case 'COMPARE_SDS_CLICKED':
      // Fire GA tracking event for SDS view/download
      wixWindowFrontend.trackEvent('CustomEvent', {
        eventCategory: 'Downloads',
        eventAction: 'SDS ' + msg.action,           // "SDS view" or "SDS download"
        eventLabel: 'SDS ' + msg.action + ' - ' + msg.sdsLabel + ' - ' + msg.bundleTitle,
        eventValue: 1,
      });
      break;
    case 'COMPARE_ERROR':
      console.error('Comparator error:', msg.message);
      break;
  }
});

Inbound — parent → iframe

These are messages your page sends to the comparator. Use iframeElement.contentWindow.postMessage(msg, '*').


COMPARE_PRODUCTS

Pre-loads up to 4 products and/or bundles by their Wix CMS _id values. Replaces all current slots.

json
{
  "type": "COMPARE_PRODUCTS",
  "productIds": ["id-1", "id-2", "id-3"],
  "bundleIds": ["bundle-name-1"]
}
FieldTypeRequiredNotes
productIdsstring[]NoArray of Wix product _id strings. Defaults to an empty array.
  • productIds fill slots first (in order), then bundleIds fill the remaining slots (in order), up to 4 total.
  • A message with only productIds (no bundleIds key, or bundleIds: []) behaves exactly as before.
  • Products with isObsolete === "true" are automatically excluded.
  • If a product ID is not found in the CMS, its slot shows an error state with a Retry button.

ADD_PRODUCT

Adds a single product to the next available slot. If all 4 slots are occupied, the oldest slot (index 0) is evicted and the new product is placed at the end.

json
{
  "type": "ADD_PRODUCT",
  "_id": "wix-product-id-here"
}
FieldTypeRequiredNotes
_idstringYesWix CMS _id of the product to add.
  • If the product is already in a slot, the message is silently ignored (no duplicate).
  • A COMPARE_PRODUCT_ADDED message (with _id) is sent back after the slot is filled.

ADD_BUNDLE

Adds a bundle to the next available slot using its bundle ID (the value of the bundle field on product records, which matches the bundleName in the Bundle CMS collection). The comparator fetches the first representative product in the bundle, marks it as a bundle slot, and applies all bundle-specific rendering (e.g. "Various" for MPN/Barcode, "Multiple sizes" for Volume, and a "View all sizes" modal).

json
{
  "type": "ADD_BUNDLE",
  "_id": "bundle-name-here"
}
FieldTypeRequiredNotes
_idstringYesThe bundleName value from the Wix Bundle CMS collection (also the bundle field on product records).
  • If a product from the same bundle is already in a slot, the message is silently ignored (no duplicate).
  • If all 4 slots are occupied, the oldest slot (index 0) is evicted before adding the bundle.
  • A COMPARE_BUNDLE_ADDED message (with _id = the bundle identifier you sent) is sent back after the slot is filled — not COMPARE_PRODUCT_ADDED.

Integration Patterns

Pre-loading products when a comparison page opens

javascript
// Wix Velo — Page code
import wixStorage from 'wix-storage';

$w.onReady(function() {
  const iframe = $w('#htmlCompare');
  iframe.onMessage(function(event) {
    if (event.data.type === 'COMPARE_READY') {
      const ids = JSON.parse(wixStorage.session.getItem('compareIds') || '[]');
      if (ids.length > 0) {
        iframe.postMessage({ type: 'COMPARE_PRODUCTS', productIds: ids });
      }
    }
  });
});

Adding a product from a Product Page

javascript
// Wix Velo — Product page code
import wixStorage from 'wix-storage';
import wixLocation from 'wix-location';

$w.onReady(function() {
  $w('#btnAddToCompare').onClick(function() {
    const productId = $w('#dynamicDataset').getCurrentItem()._id;
    let ids = JSON.parse(wixStorage.session.getItem('compareIds') || '[]');
    if (!ids.includes(productId)) {
      if (ids.length >= 4) ids.shift();
      ids.push(productId);
    }
    wixStorage.session.setItem('compareIds', JSON.stringify(ids));
    wixLocation.to('/compare');
  });
});

If your compare page is open in a lightbox or panel alongside the product page, post directly:

javascript
const iframe = $w('#htmlCompare');
iframe.postMessage({ type: 'ADD_PRODUCT', _id: productId });

Reacting to slot changes

Products and bundles emit separate events. Use _id (not productId) for local storage sync — it's the exact identifier you sent and will receive back.

javascript
$w('#htmlCompare').onMessage(function(event) {
  const msg = event.data;
  if (!msg || !msg.type) return;

  // ── Product events ──
  if (msg.type === 'COMPARE_PRODUCT_ADDED') {
    let ids = JSON.parse(wixStorage.session.getItem('compareIds') || '[]');
    if (!ids.includes(msg._id)) ids.push(msg._id);
    wixStorage.session.setItem('compareIds', JSON.stringify(ids));
    updateCompareCount();
  }
  if (msg.type === 'COMPARE_PRODUCT_REMOVED') {
    let ids = JSON.parse(wixStorage.session.getItem('compareIds') || '[]');
    ids = ids.filter((id) => id !== msg._id);
    wixStorage.session.setItem('compareIds', JSON.stringify(ids));
    updateCompareCount();
  }

  // ── Bundle events ──
  if (msg.type === 'COMPARE_BUNDLE_ADDED') {
    let bundleIds = JSON.parse(wixStorage.session.getItem('compareBundleIds') || '[]');
    if (!bundleIds.includes(msg._id)) bundleIds.push(msg._id);
    wixStorage.session.setItem('compareBundleIds', JSON.stringify(bundleIds));
    updateCompareCount();
  }
  if (msg.type === 'COMPARE_BUNDLE_REMOVED') {
    let bundleIds = JSON.parse(wixStorage.session.getItem('compareBundleIds') || '[]');
    bundleIds = bundleIds.filter((id) => id !== msg._id);
    wixStorage.session.setItem('compareBundleIds', JSON.stringify(bundleIds));
    updateCompareCount();
  }

  // ── Scroll parent to iframe on manual click ──
  if (msg.type === 'COMPARE_ADD_CLICKED') {
    $w('#htmlCompare').scrollTo();
  }
});

function updateCompareCount() {
  const ids = JSON.parse(wixStorage.session.getItem('compareIds') || '[]');
  const bundleIds = JSON.parse(wixStorage.session.getItem('compareBundleIds') || '[]');
  $w('#txtCompareCount').text = String(ids.length + bundleIds.length);
}

Full Velo Example

javascript
// Page: compare.js (Wix Velo Page Code)
import wixStorage from 'wix-storage';

$w.onReady(function () {
  const iframe = $w('#htmlCompare');

  iframe.onMessage(function (event) {
    const msg = event.data;
    if (!msg || !msg.type) return;

    // ── Iframe ready — send stored products AND bundles ──
    if (msg.type === 'COMPARE_READY') {
      const ids = JSON.parse(wixStorage.session.getItem('compareIds') || '[]');
      const bundleIds = JSON.parse(wixStorage.session.getItem('compareBundleIds') || '[]');
      // Always respond (even if empty) to ack the iframe and stop its retry loop
      iframe.postMessage({ type: 'COMPARE_PRODUCTS', productIds: ids, bundleIds: bundleIds });
    }

    // ── Product removed inside iframe — sync storage ──
    if (msg.type === 'COMPARE_PRODUCT_REMOVED') {
      let ids = JSON.parse(wixStorage.session.getItem('compareIds') || '[]');
      ids = ids.filter((id) => id !== msg._id);
      wixStorage.session.setItem('compareIds', JSON.stringify(ids));
    }

    // ── Bundle removed inside iframe — sync storage ──
    if (msg.type === 'COMPARE_BUNDLE_REMOVED') {
      let bundleIds = JSON.parse(wixStorage.session.getItem('compareBundleIds') || '[]');
      bundleIds = bundleIds.filter((id) => id !== msg._id);
      wixStorage.session.setItem('compareBundleIds', JSON.stringify(bundleIds));
    }

    // ── User clicked "Add" inside iframe — scroll parent up ──
    if (msg.type === 'COMPARE_ADD_CLICKED') {
      $w('#htmlCompare').scrollTo();
    }

    if (msg.type === 'COMPARE_ERROR') {
      console.warn('[Comparator Error]', msg.message);
    }
  });
});

Data Model

The comparator reads from the Wix CMS collection named product. The following fields are displayed:

UI LabelCMS FieldNotes
Product NameproductNameLinked to product page URL
BrandbrandTitle
MPNmpn
Pack Sizepack_sizeTitle
Volumecustom_attributesShortText5dt78mkfy0
Categorycategories0TitleAlso used for filter dropdown
Sub-categorycategories1Title
Barcodebarcode
Applicationscustom_attributesShortTextNx913599Pt
SDS Link (Part 1)sdsFormattedOpens as external link
SDS Link (Part 2)sdsFormatted1Optional; shown if present
SDS Link (Part 3)sdsFormatted2Optional; shown if present
SDS Link (Part 4)sdsFormatted3Optional; shown if present
SDS Part LabelssdsDisplayLabelPart1–Part4Custom labels for each SDS part (default: "Part 1", "Part 2", …)
Key Featurescustom_attributesShortText* (×5)Feature tag pills
Compositioncustom_attributesLongTextI567Rxud9ACollapsible
Imagepreferred_images0UrlFalls back to placeholder
Product URLlink-copy-of-products-titlePrepended with https://itwpf.com.au

Obsolete Products

Any product where isObsolete === "true" is permanently excluded from all search results and ID lookups.


FAQ & Troubleshooting

Q: The iframe loads but no products appear after sending COMPARE_PRODUCTS.

Check that you are sending the message after receiving COMPARE_READY. Always wait for it.


Q: ADD_PRODUCT silently does nothing.

Verify the _id is the raw Wix CMS _id (a UUID string), not a slug or product number.


Q: Products with known IDs show an error slot.

The product may be marked isObsolete = true in the CMS, or the _id may not exist in the product collection.


Q: Can I embed multiple comparators on the same page?

Not recommended — each iframe independently manages 4 slots.


Q: The iframe is too tall / too short on mobile.

Set the iframe height to 100vh or a percentage. Do not clip with overflow: hidden on the parent container.


Last updated: September 2026 · ITWPF Product Comparator v1.1

ITWPF Product Comparator Integration Guide · Internal Use Only