Liquid Variable Inheritance > Clean Code & Best Practices

Ceri-Waters
Shopify Partner
69 4 12

Hiya,

So I'm curious on how other devs keep their code clean with variable Inheritance restrictions with the {% render %} tag. (In that you can't return variables to the parent)

Lets say a client gets in touch, and they want a little ribbon to appear next to products when they have a certain tag, lets say "Black Friday".

Ok great, that's simple you just add some code onto the Collection Snippet.

 

{% if product.tags contains 'Black Friday' %}
Some HTML
{% endif %}

 

 

They then say they want it on the Product Template too, But actually your HTML needs to be a little different here to accomodate some styling differences. So you add some code onto the product template

 

{% if product.tags contains 'Black Friday' %}
Some other HTML
{% endif %}​

 


Then the same again for their collection sections. The client then wants some further ribbons for Limited Edition Products, you now have to update many files for this simple tweak. Surely there must be a better way to handle this? Well wouldn't it be nice if you had a file which would calculate the value of productRibbon, and the snippets/templates only have to care about outputting it? As an example:

 

# Variable Calculation Snippet
{% assign productRibbon = false %}
{% if product.tags contains 'Black Friday' %}
     {% assign productRibbon = 'Black Friday' %}
{% elsif product.tags contains 'Limited Edition' %}
     {% assign productRibbon = 'Limited Edition' %}
{% endif %}

# Collection Snippet
{% if productRibbon != false %}
     <p class="product__ribbon  ribbon--{{ productRibbon | handle }}">
          {{ productRibbon }}
     </p>
{% endif %}

# Product Template
{% if productRibbon != false %}
     <p class="product-item__ribbon  ribbon--{{ productRibbon | handle }}">
          {{ productRibbon }}
     </p>
{% endif %}

# Section Snippet
{% if productRibbon != false %}
     <p class="product--section__ribbon d-none d-lg-block  ribbon--{{ productRibbon | handle }}">
          {{ productRibbon }}
     </p>
{% endif %}

 

 

But wait.. the only way we can have the Variable Calculation Snippet pass back the variable to the parent file is by using the {% include %} tag, the {% required %} tag wouldn't make it accessibile to anything else. There's no way to make this variable global in the files that need it.

# Attributes Ribbon Snippet
{% assign productRibbon = false %}
{% if product.tags contains 'Black Friday' %}
     {% assign productRibbon = 'Black Friday' %}
{% elsif product.tags contains 'Limited Edition' %}
     {% assign productRibbon = 'Limited Edition' %}
{% endif %}

# Collection Product Snippet
{% include'attributes-ribbon' %}

<div class="product">
      {% if productRibbon != false %} 
	Some HTML
      {% endif %}
</div>



So now you have the dilema of either copy/pasting your calculation logic in each template - or you have to have a single snippet for both calculating the value, and displaying it. But the HTML needs to be different in some cases, so you pass in an 'outputType' variable too and run a switch statement on it. Everytime this file is included now it's doing extra checks for the output, and you have to run through it many combinations. 

What's the best practice here???

Thanks in Advance!

 

 

0 Likes
PaulNewton
Shopify Partner
2573 135 448

All of this could be considered a consequence of us not being able to create objects otherwise all of this silliness would go away in favor of using the "with" parameter or even the for parameter /shrug

Impossible example {% render 'logic', variables:impossibletocreatevariablesobject %}

We also have to keep in mind {% include %} is "depreciated" though still functional we need to be sure why it's being used and assuming it could just stop working one day.

Add on the depreciation of sass and variable passing is gonna become a growing pain and theme.liquid files are becoming even more monolithic configuration beasts.

 

So with that perspective what are we bounded by?

  1. Template scoped variables(include,assign,capture)
  2. Include tag variable parameters
  3. Render tag variable parameters
  4. Render tag does not allow the include tag inside them
  5. With and For parameters used with Include and Render tags
  6. Section Encapsulation
  7. There is NO access to a sections setting object outside that section
  8. Section settings|blocks are local scope only so must be passed to render tags using parameters
  9. Global objects(settings)
  10. Context objects(customer,order,etc).
  11. Not exhaustive or terms 100% accurate,   Expect changes if new theme architecture comes out.

Use  {%- include -%} tags for the variable snippet so they are Template scoped variables, scoped to the containing template where the include is used.

Use {% render %} tags for the logic that consumes those variables by passing them into the rendered snippet as variable parameters

 

{%- include 'variables' -%}
{% render 'logic',  variables_variable1:variables_variable1 , variables_variable2:variables_variable2 %}

 

Note that a namespaced naming convention will go far , otherwise it will be confusing to figure out WHERE those variables are coming from everytime there's a {% render %} tag as the file grows when those variables are not from a clear and near {% assign %} tag.

Make use of whitespace control characters, {%- -%} ,to try and limit the impact the output of the variables snippets has on the rendered source especially if used with loops, or the FOR parameter.

 

Remember then within the render you will not be able to change those parameter variables, per the render docs:

Assigning a variable within the snippet that is also assigned in the parent template does not overwrite its value in the parent template.

Though it is not uncommon to see things like '{% assign current_variant = product.selected_or_first_available_variant %}' in templates that are mere containers for sections,  but then in those sections they still have to define current_variant , it's just the template coding being confusing about where the assignment comes from by putting it before the section tags in the source order making it seem like the sections could access that variable since it's assigned before the section is called.

 

Some weird escape hatches, that are NOT clean code nor a best practice is:

Use a {%- capture -%} tag on the render tags with a config parameter so it only outputs a string of variable(s) and values then chuck the captured var into the next snippet for use with custom string parsing logic.

 

{%- capture rendered_variables -%}
  {% render 'logic',  output_variables_only: true %}
{%- endcapture -%}
{%- assign rendered_variables = rendered_variables | split:"," -%}
{%- for value in rendered_variables -%}
  {% comment %} verbose string parsing logic such as knowing when to change a variable in the current scope based on output of an include|render tag at least until json is a first class liquid context /wrist {% endcomment %}
{%- endfor -%}

 

 https://community.shopify.com/c/API-Announcements/New-json-string-value-type-for-Metafield-object/m-...

Another other unclean route is pumping the variables into frontend apis like the cart attributes through ajax indirectly on the client side.

 

It be interesting to see a test with the theme inspector tool if repeating a variables snippet with an include|render tag inside multiple snippets is optimized at all for either include or the render tag.

 

Problem Solved? ✔️Accept and ? Like the solution so you can help others.
Buy me a coffee ☕ paypal.me/paulnewton or donate to eff.org
Confused? Busy? Buy a custom solution paull.newton+shopifyforum@gmail.com
Dom_Masters
Shopify Partner
3 0 2

Best practice is so hard to define for something like this because at this level we have so many tools at our disposal to achieve what we need to do, and the "best" method will be completely dependent on what works best for you in this exact situation. Performance wise something like this is minuscule, even considering 24 or more products this would be nothing compared to the iteration cost.

Shopify isn't great at detailing how their render tag works in context of caching either, I have some assumptions that it takes into consideration what objects are passed, and referenced, e.g. accessing something like {{ customer }} or {{ cart }} may cause it to not bother caching that snippet, but I don't know for sure.  Personally I'm in favour of the horrible render, capture combo for certain cases. In this scenario I don't think you need it but when you don't have another option it's a really good alternative, especially if Shopify does consider caching the snippets based on say, the product we pass into it (not certain).

I think for this method I'd make use of the global variables, then heavily lean on CSS to do what I need, something along these lines;

<!-- templates/collection.liquid -->
...foreach product...
{% render 'object.product-badge' with { product: product } -%}

<!-- templates/product.liquid -->
...
{% render 'object.product-badge' with { product: product} -%}

<!-- snippets/object.product-badge.liquid -->
{% liquid
  for tag in product.tags
    unless tag contains 'Badge_'
      continue
    endunless

    assign name = tag | split: '_' | last | escape
    case template.name
      when 'product'
        echo '<div class="o-badge is-product">' | append: name | append: '</div>'
        break
      when 'collection'
        echo '<div class="o-badge is-collection">' | append: name | append: '</div>'
        break
      else
        break
  endfor
%}

<!-- assets/styles.css -->
.o-badge { /* Shared Styles */ }
.o-badge.is-product { /* Product Page Styles */ }
.o-badge.is-collection { /* Collection Page Styles */ }

 

I also feel it's important to consider what the item you're building means in context to your website as a whole, for me a badge is an object, something that can exist with only minimal context of what is around it, and how it displays should be fairly independent of what is around it, so having a scenario you've described where it is aware of it's surroundings sounds more like a component style object, that can perhaps be presented in different 'styles' based on context, so maybe something better suited for a with { } parameter rather than my case template.name logic.

You also mentioned in isolation about "we need to add another badge" yet your logic is hard coded to check for a specific tag, where a solution similar to above where the suffix of the tag is fairly ambiguous and only the prefix is important for the logic, allowing the client to anything they desire, such as Badge_New, Badge_Sale, Badge_Preorder, etc. If they want different styles for these badges I'd probably handlize the name and turn that into a class that I then style with CSS appropriately.

Finally just going to raddle off some things that I think help with performance which may influence your decision, may not;

  • JavaScript, HTML and CSS will always kick Liquid's butt, even if it's not elegant. Is it a core part of the site that would be noticed on JS-less browsers? Spend that server power on the core site functionality.
  • Liquid struggles with forloops and context heavily, I have done a little testing but like C it may be more efficient to allocate some memory for a variable and then constantly rewrite it
  • {% render for %} is somehow more performant than {% for %} ... {% render %}, I assume Shopify may be doing some parallel operations here.
  • Can it be static content?
  • Metafields are great for small content, when you use them heavily it really strains liquid though.
  • Avoid the Shop metafields because that's a constant strain
  • Include is perfectly cromulent and embiggens me
  • Is the very small performance gain worth the loss of code maintainability and readability?
  • Is there a stupider solution? Tags are great but what about product.collections? Can you use that and check the collection handles?
PaulNewton
Shopify Partner
2573 135 448

 


@Dom_Masters wrote:

 I have some assumptions that it takes into consideration what objects are passed, and referenced, e.g. accessing something like {{ customer }} or {{ cart }} may cause it to not bother caching that snippet, but I don't know for sure


This is something in my backlog to try validate the assumptions whether or not there's branching optimization for which states, or not,  about conditional statements using object-booleans,captures,objects, etc.

i.e.  {% if customers and customer.orders !=0 %} then variable creations, or to have separate {%if customer%}{%assign%}... ...{% if customer.orders !=%}

And if there are situations where it's better to do custom templating using string replacement on captures, such as heavily nested forloops.

 

Problem Solved? ✔️Accept and ? Like the solution so you can help others.
Buy me a coffee ☕ paypal.me/paulnewton or donate to eff.org
Confused? Busy? Buy a custom solution paull.newton+shopifyforum@gmail.com
0 Likes