"Go to Wishlist Button" Snippet

Read this article to learn more about the snippet usage.


Usage:

{% render 'wl-go-to-btn' %}

Code:

{% comment %}
  Snippet: wl-go-to-btn
  Usage:
    {% render 'wl-go-to-btn' %}
    {% render 'wl-go-to-btn',
      icon_color: '#e53e3e',
      text_color: '#333333',
      icon_size: 24,
      icon_size_mobile: 20,
      text_size: 16,
      text_size_mobile: 14,
      show_text: false,
      show_counter: true,
      count_position: 'on_icon',
      hide_counter_when_empty: true,
      count_bg_color: '#e53e3e',
      count_text_color: '#ffffff',
      count_size: 18,
      count_size_mobile: 16,
      count_font_size: 10,
      count_font_size_mobile: 10,
      icon_text_gap: 6,
      margin_top: 0,
      margin_right: 0,
      margin_bottom: 0,
      margin_left: 0,
      max_width: 0
    %}

  Parameters (all optional, defaults shown above):
    icon_color            — icon stroke/fill color
    text_color            — link text color
    icon_size             — icon size in px (desktop)
    icon_size_mobile      — icon size in px (mobile)
    text_size             — font size in px (desktop)
    text_size_mobile      — font size in px (mobile)
    show_text             — show link text (true/false)
    show_counter          — show wishlist count badge (true/false)
    count_position        — 'on_icon' | 'left' | 'right'
    hide_counter_when_empty — hide badge when 0 items (true/false)
    count_bg_color        — badge background color
    count_text_color      — badge text color
    count_size            — badge circle size in px (desktop)
    count_size_mobile     — badge circle size in px (mobile)
    count_font_size       — badge font size in px (desktop)
    count_font_size_mobile— badge font size in px (mobile)
    icon_text_gap         — gap between icon and text in px
    margin_top/right/bottom/left — outer margins in px
    max_width             — max-width of the link in px (0 = none)
{% endcomment %}

{% liquid
  assign _icon_color = icon_color | default: '#dcdcdcff'
  assign _text_color = text_color | default: '#dcdcdcff'
  assign _icon_size = icon_size | default: 20
  assign _icon_size_mobile = icon_size_mobile | default: 20
  assign _text_size = text_size | default: 14
  assign _text_size_mobile = text_size_mobile | default: 14
  assign _show_text = show_text | default: false
  assign _show_counter = show_counter | default: true
  assign _count_position = count_position | default: 'on_icon'
  assign _hide_counter_when_empty = hide_counter_when_empty | default: true
  assign _count_bg_color = count_bg_color | default: '#dcdcdcff'
  assign _count_text_color = count_text_color | default: '#000000ff'
  assign _count_size = count_size | default: 18
  assign _count_size_mobile = count_size_mobile | default: 18
  assign _count_font_size = count_font_size | default: 10
  assign _count_font_size_mobile = count_font_size_mobile | default: 10
  assign _icon_text_gap = icon_text_gap | default: 6
  assign _margin_top = margin_top | default: 0
  assign _margin_right = margin_right | default: 0
  assign _margin_bottom = margin_bottom | default: 0
  assign _margin_left = margin_left | default: 0
  assign _max_width = max_width | default: 0

  assign _snippet_id = 'wl-go-' | append: section.id | append: '-' | append: block.id

  assign is_rtl = false
  if request.locale.iso_code == 'ar' or request.locale.iso_code == 'he' or request.locale.iso_code == 'fa' or request.locale.iso_code == 'ur'
    assign is_rtl = true
  endif
  assign dir_value = 'ltr'
  if is_rtl
    assign dir_value = 'rtl'
  endif
%}

{% comment %} ── CSS (injected once per page via snippet-id guard) ────────── {% endcomment %}
<style id='wl-go-btn-styles' data-wl-go-styles>
  .hidden {
    display: none !important;
  }

  .wl-add a,
  .wl-page a,
  a.wl-go {
    text-decoration: none;
  }

  button::before,
  button::after {
    content: none !important;
  }

  .wl-go {
    display: inline-flex;
    flex-wrap: nowrap;
    justify-content: var(--wl-go-align, center);
    align-items: center;
    gap: var(--wl-go-gap, 8px);
    transition: opacity 0.2s;
    margin: var(--wl-go-margin-top, 0) var(--wl-go-margin-right, 0)
      var(--wl-go-margin-bottom, 0) var(--wl-go-margin-left, 0);
    width: 100%;
    max-width: var(--wl-go-max-width, none);
    color: var(--wl-go-text-color, #000);
    font-size: var(--wl-go-text-size, 14px);
  }
  .wl-go:hover {
    opacity: 0.8;
  }
  .wl-go:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 2px;
  }

  .wl-go__icon {
    display: flex;
    flex-shrink: 0;
    justify-content: center;
    align-items: center;
    color: var(--wl-go-icon-color, #000);
    width: var(--wl-go-icon-size, 20px);
    height: var(--wl-go-icon-size, 20px);
  }
  .wl-go__icon svg {
    width: 100%;
    height: 100%;
  }

  .wl-go__icon--counter-left,
  .wl-go__icon--counter-right {
    gap: 0.25em;
  }

  .wl-go__icon--counter-left .wl-go__counter {
    order: -1;
  }
  .wl-go__icon--counter-on_icon {
    position: relative;
  }

  .wl-go__text {
    color: var(--wl-go-text-color, #000);
    font-size: var(--wl-go-text-size, 14px);
  }

  .wl-go__counter {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    border-radius: 999px;
    background-color: var(--wl-go-count-bg-color, #000);
    width: fit-content;
    min-width: var(--wl-go-count-size, 18px);
    height: var(--wl-go-count-size, 18px);
    color: var(--wl-go-count-color, #fff);
    font-weight: 600;
    font-size: var(--wl-go-count-font-size, 10px);
  }
  .wl-go__icon--counter-on_icon .wl-go__counter {
    position: absolute;
    top: -4px;
    inset-inline-end: -4px;
  }
  .wl-go__counter--empty {
    display: none;
  }

  :dir(rtl) .wl-go__icon--counter-left .wl-go__counter {
    order: 1;
  }
  :dir(rtl) .wl-go__icon--counter-right .wl-go__counter {
    order: -1;
  }
</style>

{% comment %} ── Per-instance CSS custom properties ───────────────────────── {% endcomment %}
<style>
  #{{ _snippet_id }} .wl-go {
    --wl-go-icon-color: {{ _icon_color }};
    --wl-go-text-color: {{ _text_color }};
    --wl-go-gap: {{ _icon_text_gap }}px;
    --wl-go-margin-top: {{ _margin_top }}px;
    --wl-go-margin-right: {{ _margin_right }}px;
    --wl-go-margin-bottom: {{ _margin_bottom }}px;
    --wl-go-margin-left: {{ _margin_left }}px;
    --wl-go-icon-size: {{ _icon_size_mobile }}px;
    --wl-go-text-size: {{ _text_size_mobile }}px;
    --wl-go-count-bg-color: {{ _count_bg_color }};
    --wl-go-count-color: {{ _count_text_color }};
    --wl-go-count-size: {{ _count_size_mobile }}px;
    --wl-go-count-font-size: {{ _count_font_size_mobile }}px;
    {% if _max_width > 0 %}--wl-go-max-width: {{ _max_width }}px;{% endif %}
  }
  @media screen and (min-width: 768px) {
    #{{ _snippet_id }} .wl-go {
      --wl-go-icon-size: {{ _icon_size }}px;
      --wl-go-text-size: {{ _text_size }}px;
      --wl-go-count-size: {{ _count_size }}px;
      --wl-go-count-font-size: {{ _count_font_size }}px;
    }
  }
</style>

{% comment %} ── Markup ────────────────────────────────────────────────────── {% endcomment %}
<span id='{{ _snippet_id }}'>
  <a
    href='#'
    class='wl-go'
    data-wishlist-go-link
    dir='{{ dir_value }}'
    {% if _show_counter %}
      data-wishlist-counter-hide-when-empty='{{ _hide_counter_when_empty }}'
    {% endif %}
  >
    <span class='wl-go__icon{% if _show_counter %} wl-go__icon--counter-{{ _count_position }}{% endif %}'>
      <svg
        xmlns='http://www.w3.org/2000/svg'
        fill='none'
        viewBox='0 0 24 24'
        aria-hidden='true'
        focusable='false'
      >
        <path
          stroke="currentColor"
          stroke-linecap="round"
          stroke-linejoin="round"
          stroke-width="1.5"
          d="M7.87891 3.75c.67013.00152 1.33423.14051 1.9541.40918.61989.26872 1.18439.66253 1.66019 1.16016.1399.14633.3336.22972.5361.23144.2024.00163.3967-.07878.5391-.22266.9658-.9764 2.2584-1.51357 3.5957-1.50488 1.3372.00873 2.6231.5627 3.5771 1.55176.955.98998 1.5 2.33576 1.5088 3.74707.0087 1.41133-.52 2.76473-1.4629 3.76753l-6.6318 6.877c-.3018.3128-.7055.4834-1.1201.4834-.4146-.0001-.8175-.1707-1.1192-.4834l-6.63963-6.8828-.00196-.0029-.17578-.1885c-.3984-.452-.71987-.9742-.94922-1.543-.26198-.6498-.39789-1.34784-.39941-2.0537-.0015-.70608.13124-1.40537.39062-2.05664.25939-.6512.64026-1.24084 1.11817-1.73633.47787-.49543 1.04403-.88648 1.66504-1.15234.62086-.26578 1.28506-.40188 1.95508-.40039"
        />
      </svg>
      {% if _show_counter %}
        <span
          class='wl-go__counter wl-go__counter--empty'
          data-wishlist-count
          aria-hidden='true'
        ></span>
      {% endif %}
    </span>
    {% if _show_text %}
      <span class='wl-go__text' data-wishlist-go-text></span>
    {% endif %}
  </a>
</span>

{% comment %} ── JavaScript (injected once per page via id guard) ──────────── {% endcomment %}
<script>
(function () {
  'use strict';

  // Prevent double-init across multiple renders of this snippet
  if (window.__wlGoSnippetInit) return;
  window.__wlGoSnippetInit = true;

  const SELECTORS = {
    counter: '[data-wishlist-count]',
    text: '[data-wishlist-go-text]',
    link: '[data-wishlist-go-link]',
  };

  const DEBOUNCE_MS = 80;

  const namespace = (window.__refactorWishlistApp =
    window.__refactorWishlistApp || {});

  // ── Helpers ──────────────────────────────────────────────────────────────
  const logger = { warn: (...a) => console.warn('[Wishlist]', ...a) };

  // Use the same namespace key and same method names as add-to-wishlist-btn
  // so whichever script runs first, the cached object is compatible with both.
  const getWishlistRuntimeHelpers = () => {
    if (!namespace.runtimeHelpers) {
      namespace.runtimeHelpers = {
        getErrorMessage(error) {
          return error instanceof Error ? error.message : String(error ?? 'Unknown error');
        },
        isFunction(value) {
          return typeof value === 'function';
        },
        createDebouncedFunction(callback, delayMs) {
          let timerId = null;
          return (...args) => {
            clearTimeout(timerId);
            timerId = setTimeout(() => callback(...args), delayMs);
          };
        },
        resolveWishlistInstance() {
          const helper = window.refactor_apps?.wishlist ?? null;
          if (helper) return { helper, error: null };
          return { helper: null, error: new Error('WishlistHelper is not available') };
        },
        ensureHelperContractOrThrow(helper, methods) {
          for (const methodName of methods) {
            if (typeof helper?.[methodName] === 'function') continue;
            throw new Error(`WishlistHelper method is missing: ${methodName}`);
          }
        },
      };
    }
    return namespace.runtimeHelpers;
  };

  const {
    getErrorMessage,
    isFunction,
    createDebouncedFunction,
    resolveWishlistInstance,
    ensureHelperContractOrThrow,
  } = getWishlistRuntimeHelpers();

  // ── Wishlist adapter ──────────────────────────────────────────────────────
  const createAdapter = () => {
    let readyPromise = null;

    const getHelper = async () => {
      if (readyPromise) return readyPromise;
      readyPromise = (async () => {
        const { helper, error } = resolveWishlistInstance();
        if (error) throw error;
        if (isFunction(helper.ready)) await helper.ready();
        return helper;
      })().catch((e) => { readyPromise = null; throw e; });
      return readyPromise;
    };

    return {
      ready: async () => { await getHelper(); },
      getItems: async () => {
        const h = await getHelper();
        ensureHelperContractOrThrow(h, ['getUniqueWishlistItems']);
        return h.getUniqueWishlistItems({ forceSync: true }) ?? [];
      },
    };
  };

  // ── State ─────────────────────────────────────────────────────────────────
  let linkEntries = [];
  let inFlight = null;
  const adapter = createAdapter();
  const cleanups = [];

  // ── Resolve href + text from wishlist app ─────────────────────────────────
  const resolveLinkMeta = () => {
    try {
      const wl = window.refactor_apps?.wishlist;
      return {
        href: wl?.linkUrl || '#',
        text: wl?.linkText || '',
      };
    } catch (_) {
      return { href: '#', text: '' };
    }
  };

  const applyLinkMeta = () => {
    const { href, text } = resolveLinkMeta();
    for (const { link } of linkEntries) {
      if (href && href !== '#') link.setAttribute('href', href);
      if (text) link.setAttribute('aria-label', text);
      const textEl = link.querySelector(SELECTORS.text);
      if (textEl && text) textEl.textContent = text;
    }
  };

  // ── DOM ───────────────────────────────────────────────────────────────────
  const collect = () => {
    linkEntries = [...document.querySelectorAll(SELECTORS.link)].map((link) => ({
      link,
      counter: link.querySelector(SELECTORS.counter),
    }));
  };

  const updateCounter = (el, count, hideWhenEmpty) => {
    const hide = count === 0 && hideWhenEmpty;
    el.textContent = hide ? '' : String(count);
    el.classList.toggle('wl-go__counter--empty', hide);
  };

  const updateAll = (count) => {
    applyLinkMeta();
    for (const { link, counter } of linkEntries) {
      link.classList.toggle('wl-go--active', count > 0);
      if (!counter) continue;
      const hideWhenEmpty =
        link.getAttribute('data-wishlist-counter-hide-when-empty') === 'true';
      updateCounter(counter, count, hideWhenEmpty);
    }
  };

  // ── Update cycle ──────────────────────────────────────────────────────────
  const runUpdate = async () => {
    if (inFlight) return inFlight;
    const { helper } = resolveWishlistInstance();
    if (!helper) { updateAll(0); return; }

    inFlight = (async () => {
      try {
        await adapter.ready();
        const items = await adapter.getItems();
        updateAll(items.length);
      } catch (e) {
        logger.warn('wl-go-to-btn: failed to update counter', getErrorMessage(e));
        updateAll(0);
      } finally {
        inFlight = null;
      }
    })();

    return inFlight;
  };

  const reloadUpdate = async () => {
    collect();
    await runUpdate();
  };

  // ── Teardown ──────────────────────────────────────────────────────────────
  const teardown = () => {
    cleanups.forEach((fn) => fn());
    cleanups.length = 0;
    linkEntries = [];
    inFlight = null;
    window.__wlGoSnippetInit = false;
  };

  const on = (target, event, handler) => {
    target.addEventListener(event, handler);
    cleanups.push(() => target.removeEventListener(event, handler));
  };

  // ── Init ──────────────────────────────────────────────────────────────────
  const init = () => {
    collect();
    void runUpdate();

    const debouncedUpdate = createDebouncedFunction(runUpdate, DEBOUNCE_MS);
    on(window, 'wishlist-updated', debouncedUpdate);
    on(window, 'wishlist:updated', debouncedUpdate);
    on(window, 'wishlist-initialized', debouncedUpdate);
    on(document, 'shopify:section:load', reloadUpdate);
    on(document, 'turbo:before-cache', teardown);
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();
</script>

Do you need help?

If you have any questions or run into issues, please contact us — we’re happy to help.

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.

Still need help? Contact Us Contact Us