Dynamic Image Display on Product Page

natecoder
Visitor
3 0 0

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:

https://rvrs.shop/

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! 🙂

Replies 3 (3)

PaulNewton
Shopify Partner
6274 573 1319

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

 https://community.shopify.com/c/Shopify-Design/Product-pages-Add-color-swatches-to-products/m-p/6164... 

 

 

 

 

Save time & money ,Ask Questions The Smart Way


Confused? Busy? Get the solution you need paull.newton+shopifyforum@gmail.com


Problem Solved? ✔Accept and Like solutions to help future merchants

Answers powered by coffee Buy Paul a Coffee for more answers or donate to eff.org


natecoder
Visitor
3 0 0

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! 🙂

natecoder
Visitor
3 0 0

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:

  • all images show up in the "slideshow view" regardless of the selected design
    • you can enter the "slideshow mode" by clicking on the main product image that is displayed, then if you cycle through the images you will see the ones I am hiding
  • the hidden images may very briefly appear when the page loads
  • the main selected image is not updated properly when the design selection changes

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">
        &mdash;
        <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!