Liquid, JavaScript, themes, sales channels
Hello! Before I begin I'd like to say that I am extremely new to Shopify, so if I am posting on the wrong forum or somehow breaking some post guidelines, please accept my sincerest apologies. I am happy to redirect/rewrite this post as necessary.
That said, I am working on behalf of an acquaintance who is looking to add some extra features to their website. They are looking to run a website for a clothing store. There is a prototype for this website available at this link:
In particular, we are looking to change up one of the product pages so that the images displayed on screen change as the user clicks on different things. For example, if you go to one of their sample pages to sell a shirt product, at this link:
https://rvrs.shop/products/mens-longsleeve-fitted
You'll notice that the product images which are displayed change when the user changes the color option from green to red. We were hoping to add similar functionality that could change which product images get displayed as the "Design" field changes.
For example, while the user has the "Green" color and the "1MoreRep" design selected, we would hope to see sample images for the green shirt as well as the "1MoreRep" shirt design. Then, when the user keeps the color the same but changes the design to "10MoreLbs", we'd want the green shirt images to stay, have the "1MoreRep" samples go away, and have "10MoreLbs" sample images show up.
Behind the scenes, on our admin page, within the product page for this shirt ("Men's Longsleeve Fitted"), we have different Media images loaded. Each of these images has an "alt text", and some of them end with the "#color_military-green", which we suspect might cause that image to show up when the color field is green. However, appending similar alt text for the design, i.e. "#design_DontGiveUp", doesn't seem to work. We recognize that it's possible the alt code may actually have no effect on what gets displayed and maybe it's just a coincidence.
That said, is this dynamic display of images something that should be possible? Do you have any suggestions on where to start? I have tons of programming experience but no familiarity with Shopify.
Thank you VERY MUCH in advance for any help you have to offer! Thanks and take care! 🙂
Aside - Яeversed branding and product offering is a cute idea, though the copy and video definitely needs some work to get the point across that the clothing is reversed for the person at a mirror,( or looking down at it if they're also doing that gimmick). Currently is just seems like budget video editing didn't flip the video so the shirt text is backwards. Video probably needs a rotation shot clearing pointing towards a mirror to see the proper flow of motivational text.
For the technical,
@natecoder wrote:You'll notice that the product images which are displayed change when the user changes the color option from green to red. We were hoping to add similar functionality that could change which product images get displayed as the "Design" field changes.
Are you not using the 3rd option of the products variant options and assigning an image with that text?
With the 100 variant limit and current options this shouldn't be a problem. And if it will be a limit problem in the future note that if an attribute like Size doesn't change the price or the sku just make it a line item property to free up the slots.
@natecoder wrote:Each of these images has an "alt text", and some of them end with the "#color_military-green",
Did someone else do an initial setup but not give any docs or a walkthrough?
Using an admin images alt text is a convention some themes and conventions use to try and attach meta data to image assets.
Theme seems like the streamline theme https://themes.shopify.com/themes/streamline/styles/core
Which yes uses that convention for it's "variant-image-sets" feature https://archetypethemes.co/blogs/streamline/create-variant-image-sets
Themes sold on the theme store should already support this otherwise start in the dev docs to implement it.
A custom solution should be solvable by using the variants literal assigned images first for the complete 3 options.
https://shopify.dev/themes/product-merchandising/variants
Other techniques can be inferred from the depreciated swatch tutorial
Hello there, just wanted to say thank you very much for the reply! I am still trying to make sense of this and will post again if/how I was able to make it work. Thank you! 🙂
Hello there,
I am bumping this post as I have some updates and questions on all of this. (I wasn't sure if I should bump this post or start a new one, so apologies if I should have started a new post; I am still willing to start a new post of course). Also, I'd like to take this moment to once again thank @PaulNewton for your reply!
In short, it seems I got a basic version working, but I am posting here to make sure that the solution I came up with seems valid, and wouldn't clash with any Shopify conventions, cause any issues across different browsers/devices, have any security flaws, or anything else like that. I looked over @PaulNewton 's post but as I am new to Shopify and Liquid, I must admit a lot of it didn't make sense to me. I wasn't sure of any docs/walkthroughs to follow. I went through the links @PaulNewton mentioned, and many others I could find, but none of it seemed to be working.
So, after some time, I decided to "jerry-rig" my own Javascript solution using basic HTML DOM stuff. I'm not exactly the most experienced web developer so I wouldn't be surprised if my solution or my code has some flaws (and it certainly isn't using the fanciest methods/code out there). This is the solution/algorithm I came up with:
1) Include each product image's design as a tag in the image's alt-text (using the % symbol instead of the # symbol to avoid interference with Shopify's existing # alt-text tagging system)
2) Identify what design button is currently pressed
3) Hide thumbnail images and thumbnail buttons that do not match the selected design, and show those that do
Currently, it seems to be working pretty well, at least with some brief testing from my Windows desktop computer with Google Chrome, Mozilla Firefox, and Microsoft Edge.
Here are some known issues:
By the way, we are indeed using the "Streamline" theme, and so far the code has only been applied to the "Men's Longsleeve Fitted" product page.
The main two .liquid code files I have been working in are "product-images.liquid" (for the thumbnails) and "variant-button.liquid" (to react to design button presses). Here is the entire code I have for both files:
product-images.liquid
{% comment %}
It's best to load images as JPG instead of PNG for faster
loading, so that is our default. If you upload
PNG files and want to keep them that way, set the line
below to false.
Can set thumbnail position to 'below', but only suggested
when using the 'Fade' image style (set in Product grid)
{% endcomment %}
{%- liquid
assign force_jpg = true
assign featured_media = product.selected_or_first_available_variant.featured_media | default: product.featured_media
assign thumbnail_position = 'beside'
-%}
{%- unless product.empty? -%}
<div
data-product-images
data-zoom="{{ product_zoom_enable }}"
{% if product_zoom_enable %}
{% if product_image_type == 'slider' %}
data-has-slideshow="true"
{% endif %}
{% endif %}>
<div class="product__photos product__photos--{{ thumbnail_position }}">
<div class="product__main-photos{% if product_image_type == 'slider' %} product__main-photos--slider{% endif %}" data-aos data-product-single-media-group>
<div class="product__main-photos-wrapper">
<div id="ProductPhotos-{{ section_id }}" data-product-photos>
{%- for media in product.media -%}
{%- render 'media',
section_id: section_id,
media: media,
force_jpg: force_jpg,
featured_media: featured_media,
loopIndex0: forloop.index0,
loopIndex: forloop.index,
product_zoom_enable: product_zoom_enable,
product_zoom_size: product_zoom_size,
product_image_size: product_image_size,
product_image_type: product_image_type,
isModal: isModal,
video_looping: video_looping,
video_style: video_style
-%}
{%- endfor -%}
</div>
{%- if product_zoom_enable -%}
<button type="button" class="btn btn--tertiary btn--circle product__photo-zoom{% if product_image_type == 'stacked' %} medium-up--hide{% endif %}">
<svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-search" viewBox="0 0 64 64"><path d="M47.16 28.58A18.58 18.58 0 1 1 28.58 10a18.58 18.58 0 0 1 18.58 18.58zM54 54L41.94 42"/></svg>
<span class="icon__fallback-text">{{ 'general.accessibility.close_modal' | t }}</span>
</button>
{%- endif -%}
</div>
{%- assign first_3d_model = product.media | where: 'media_type', 'model' | first -%}
{%- if first_3d_model -%}
<button
aria-label="{{ 'products.product.view_in_space_label' | t }}"
class="product-single__view-in-space"
data-shopify-xr
data-shopify-model3d-id="{{ first_3d_model.id }}"
data-shopify-title="{{ product.title }}"
data-shopify-xr-hidden
>
<svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-3d" viewBox="18.24 17.35 24.52 28.3"><path fill="#3A3A3A" d="M30.5 17.35l-12.26 7.07v14.16l12.26 7.07 12.26-7.08V24.42L30.5 17.35zM20.24 37.42V25.58l10.26-5.93 10.13 5.85-10.13 5.88v12l-10.26-5.96z"/></svg>
<span class='product-single__view-in-space-text'>
{{ 'products.product.view_in_space' | t }}
</span>
</button>
{%- endif -%}
<div class="product__photo-dots"></div>
</div>
<div
id="ProductThumbs-{{ section_id }}"
class="product__thumbs product__thumbs--{{ thumbnail_position }} small--hide{% if product.media.size == 1 %} medium-up--hide{% endif %}"
data-position="{{ thumbnail_position }}"
data-product-thumbs
data-aos>
{%- if product_image_type == 'stacked' -%}
<div class="product__thumbs-sticky">
{%- endif -%}
{%- if product.media.size > 1 -%}
{%- for media in product.media -%}
{%- liquid
assign image_set = false
assign image_set_group = ''
if media.alt contains '#'
assign image_set_full = media.alt | split: '#' | last
if image_set_full contains '_'
assign image_set = true
assign image_set_group = image_set_full | split: '_' | first
endif
endif
-%}
<div class="product__thumb-item{% if product_image_type == 'stacked' and forloop.index == 1 %} thumb--current{% endif %}"
data-product-thumb-item
data-index="{{ forloop.index0 }}"
{% if image_set %}
data-set-name="{{image_set_group}}"
data-group="{{image_set_full}}"
{% else %}
data-group
{% endif %}>
<a
id="{{media.id}}"
href="{{ media.preview_image | img_url: product_zoom_size }}"
class="image-wrap product__thumb js-no-transition"
data-id="{{ media.id }}"
data-index="{{ forloop.index0 }}"
data-product-thumb
style="height: 0; padding-bottom: {{ 100 | divided_by: media.preview_image.aspect_ratio }}%;">
<!--
<span class="product__thumb-icon">
<svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-3d" viewBox="18.24 17.35 24.52 28.3"><path fill="#3A3A3A" d="M30.5 17.35l-12.26 7.07v14.16l12.26 7.07 12.26-7.08V24.42L30.5 17.35zM20.24 37.42V25.58l10.26-5.93 10.13 5.85-10.13 5.88v12l-10.26-5.96z"/></svg>
</span> -->
{%- if media.media_type == 'video' or media.media_type == 'external_video' -%}
<span class="product__thumb-icon">
<svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-play" viewBox="18.24 17.35 24.52 28.3"><path fill="#323232" d="M22.1 19.151v25.5l20.4-13.489-20.4-12.011z"/></svg>
</span>
{%- endif -%}
{%- if media.media_type == 'model' -%}
<span class="product__thumb-icon">
<svg aria-hidden="true" focusable="false" role="presentation" class="icon icon-3d" viewBox="18.24 17.35 24.52 28.3"><path fill="#3A3A3A" d="M30.5 17.35l-12.26 7.07v14.16l12.26 7.07 12.26-7.08V24.42L30.5 17.35zM20.24 37.42V25.58l10.26-5.93 10.13 5.85-10.13 5.88v12l-10.26-5.96z"/></svg>
</span>
{%- endif -%}
{%- if force_jpg -%}
{%- assign img_url = media.preview_image | img_url: '1x1', format: 'jpg' | replace: '_1x1.', '_{width}x.' -%}
{%- else -%}
{%- assign img_url = media.preview_image | img_url: '1x1' | replace: '_1x1.', '_{width}x.' -%}
{%- endif -%}
<img class="lazyload"
data-src="{{ img_url }}"
data-widths="[120, 360, 540, 750]"
data-aspectratio="{{ image.aspect_ratio }}"
data-sizes="auto"
alt="{{ media.alt | escape }}">
<noscript>
<img class="lazyloaded" src="{{ media.preview_image | img_url: '180x' }}" alt="{{ media.alt | escape }}">
</noscript>
</a>
</div>
{%- endfor -%}
{%- endif -%}
{%- if product_image_type == 'stacked' -%}
</div>
{%- endif -%}
</div>
</div>
</div>
<script type="application/json" id="ModelJson-{{ section_id }}">
{{ product.media | where: 'media_type', 'model' | json }}
</script>
{%- else -%}
<div class="product__photos">
<div class="product__main-photos" style="width: 100%">
<div id="ProductPhotos-{{ section_id }}">
<div data-index="{{ forloop.index0 }}">
<a href="#">
{{ 'product-1' | placeholder_svg_tag: 'placeholder-svg' }}
</a>
</div>
</div>
</div>
</div>
{%- endunless -%}
variant-button.liquid
{%- assign swatch_file_extension = 'png' -%}
{%- assign option_index = forloop.index -%}
<div class="variant-wrapper variant-wrapper--{{ variant_type }} js">
<label class="variant__label{% if option.name == 'Default' or option.name == 'Title' %} hidden-label{% endif %}{% unless variant_labels_enable %} hidden-label{% endunless %}"
for="ProductSelect-{{ section_id }}-option-{{ forloop.index0 }}">
{{ option.name }}
{%- if is_color -%}
<span class="variant__label-info">
—
<span
id="VariantColorLabel-{{ section_id }}-{{ forloop.index0 }}"
data-option-index="{{ color_option_index }}">
{{ option.selected_value }}
</span>
</span>
{%- endif -%}
</label>
<fieldset class="variant-input-wrap"
name="{{ option.name }}"
data-index="option{{ option_index }}"
data-handle="{{ option.name | handleize }}"
id="ProductSelect-{{ section_id }}-option-{{ forloop.index0 }}">
<legend class="hide">{{ option.name }}</legend>
{%- for value in option.values -%}
{%- assign product_available = true -%}
{%- if product.options.size == 1 -%}
{%- assign product_available = product.variants[forloop.index0].available -%}
{%- endif -%}
<div
class="variant-input"
data-index="option{{ option_index }}"
data-value="{{ value | escape }}">
<input type="radio"
{% if option.selected_value == value and product_available %} checked="checked"{% endif %}
value="{{ value | escape }}"
data-index="option{{ option_index }}"
name="{{ option.name }}"
data-variant-input
{% if is_color %}data-color-swatch{% endif %}
class="variant__input-{{ section_id }}{% unless product_available %} disabled{% endunless %}"
{% if is_color %} data-color-name="{{ value | escape }}"{% endif %}
{% if is_color %} data-color-index="{{ color_option_index }}"{% endif %}
id="ProductSelect-{{ section_id }}-option-{{ option.name | handleize }}-{{ value | url_encode }}">
{%- if is_color -%}
{%- assign color_image = value | handle | append: '.' | append: swatch_file_extension | asset_img_url: '50x' | prepend: 'https:' | split: '?' | first -%}
{%- assign color_swatch_fallback = value | split: ' ' | last | handle -%}
<label
for="ProductSelect-{{ section_id }}-option-{{ option.name | handleize }}-{{ value | url_encode }}"
class="color-swatch color-swatch--{{ value | handle }}{% unless product_available %} disabled{% endunless %}"
style="background-image: url({{ color_image }}); background-color: {{ color_swatch_fallback }};"
>
{{ value | escape }}
</label>
{%- else -%}
<label onclick="updateSelectedDesign(this);" for="ProductSelect-{{ section_id }}-option-{{ option.name | handleize }}-{{ value | url_encode }}"{% unless product_available %} class="disabled"{% endunless %}>{{ value | escape }}</label>
{%- endif -%}
</div>
{%- endfor -%}
</fieldset>
</div>
<!-- CUSTOM JAVASCRIPT THUMBNAIL DESIGN SOLUTION CODE -->
<script>
// call init on page load
window.addEventListener("load", init);
// whether or not page has loaded once
var loaded = false;
// currently selected design
var currentDesign = undefined;
// called after the page is loaded to make sure all relevant elements can be retrieved from page
// for some reason this is called multiple times so we use the "loaded" variable to prevent this function from triggering multiple times
function init()
{
if(loaded)
{
return;
}
loaded = true;
currentDesign = getInitialDesign();
updateThumbnails();
}
// hides thumbnails that do not match the currently selected design and shows those that do
function updateThumbnails()
{
var thumbnails = document.getElementsByClassName("lazyautosizes lazyloaded");
for(var i = 0; i < thumbnails.length; i++)
{
var thumbnail = thumbnails[i]; // <img>
var thumbnailLink = thumbnail.parentElement; // <a>
var thumbnailContainer = thumbnailLink.parentElement; // <div>
if(!thumbnail.alt.includes(currentDesign) && !thumbnail.alt.includes("%design_none") && currentDesign != undefined)
{
thumbnail.style.visibility = 'hidden'; // hide thumbnail image
thumbnailLink.style.visibility = "hidden"; // hide thumbnail link
thumbnailLink.style.pointerEvents = "none"; // make thumbnail link unclickable
thumbnailContainer.style.display = "none"; // hide thumbnail container
}
else
{
thumbnail.style.visibility = 'visible'; // show thumbnail image
thumbnailLink.style.visibility = "visible"; // show thumbnail link
thumbnailLink.style.pointerEvents = "auto"; // make thumbnail link clickable
thumbnailContainer.style.display = "block"; // show thumbnail container
}
}
}
// updates currentDesign to match design string from recently clicked design button
// called via the relevant design label element's onclick property
function updateSelectedDesign(selectedElement)
{
currentDesign = selectedElement.innerHTML;
updateThumbnails();
}
// helper function that extracts the "value" property (design string) from a design radio button
function extractDesign(button)
{
var designButtonInnerHTML = button.innerHTML;
var valueStart = designButtonInnerHTML.indexOf("value=\"") + 7;
var valueEnd = designButtonInnerHTML.indexOf("data-index=") - 2;
var design = designButtonInnerHTML.substring(valueStart, valueEnd);
return design;
}
// gets the design that is selected when the page initially loads
function getInitialDesign()
{
var radioButtons = document.getElementsByClassName("variant-input");
for(var i = 0; i < radioButtons.length; i++)
{
var radioButton = radioButtons[i];
if (radioButton.innerHTML.includes("checked=\"checked\"") && radioButton.innerHTML.includes("name=\"Design\""))
{
return extractDesign(radioButton);
}
}
}
</script>
Any ideas, comments, feedback, concerns, criticism, WHATEVER would be greatly appreciated! 🙂 Thanks!
User | RANK |
---|---|
33 | |
25 | |
17 | |
9 | |
9 |
Photo by Marco Verch Sales channels on Shopify are various platforms where you can sell...
By Ollie May 25, 2023Summary of EventsBeginning in January of 2023, some merchants reported seeing a large amo...
By Trevor May 15, 2023With 2-Factor Authentication being required to use Shopify Payments, we’re here to help yo...
By Imogen Apr 26, 2023