Building Better Liquid Arrays

Working with variables in Liquid is a bit of a challenge. It doesn’t seem to have been designed with complex data-manipulation in mind. Who’d have thought? As soon as you need to create any sort of structured data, you’ll begin to realise how much of a pain liquid can be, and you might find yourself tempted to just print all of your variables and let JavaScript handle them.

Outside of the built-in liquid objects, such as products, collections and orders, the only luck you generally have with creating your own objects is via metafields or sections.

You can’t create objects at all (to my knowledge, watch this space); and whilst you can create arrays, you can’t exactly create them directly. Most of the time if you want to create a new array, you’ll probably end up creating a string of the values you need, joined with a delimiter such as a comma, then splitting said string by the comma; but if you need to do anything complex this can still be a massive pain. What if you need a non-string value? Sometimes you can convert them to their old data type afterwards, but this causes your logic to get all kinds of messy.

In a lot of cases, converting values to strings can still work just fine, but there are plenty of times where it’s just more convenient not to do so. For example, imagine that you’re working on a store that sells, among other things, the most exciting of all products: notebooks.

Some of these notebooks are lined, others have grids, and others have blank pages. They also come in a bunch of different colours, and you want to be able to recommend all the different coloured notebooks per-paper type on each product page.

The first step you’ve taken in order to do so is to tag each notebook with a relevant tag to represent its paper type, such as paper:grid. In order to find related grid notebooks you do the following:

{% assign delimiter = ',' %}
{% assign notebook_handles = '' %}

{% for product in collections.all.products %}
  {% if product.tags contains 'paper:grid' %}
    {% unless notebook_handles == '' %}
      {% assign notebook_handles = notebook_handles | append: delimiter %}
    {% endunless %}

    {% assign notebook_handles = notebook_handles | append: product.handle %}
  {% endif %}
{% endfor %}

{% assign notebook_handles = notebook_handles | split: delimiter %}

Party time! Now you’ve got an array of handles that you can use to reference all of the notebooks with grid paper anywhere else within the template, but each time you do so, you have to access the product by calling all_products[notebook_handle]. This is pretty rubbish. Why wouldn’t liquid just let you push the product itself into an array? Well it turns out, it will.

Enter the sort filter. sort does was it says on the tin. It sorts a list of values. But what if you use it on a non-array value? Turns out, given a non-array value, such as a product object, sort will simply place that value inside of an array.

With this in mind, the example above can become the following:

{% assign notebooks = null | sort %}

{% for product in collections.all.products %}
  {% if product.tags contains 'paper:grid' %}
    {% assign notebook = product | sort %}
    {% assign notebooks = notebooks | concat: notebook %}
  {% endif %}
{% endfor %}

Now you have an array of notebook objects you can use anywhere else within the code. It’s worth noting here that before looping through the products, I created an empty array by using sort on null. I then had to convert the notebook I was adding to the array into an array containing said notebook, using the sort filter. From there, I could use the liquid concat filter to merge it into the notebooks array.

This probably isn’t the sort of exciting news that will cause you to go refactoring all of your liquid split functions, but at least next time you can more easily creating a more complex data structure.

Being able to preserve your data types is extremely handy because it saves you having to use extra complex logic to determine what datatype a variable should be, or used to be.