Skip to content

Using Liquid without Rails

candlerb edited this page Aug 12, 2010 · 14 revisions

Want to use Liquid along with some plain old Ruby code?
First, of course, you need to tell Ruby about the Liquid library.
Assuming that you’re in the liquid directory, as cloned from GitHub,
just require 'lib/liquid'.

The big secret, however, is creating a to_liquid method for any classes you want Liquid to access.

Let’s say you have a Product class:

class Product
  attr_accessor :name, :price

  def initialize(name,price)
    @name  = name
    @price = price
  end
end

And you have a chunk of text that you want to Liquidize by inserting some details about your products:

sentence = "I'm running to the store with {{ product.price }} dollars in my pocket "
sentence += "to buy a {{ product.name }}."

And you create a product and parse the sentence:

my_purchase = Product.new('box of sausages', 20)
puts Liquid::Template.parse(sentence).render('product' => my_purchase)

You excitedly execute your code, expecting to see your beautiful new sentence, but instead you get:

I'm running to the store with Liquid error:
undefined method `to_liquid' for #<Product:0x1388c08 ...

to_liquid ???
Where did that come from?
Turns out you need to add a method to your class that returns your instance variables as part of a hash.
(Liquid was built to be very paranoid about not letting people access something they’re not supposed to,
so you need to explicitly let Liquid know how to access everything in your object.)
So, we update the Product class description to include that method:

class Product
  attr_accessor :name, :price

  def initialize(name, price)
    @name  = name
    @price = price
  end

  def to_liquid
    { "name"  => self.name,
      "price" => self.price }
  end
end

Run that again and presto:

I'm running to the store with 20 dollars in my pocket to buy a box of sausages.

However, that’s a bit verbose, so you may want to use liquid_methods, instead:

class Product
  attr_accessor  :name, :price
  liquid_methods :name, :price

  def initialize(name, price)
    @name  = name
    @price = price
  end
end

(Thanks to Tom’s Jekyll code for helping me figuring this out.)

A bit more detail

Every variable rendered by liquid must either:

  • implement a to_liquid method; or
  • be a Proc. This is called with the Liquid::Context as a parameter and must return an object which responds to to_liquid

Liquid defines to_liquid for the following standard classes: String, Array, Hash, Numeric, Time, DateTime, Date, TrueClass, FalseClass, NilClass. For each of these, to_liquid just returns the object itself. (See: lib/liquid/extensions.rb)

If the value returned by to_liquid is inserted into the output, it will be further converted with to_s (actually this is done implicitly by Array#join – see lib/liquid/template.rb)

When used in a chained expression like foo.bar or foo['bar'], the object returned by to_liquid must either:

  • respond to has_key? and [] (i.e. duck-type like a Hash); or
  • respond to fetch and [] with an Integer argument (i.e. duck-type like an Array); or
  • respond to size or first or last

In each case the object returned must also respond to to_liquid or be a Proc. In the latter case, it will only be called if the previous element in the chain also responds to []= – that is, if the value can be cached in the parent object.

(See: lib/liquid/context.rb)

>> assigns = {"foo"=>{"bar"=>{"baz"=>lambda { |x| Time.now } }}}
=> {"foo"=>{"bar"=>{"baz"=>#<Proc:0xb7cd9af8@(irb):11>}}}
>> t = Liquid::Template.parse("{{ foo.bar.baz }}")
=> #<Liquid::Template:0xb7d2e738 ... >
>> t.render(assigns)
=> "Fri Jun 05 11:23:56 +0100 2009"
>> assigns
=> {"foo"=>{"bar"=>{"baz"=>Fri Jun 05 11:23:56 +0100 2009}}}