Skip to content

Instantly share code, notes, and snippets.

@zacariec
Last active May 7, 2025 02:23
Show Gist options
  • Save zacariec/57507c9fcf18f5226be650d915e7e6b4 to your computer and use it in GitHub Desktop.
Save zacariec/57507c9fcf18f5226be650d915e7e6b4 to your computer and use it in GitHub Desktop.
Brand Switcher for Shopify
<style>
#shopify-section-{{ section.id }} ul {
display: flex;
flex-direction: row;
list-style: none;
margin: unset;
padding: unset;
}
#shopify-section-{{ section.id }} brand-button a {
display: flex;
position: relative;
height: fit-content;
width: fit-content;
align-items: center;
padding: 20px 10px;
}
#shopify-section-{{ section.id }} brand-button a img {
transition: all 0.3s ease-in-out;
}
#shopify-section-{{ section.id }} brand-button a:visited {
color: unset;
}
#shopify-section-{{ section.id }} li:not(:last-child) brand-button a:after {
content: "";
height: 25px;
right: 0;
border-left: 1px solid #e5e5e5;
position: absolute;
}
{% for block in section.blocks %}
#{{ block.id }} img {
filter: opacity(0.5) drop-shadow(0 0 0 {{ block.settings.color }}) saturate(1000%);
}
#{{ block.id }} [data-active] img {
filter: opacity(0.5) drop-shadow(0 0 0 {{ block.settings.active_color }}) saturate(1000%);
}
#{{ block.id }} a:hover img,
#{{ block.id }} a:focus img {
filter: opacity(0.5) drop-shadow(0 0 0 {{ block.settings.active_color }}) saturate(1000%);
}
{% endfor %}
</style>
{% if section.blocks.size != 0 %}
<nav>
<ul>
{% for block in section.blocks %}
{% case block.type %}
{% when 'brand' %}
{% unless block.settings.page == null %}
<li id="{{ block.id }}" {{ block.shopify_attributes }}>
<brand-button data-brand="{{ block.settings.brand }}">
{% assign search_parameter = block.settings.brand | url_encode | prepend: '?context=' %}
{% assign url = block.settings.page.url | append: search_parameter %}
<a href="{{ url }}">
{% if block.settings.logo != null %}
{{ block.settings.logo | image_url: height: 25 | image_tag: alt: block.settings.brand }}
{% else %}
{{ block.settings.brand }}
{% endif %}
</a>
</brand-button>
</li>
{% endunless %}
{% endcase %}
{% endfor %}
</ul>
</nav>
{% endif %}
<script type="application/javascript">
/**
* BrandContext custom element that manages brand context across the application.
* This element wraps every HTMLElement that requires context of the brand from a JS perspective.
* It listens to "subscribe:store_context" events on the window to add subscribers to context changes.
* When context is changed, it triggers a "context_changed" event to all subscribers.
*
* Setting context on the brand-context element will trigger updates: element.context = "Brand";
* @extends {HTMLElement}
*/
class BrandContext extends HTMLElement {
/**
* Initialize the BrandContext element
*/
constructor() {
super();
/** @type {URL} The current page URL */
this.url = new URL(window.location.href);
/** @type {string|null} The context parameter from the URL search params */
this.searchParamContext = this.url.searchParams.get('context');
/** @type {Array<HTMLElement>} Array of elements subscribed to context changes */
this.subscribers = [];
}
/**
* Set the brand context value in session storage and notify subscribers
* @param {string} value - The brand context value to set
* @returns {string} The current context value from session storage
*/
set context(value) {
window.sessionStorage.setItem('store_context', value);
if (window.sessionStorage.getItem('store_context') !== value) {
console.error('Error setting store context, ', value);
return window.sessionStorage.getItem('store_context');
}
this.triggerContextChange(value);
return window.sessionStorage.getItem('store_context');
}
/**
* Get the current brand context value from session storage
* @returns {string|null} The current context value
*/
get context() {
return window.sessionStorage.getItem('store_context');
}
/**
* Notify all subscribers of a context change
* @param {string} value - The new context value
*/
triggerContextChange(value) {
for (const subscriber of this.subscribers) {
subscriber.dispatchEvent(
new CustomEvent('context_changed', {
detail: {
/** @type {BrandContext} The source of the context change */
source: this,
/** @type {string} The new brand value */
brand: value,
},
}),
);
}
}
/**
* Add a subscriber to context changes
* @param {CustomEvent} event - The event containing the subscriber element
* @returns {void}
*/
addSubscriber(event) {
if (!event?.detail) {
return console.error('No CustomEvent was passed.');
}
if (event.detail instanceof HTMLElement === false) {
return console.error('No valid element was passed to the event.');
}
if (!event.detail?.uuid) {
return console.error("Element doesn't have a generated UUID");
}
/** @type {HTMLElement|undefined} */
const isSubscribed = this.subscribers.find((element) => element.uuid === event.detail.uuid);
if (isSubscribed) {
return console.warn('Element already subscribed, not subscribing.');
}
this.subscribers.push(event.detail);
}
/**
* Add event listeners for subscriber registration
*/
addListeners() {
window.addEventListener('subscribe:store_context', this.addSubscriber.bind(this));
}
/**
* Initialize the component - called from connectedCallback
*/
initialize() {
this.addListeners();
// Set context from URL parameter if available
if (this.searchParamContext) {
/** @type {string} The decoded context from the URL */
const decodedContext = decodeURIComponent(this.searchParamContext);
window.sessionStorage.setItem('store_context', decodedContext);
return;
}
}
/**
* Lifecycle callback when element is added to the DOM
*/
connectedCallback() {
this.initialize();
}
}
window.customElements.get('brand-context') || window.customElements.define('brand-context', BrandContext);
/**
* BrandButton custom element that represents a selectable brand option
* @extends {HTMLElement}
*/
class BrandButton extends HTMLElement {
/**
* Initialize the BrandButton element
*/
constructor() {
super();
/** @type {HTMLAnchorElement|null} The anchor element within this button */
this.anchor = this.querySelector('a');
/** @type {BrandContext|null} Reference to the brand context element */
this.context = document.querySelector('brand-context');
/** @type {string} The brand name associated with this button */
this.brand = this.dataset.brand;
/** @type {string} Unique identifier for this element */
this.uuid = window.crypto.randomUUID();
}
/**
* Handle context change events
* @param {CustomEvent} event - The context changed event
*/
update(event) {
// do whatever you would want here.
console.log(event);
}
/**
* Subscribe to the brand context
*/
subscribeToContext() {
/** @type {number} Interval ID for checking if context is mounted */
const checkInterval = setInterval(() => {
if (window.customElements.get('brand-context')) {
clearInterval(checkInterval);
window.dispatchEvent(new CustomEvent('subscribe:store_context', { detail: this }));
this.addEventListener('context_changed', this.update.bind(this));
}
}, 100);
}
/**
* Lifecycle callback when element is added to the DOM
*/
connectedCallback() {
this.subscribeToContext();
// if context exists and value of context is equal to this brand, it's active.
if (this.context && this.context.context == this.brand) {
this.dataset.active = true;
}
}
}
window.customElements.get('brand-button') || window.customElements.define('brand-button', BrandButton);
</script>
{% schema %}
{
"name": "Brand Switcher",
"tag": "section",
"settings": [
{
"type": "color",
"label": "Background",
"id": "background"
}
],
"blocks": [
{
"type": "brand",
"name": "Brand",
"limit": 5,
"settings": [
{
"type": "text",
"label": "Brand Name",
"id": "brand"
},
{
"type": "page",
"label": "Page",
"id": "page"
},
{
"type": "image_picker",
"label": "Logo",
"id": "logo"
},
{
"type": "color",
"label": "Non-Active color",
"id": "color"
},
{
"type": "color",
"label": "Active color",
"id": "active_color"
}
]
}
],
"presets": [
{
"name": "Brand Switcher"
}
]
}
{% endschema %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment