Swym Wishlist for Retail Cloud Connect™ Product Cards
This guide adds a Swym Wishlist Plus heart to Retail Cloud Connect product cards with a Custom component. It is intended for theme developers and supports Swym's default single-list and guest-wishlist behavior.
The built-in Wishlist component remains a generic integration hook for wishlist apps that initialize from HTML attributes. Swym needs its own Web Component because product cards can be replaced after filtering or pagination and every visible heart must share the same saved state.
[!warning]
This example is not a drop-in implementation for stores that require sign-in or use multiple Swym lists. Review
Prerequisites
- Swym Wishlist Plus is installed and its App Embed is enabled in the target theme
- Swym works on an ordinary product page before the product-card integration is added
- The store's guest, required-sign-in, and single-list or multi-list policy is known
- A theme developer can add JavaScript and CSS assets to the target theme
Enable the target store's own Swym App Embed. Do not copy App Embed IDs, app UUIDs, or config/settings_data.json entries from another store.
Add the Web Component
Create assets/rcc-product-wishlist.js in the Shopify theme and add the following component:
(function () {
'use strict';
if (customElements.get('rcc-product-wishlist')) return;
const wishlist = {
swat: null,
ready: false,
variants: new Set(),
components: new Set(),
};
function normalizeShopifyId(value) {
const id = String(value ?? '')
.split('@')[0]
.split('/')
.pop();
return /^\d+$/.test(id || '') ? id : null;
}
function createItem(product) {
const productId = normalizeShopifyId(product?.productId);
const variantId = normalizeShopifyId(product?.variantId);
if (!productId || !variantId || !product?.url) return null;
try {
const url = new URL(product.url, window.location.origin);
url.search = '';
return {
empi: Number(productId),
epi: Number(variantId),
du: url.href,
};
} catch {
return null;
}
}
function syncWishlist() {
wishlist.components.forEach((component) => component.updateState());
}
function setSaved(variantId, saved) {
const id = normalizeShopifyId(variantId);
if (!id) return;
if (saved) wishlist.variants.add(id);
else wishlist.variants.delete(id);
syncWishlist();
}
function updateFromEvent(event, saved) {
setSaved(event.detail?.d?.epi, saved);
}
function initializeWishlist(swat) {
if (!swat || wishlist.swat) return;
wishlist.swat = swat;
const finishFetch = (items) => {
wishlist.variants = new Set(
(items || [])
.map((item) => normalizeShopifyId(item?.epi))
.filter(Boolean),
);
wishlist.ready = true;
syncWishlist();
};
if (typeof swat.fetch === 'function') {
try {
swat.fetch(finishFetch, () => finishFetch([]));
} catch {
finishFetch([]);
}
} else {
finishFetch([]);
}
const addedEvent = swat.JSEvents?.addedToWishlist || 'sw:addedtowishlist';
const removedEvent =
swat.JSEvents?.removedFromWishlist || 'sw:removedfromwishlist';
swat.evtLayer?.addEventListener(addedEvent, (event) =>
updateFromEvent(event, true),
);
swat.evtLayer?.addEventListener(removedEvent, (event) =>
updateFromEvent(event, false),
);
}
class RccProductWishlist extends HTMLElement {
constructor() {
super();
this._busy = false;
this._item = null;
this._product = null;
}
set product(product) {
this._product = product;
this.render();
}
get product() {
return this._product;
}
connectedCallback() {
wishlist.components.add(this);
this.render();
}
disconnectedCallback() {
wishlist.components.delete(this);
}
render() {
this._item = createItem(this._product);
this.replaceChildren();
if (!this._item) return;
const button = document.createElement('button');
button.type = 'button';
button.className = 'rcc-product-wishlist__button';
button.dataset.rccWishlistVariantId = String(this._item.epi);
button.innerHTML = `
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M12 21s-7.5-4.35-9.6-8.55C.57 8.8 2.75 4.5 6.9 4.5c2.1 0 3.45 1.2 4.2 2.25.75-1.05 2.1-2.25 4.2-2.25 4.15 0 6.33 4.3 4.5 7.95C17.7 16.65 12 21 12 21Z" />
</svg>
`;
button.addEventListener('click', (event) => this.toggle(event));
this.append(button);
this.updateState();
}
updateState() {
const button = this.querySelector('button');
if (!button || !this._item) return;
const saved = wishlist.variants.has(String(this._item.epi));
button.classList.toggle('swym-added', saved);
button.disabled = this._busy || !wishlist.ready;
button.setAttribute('aria-pressed', String(saved));
button.setAttribute(
'aria-label',
saved ? 'Remove from wishlist' : 'Add to wishlist',
);
}
toggle(event) {
event.preventDefault();
event.stopPropagation();
if (this._busy || !this._item || !wishlist.ready) return;
const item = this._item;
const saved = wishlist.variants.has(String(item.epi));
const method = saved ? 'removeFromWishList' : 'addToWishList';
const action = wishlist.swat?.[method];
if (typeof action !== 'function') return;
this._busy = true;
this.updateState();
const finish = (success) => {
this._busy = false;
if (success) setSaved(item.epi, !saved);
else this.updateState();
};
try {
action.call(
wishlist.swat,
item,
() => finish(true),
() => finish(false),
);
} catch {
finish(false);
}
}
}
customElements.define('rcc-product-wishlist', RccProductWishlist);
window.SwymCallbacks = window.SwymCallbacks || [];
window.SwymCallbacks.push(initializeWishlist);
if (window._swat) initializeWishlist(window._swat);
})();
The component uses only product.productId, product.variantId, and product.url. Retail Cloud Connect assigns those values through the element's product property, so this integration does not need a metafield, Storefront GraphQL requirement, or request from each product card.
The component also:
- Normalizes numeric IDs, Shopify GIDs, and Retail Cloud Connect
@...product suffixes before calling Swym - Removes query parameters from the saved product URL
- Fetches the default wishlist once per page
- Uses one Swym event subscription to synchronize duplicate product cards
- Prevents a heart click from opening the product-card link
- Disables the button while an add or remove request is running
The example uses English accessible labels. Replace Add to wishlist and Remove from wishlist with the theme's locale strings on multilingual storefronts.
Load the Component Before App Blocks
Load the JavaScript synchronously in the document <head> before a Retail Cloud Connect App Block can render. The recommended location is snippets/nimstrata.liquid; see nimstrata.liquid Setup.
{% # theme-check-disable ParserBlockingScript %}
{{- 'rcc-product-wishlist.js' | asset_url | script_tag -}}
{% # theme-check-enable ParserBlockingScript %}
Do not add defer, async, or type="module". The Product Card renderer checks whether the Custom element is registered before rendering it.
The component accepts Swym through its callback queue and also handles a Swym instance that has already loaded. Its registration guard prevents duplicate theme includes or Theme Editor reloads from binding the integration twice.
Add It in the Product Card Builder
In the Retail Cloud Connect Shopify App:
- Open Product Card Builder.
- Add a Custom component to the product-card block that contains the image.
- Set Component to
rcc-product-wishlist. - Leave Product metafields empty.
- Save and publish the layout.
The component name must match exactly. A JavaScript file alone does not add the component to the product-card layout.
Add the Styles
Add the following rules to the theme stylesheet. The first selector assumes the Custom component is inside the standard vertical image block; use the actual nearest product-card wrapper if the store's layout differs.
.rcc-search__product__block--vertical {
position: relative;
}
rcc-product-wishlist {
position: absolute;
top: 0.5rem;
right: 0.5rem;
z-index: 5;
display: block;
}
rcc-product-wishlist:empty {
display: none;
}
.rcc-product-wishlist__button {
display: grid;
width: 2.75rem;
height: 2.75rem;
padding: 0;
place-items: center;
color: var(--color-foreground, #1f1f1f);
background: var(--color-background, #fff);
border: 1px solid currentColor;
border-radius: 50%;
cursor: pointer;
}
.rcc-product-wishlist__button svg {
width: 1.4rem;
fill: transparent;
stroke: currentColor;
stroke-width: 1.75;
}
.rcc-product-wishlist__button.swym-added svg {
fill: currentColor;
}
.rcc-product-wishlist__button:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
.rcc-product-wishlist__button:disabled {
cursor: wait;
opacity: 0.6;
}
Keep the heart clear of badges and other overlays. Adjust top, right, and z-index for the active theme rather than applying global .swym-button overrides.
Sign-In and Multiple Lists
Some themes replace a Swym API to require authentication before a shopper can save an item. Calling the top-level swat.addToWishList() method directly can bypass a theme-specific sign-in override.
For a required-sign-in store, route the add action through the theme's documented public sign-in adapter. The adapter must accept the same { empi, epi, du } item and replay the action after login. Do not mark the heart as saved before login and a successful replay. Keep the data-rcc-wishlist-variant-id hook from the example so replay code can locate the correct product card.
The example uses swat.fetch() and swat.removeFromWishList(), which cover the default wishlist. For multiple lists, confirm the API contract for the installed Swym version, hydrate all listcontents with fetchLists(), retain each list ID, and remove with deleteFromList() for the correct list.
Avoid adding Swym's native action classes or data-swaction to the custom button. A later Swym action-button scan can otherwise attach a second click handler and submit the same item twice.
Validation Checklist
- Run
node --check assets/rcc-product-wishlist.jsand Shopify Theme Check - Confirm
customElements.get('rcc-product-wishlist')is defined before product cards render - Confirm each valid product card contains one wishlist button
- Test numeric, Shopify GID, and
@...ID forms - Verify each add request has the correct product ID, selected variant ID, and absolute product URL
- Add and remove while signed in, then reload and confirm the state persists
- Render the same variant more than once and confirm every heart stays synchronized
- Test filtering, sorting, pagination, and any Recommendations AI App Blocks that use the same product-card layout
- Confirm keyboard activation, visible focus,
aria-label, andaria-pressed - Confirm clicking the heart does not open the product and clicking elsewhere on the card still does
- Test signed-out behavior when the store requires sign-in
- Test products outside the default list when the store uses multiple lists
- Confirm Swym's native collection integration has not injected a second heart
Troubleshooting
Related Guides
- Custom Components for the full Product Card Web Component contract
- Product Card Builder for layout configuration
- Customizing App Blocks for product-card CSS hooks