← Home ← Code

Limitations in Liquid

I use LiquidJS on a daily basis. I have found that it's one of the most usable templating languages out there (or, well, Liquid is; LiquidJS is just one implementation of it). Nunjucks is its closest contender, but I have found it hard to make the switch. I tried Nunjucks a little, yet I found the documentation hard to get through and ultimately it seems a bit more complicated than Liquid. For example, Nunjucks has {% macro %}, but so far I haven't found a good use-case for it. On the other hand, I've seen people run into nasty issues with asynchronous code not working inside macros, and so it has come to look like more of a nuisance than anything.

My biggest issue with Liquid

The worst part of Liquid is doing any remotely complex logic. A simple for loop or if statement is fine. But as soon as you need to process some data before rendering, it starts falling apart. And not a little bit, it's complete disintegration.

For example, creating a new object. This is just… not possible in Liquid. You can do "" | split: "," to hack your way into creating a new empty array, but objects (or "hashes") cannot be instantiated.

The order of operations is also fixed. Operations in Liquid are all expressed as filters, and you are not allowed to use parentheses. That means you need to assign more variables than you really want to, which creates unnecessary bloat.

My ideal world

In an ideal world, I could use JavaScript for my logic. In a recent task, I had two arrays that were linked element-per-element (i.e. the elements at each index belonged together). I needed to sort them based on a property on the items in the first array, though. One way to do it is

{% liquid
	# given `main_array` and `paired_array`, we sort both
	# by the `age` key on `main_array`'s items.

	assign main_result = "" | split: ","
	assign paired_result = "" | split: ","
	assign length = main_array.length
	for item in main_array
		assign paired_item = paired_array[forloop.index0]
		assign index = main_result.length
		for sorted_item in main_result
			if sorted_item.age <= item.age
				continue
			endif
			assign index = forloop.index0
			break
		endfor
		assign main_left = main_result | slice: 0, index
		assign paired_left = paired_result | slice: 0, index
		assign main_right = main_result | slice: index, length
		assign paired_right = paired_result | slice: index, length
		assign main_result = main_left | push: item
		assign main_result = main_result | concat: main_right
		assign paired_result = paired_left | push: paired_item
		assign paired_result = paired_result | concat: paired_right
	endfor

	# done! Out come `main_result` and `paired_result`.
%}

This is awkward. And very verbose. I would love to just be able to do

{% js
	// We're given `mainArray` and `pairedArray` here
	const map = new Map()
	mainArray.forEach((item, index) => {
		map.set(item, pairedArray[index])
	})
	const mainResult = mainArray.toSorted((a, b) => a.age - b.age)
	const pairedResult = mainResult.map(item => map.get(item))
%}

It's still a bit awkward (that's because the operation itself is), but ultimately this would help so much. A lot of the time, I find myself forced into a restricted mental model for Liquid simply because it lacks many constructs or features of a "regular" programming language. If I could use JavaScript directly, I could also easily instantiate new objects, and do essentially anything else I want. Unfortunately, it's currently very hard (if at all possible) to make this type of syntax happen, and even if it was, it might not be particularly performant.

New filters

A lot of these issues can be solved by adding filters. For example, to solve the issue of hash creation, one can write a simple parse_json filter together with a set_key filter and do

{% assign hash = "{}"
	| parse_json
	| set_key: "message", "hello"
	| set_key: "name", "world"
%}
{{ hash.message }} {{ hash.name }}

In fact, it'd be nice to have a package for filters such as this; it would be pretty helpful in most medium-plus projects. Here are some ideas for filters I think could be useful:

Actually, quite a few native JavaScript methods on primitives or basic objects are not supported in Liquid. It should be fairly straight-forward to implement most of these. Might be a fun weekend project!