views:

44

answers:

2

I've been playing around with the Liquid templating engine this weekend, and I wonder if the following is possible.

Say I have a latest_posts method in a Blog model, which I can pass an integer to to get the latest N posts. Is it possible to use that method in a liquid template?

For example:

class Blog

  has_many :posts

  def latest_posts(n)
    posts.latest(n) # using a named scope
  end

  def to_liquid(*args)
    {
      'all_posts' => posts.all,  # allows me to use {% for posts in blog.all_posts %}
      'last_post' => post.last,  # allows me to use {% assign recent = blog.last_post %}
      'latest_posts' => posts.latest_posts(args[0])  # how do I pass variables to this?
    }
  end

end

In the simplified example above, in my liquid templates I can use blog.all_posts and blog.last_post, but have no idea how I would do anything like blog.latest_posts: 10.

Can anyone point my in the right direction?

One idea I thought of was to create a Liquid filter and pass both the Blog object and an integer to that. Something like:

{% for post in blog | latest_posts(10) %}
  • but haven't tried that yet as feel like I'm stabbing around in the dark a bit. Would appreciate some help from more experienced Liquid users.
A: 

I think liquid is a fantastic template system. Congrats on investigating/using it.

By default, none of the model's methods are available to the liquid template. This is a good thing. You then specify which methods shall be available. (A white list.)

I use an extension to Module which was sent on the mailing list. Complete extension is below. It handles the Liquid::Drop creation for you by adding a simple #liquid_methods method to classes and modules.

Then, in your models, just do:

class Blog
  # id
  # name
  has_many :posts

  def latest_posts(n)
    posts.latest(n) # using a named scope
  end

  def latest_10_posts;latest_posts(10); end

  liquid_methods :id, :name, :posts, :latest_10_posts
end

I'm not sure offhand how/if you can pass params into a drop. Ask on the Liquid mailing list. I think you can.

Added: I now re-read your question and see that you really want to send in that param to the method. You can send in more than one argument/parameter to a Liquid filter. So you could have a filter:

# Define as a Liquid filter
def latest_posts(blog, n)
  blog.latest(n)
end

# then call the filter in a template:
{{ blog2 | latest_posts: 10 }}  
# Note that the second param is after the filter name.

In this example, also remember that you'll need to declare liquid methods in the Post class too.

Here is the module extension.

# By dd -- http://groups.google.com/group/liquid-templates/browse_thread/thread/bf48cfebee9fafd9
# This extension is usesd in order to expose the object of the implementing class
# to liquid as it were a Drop. It also limits the liquid-callable methods of the instance
# to the allowed method passed with the liquid_methods call
# Example:
#
# class SomeClass
#   liquid_methods :an_allowed_method
#
#   def an_allowed_method
#     'this comes from an allowed method'
#   end
#   def unallowed_method
#     'this will never be an output'
#   end
# end
#
# if you want to extend the drop to other methods you can define more methods
# in the class <YourClass>::LiquidDropClass
#
#   class SomeClass::LiquidDropClass
#     def another_allowed_method
#       'and this is another allowed method'
#     end
#   end
# end
#
# usage:
# @something = SomeClass.new
#
# template:
# {{something.an_allowed_method}}{{something.unallowed_method}}{{something.another_allowed_method}}
#
# output:
# 'this comes from an allowed method and this is another allowed method'
#
# You can also chain associations, by adding the liquid_method calls in the
# association models.
#
class Module

  def liquid_methods(*allowed_methods)
    drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end"
    define_method :to_liquid do
      drop_class.new(self)
    end

    drop_class.class_eval do
      allowed_methods.each do |sym|
        define_method sym do
          @object.send sym
        end
      end
      def initialize(object)
        @object = object
      end
    end

  end
end 
Larry K
aaronrussell
+1  A: 

Answering my own question here, I found a solution documented in the Liquid groups pages.

Essentially, I needed to create a drop for the latest posts - a LatestPostsDrop - and kind of hack passing a variable to it using the before_method method. Here is the complete solution:

class Blog

  has_many :posts

  def latest_posts
    LatestPostsDrop.new(posts)
  end

  def to_liquid
    {
      'all_posts' => posts.all,
      'last_post' => post.last,
      'latest_posts' => latest_posts
    }
  end

end

class LatestPostsDrop < Liquid::Drop

  def initialize(posts)
    @posts = posts
  end

  def before_method(num)
    @posts.latest(num)    # Post.latest is a named scope
  end

end

Doing the above, allows you to iterate through any number of latest posts using something like:

{% for post in blog.latest_posts.10 %}  # the last attribute can be any integer
  <p>{{ post.title }}</p>
{% endfor %}

It seems a bit hacky, but it works :)

aaronrussell
Thanks for the info on the before_method. I agree that it's a bit hacky but remember that the focus for Liquid is the template, not the machinery behind the template. The intent is to enable other sets of people to use just the template language to make useful/sophisticated views of data in a safe way. I think its very good for that--both my customers and their contractors use Liquid templates with my SAAS data.
Larry K