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 publically.

Trying to jump into the libary 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 specifed, 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.

1 Response to “Liquids leaking from a Developer”

  1. wesr Says:

    Thank you SO much! You should seriously consider seeing if you can contribute this to the Liquid docs.

Sorry, comments are closed for this article.