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
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:
parse_json
set_key
and delete_key
exec
for inline JS functions
insert_at
and/or splice
regex_match
and regex_match_all
substr
(for start-end index substrings)
index_of
and last_index_of
pad_start
and pad_end
repeat
and/or fill
flat
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!