Liquids leaking from a Developer
After eyeing Liquid for quite some time, we decided to use it on a project to allow a customer template his app from the admin side. After seeing a lot of documentation for Designers and Templaters, I felt there was something needed from a developer’s perspective.
I want to extend a gracious “Thank You” to Jaded Pixel the creators behind Shopify for taking to the time to extract this library from their own work, and provide it publicly.
Trying to jump into the library was a little difficult, and I learned a lot from just reading the source and the documentation, as well as the code for Mephisto, one of the few open source projects that I know uses Liquid.
Using Liquid
Using liquid is pretty straightforward. You have Liquid parse a template, and then render it giving the appropriate information
Liquid::Template.parse(some_content_as_a_string).render(assigns, :registers => registers)
You notice that I pass Liquid two other items. assigns
is a hash of available variables, objects or drops that the template can reference. registers
is a hash of variables that are accessible from Drops, Tags, and Filters. Think of assigns
as exposed to the template, and registers
only used within the back-end processing of the template.
Liquid::Template.parse(some_content_as_a_string).render({"foo" => "bar"}, :registers => { "something" => "only in the backend"})
... in the template ...
{{ foo }} # => "bar"
{{ something }} # => ""
{{ something_else_wacky }} # => ""
When passing an object in the assigns, it will check either the @to_liquid@ of the object, or check to see if it’s a Drop
Object#to_liquid
Most objects are expected to provide a to_liquid
method that will convert itself into a hash which will permit the template access information from the object. No methods will be exposed to the outside, which is convenient for the sake of security.
Keep in mind, you don’t need to provide a 1 to 1 mapping to liquid exposed methods to real methods. You can create additional items for the view.
class Dog
def bark
"woof"
end
def something_secure
"don't touch"
end
def to_liquid
{ "bark" => "woof", "fetch" => "some newspaper"}
end
end
Liquid::Template.parse(some_content_as_a_string).render({"dog" => Dog.new})
... in template ...
{{ dog.bark }} # => "woof"
{{ dog.something_secure}} # => ""
{{ dog.fetch }} # => "some newspaper"
Drops
Sometimes creating to_liquid
is either more work than necessary, or you notice a lot of code regarding opening an object. Drops will help in these situations. A drop is an object that will expose all public methods to the template. I could easily rewrite the previous code as a drop (without having to write a to_liquid
method)
class DogDrop < Liquid::Drop
def initialize(dog)
@dog = dog
end
def bark
@dog.bark
end
def fetch
"some newspaper"
end
end
Liquid::Template.parse(some_content_as_a_string).render({"dog" => DogDrop.new(Dog.new)})
… in the template …
{{ dog.bark }} # => "woof"
{{ dog.something_secure}} # => ""
{{ dog.fetch }} # => "some newspaper"
{{ dog.something_wacky }} # => ""
Filters
Filters are a way of manipulating the output the input passed to it. They can be chained in any order without serious harm (though obviously you can write them in that fashion, I wouldn’t suggest it).
module MyFilters
def uppercase(text)
text.upcase
end
def reverse(text)
text.reverse
end
def replace_chars(text, item_to_replace, substitute)
text.gsub(item_to_replace, replace_with)
end
end
... in template ...
{{ dog.bark | uppercase }} # => "WOOF"
{{ dog.bark | reverse }} # => "foow"
{{ dog.bark | uppercase | reverse }} # => "FOOW"
{{ dog.bark | replace_chars: 'w', 'b' }} # => "boof"
To use filters within the templates, you need to register them with Liquid
Liquid::Template.register_filter MyFilters
Tags
These are another core piece of liquid that are generally wrapped in {% %}
. You can see the implementation of control structures like if
, for
, while
blocks. As well as other useful tags such as capture
, include
and assign
. I suggest looking at the source for each of those to see how they are implemented, and how to pass parameters.
Later I will post on how to use tags to implement forms that are templatable by designers. (All credit goes to Mephisto for this one).
Creating forms with tags will essentially allow you to write a form like the following, which places a lot of needed control in the designer’s hands.
{% myform %}
<p>
<label for="some_item">First Item</label> {{ form.first_item }}
</p>
<p>
<label for="some_other_item">Second Item</label> {{ form.second_item }}
</p>
<p>
<input type="submit" name="commit" value="Submit this Form" />
</p>
{% endmyform%}
Filesystems
These are a neat concept that is only used for the include
tags. You set a “filesystem” with Liquid by setting it to an object.
Liquid::Template.file_system = MyFileSystem.new
Liquid will then pass all {% include 'some_include' %}
to the filesystem specified, which allows you to customize where the partial actually resides. For instance, to allow users to create templates from an interface and retrieve them from the database, you can implement a similar filesystem
.
class MyFileSystem
def read_template_file(template_name)
do_something_to_retreive_a_string_from_db(template_name)
end
end
Nothing extraordinary, but very useful. And of course the database isn’t the only place you could store the includes.
All in all, a great library, and I plan to integrate templates with Eventable soon, since it was very successful with our previous project.
Thanks Jaded Pixel.